fix(i18n): second screenshot review: remaining English, numbers and wraps

From the re-shot Russian screenshots:
- Translated: weather frame/imagery status errors, HUD summary provenance
  ("НЕДОСТУПНО: СУДА AIS ОНЛАЙН"), Bhote Koshi map labels, cockpit signal
  distances (КМ). The HUD summary fields are separated with " · " in every
  language, so the style name and view band no longer read as one phrase.
- Numbers follow the locale: cockpit altitude ("26 175"), precipitation.
- No breaks inside codes and dates: ADS-B and similar in layer rows,
  Sentinel-2, cyclone issue dates (word joiner; the UI font has no U+2011),
  no-break space after label colons in cyclone details.
- Layout: count lines under ALPR, short unavailable chip (context
  "status chip"), roomier draw segments, tighter CCTV calibration chips,
  two-line beat titles and media card titles, wrapping weather status.
- Wording: ru advisory is «сводка» (was «бюллетень»; "Advisory center" is
  «Центр циклона по сводке»), Street View is «Просмотр улиц», OPACITY
  «НЕПРОЗР.», synced «синхр.», Area «Зона»; the import dialog no longer says
  "this scene" for several scenes. Glossary updated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-13 19:13:12 +04:00
1 parent 2b6ccad732
commit 54dce5b3bd
32 files changed
+1204 -346

No files matched your search

+3 -2
View File
@@ -17,10 +17,11 @@ import {
import { t, tn, msg } from '../../i18n/index.js';
import { formatQuantity } from '../../ui/unitLabels.js';
// "10-12 15:00 UTC" never breaks across lines.
// "10-12 15:00 UTC" never breaks across lines: no-break spaces, and a word
// joiner after the hyphen (JetBrains Mono has no U+2011 non-breaking hyphen).
const utc = (value) =>
value
? `${value.slice(5, 16).replace('T', '\u00a0')}\u00a0UTC`
? `${value.slice(5, 16).replace('-', '-\u2060').replace('T', '\u00a0')}\u00a0UTC`
: t('Unavailable');
const COVERAGE = msg(
'Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.',
+2 -1
View File
@@ -565,7 +565,8 @@ test('advisory selection uses accessible row descriptors and shared camera hando
1,
);
assert.equal(controls.summary.status, 'Track/cone awaiting advisory 10');
assert.match(controls.info, /09-16\u00a003:00\u00a0UTC/);
// No line break inside the date: a word joiner holds the hyphen.
assert.match(controls.info, /issued 09-\u206016\u00a003:00\u00a0UTC/);
assert.match(controls.info, /: 25\u00a0kt · .*: 1006\u00a0hPa/);
assert.match(controls.infoTitle, /not storm size/);
assert.match(controls.infoTitle, /follows the surface/);
+4 -1
View File
@@ -27,7 +27,10 @@ export const displayMetres = (value) => formatQuantity(value, 'm');
export const READOUT_WORDS = Object.freeze({
date: (day) => displayDay(day, LONG_DAY),
daysAgo: (delta) => formatRelativeTime(delta > 0 ? -delta : 0, 'day'),
sensor: (sensor) => t(sensor),
// "Sentinel-2" never breaks at its hyphen: a word joiner follows it
// (JetBrains Mono has no U+2011 non-breaking hyphen).
sensor: (sensor) =>
t(sensor).replace(/(?<=[\p{L}\p{N}])-(?=[\p{L}\p{N}])/gu, '-\u2060'),
metres: displayMetres,
overview: () => t('overview'),
cloud: (cloud) => {
+5 -1
View File
@@ -1,3 +1,5 @@
import { msg } from '../../i18n/index.js';
/** Resolve the visible surface that can own weather imagery without scene mutation. */
export function resolveImageryHost({ viewer, tileset }) {
if (viewer?.scene?.globe?.show === true)
@@ -7,7 +9,9 @@ export function resolveImageryHost({ viewer, tileset }) {
return { collection: null, kind: 'none' };
}
export const NO_IMAGERY_HOST = 'Hidden by this map source · choose a globe map';
export const NO_IMAGERY_HOST = msg(
'Hidden by this map source · choose a globe map',
);
/** Report imagery suspension without changing or detaching the resolved host. */
export function imageryHostStatus(host) {
+17 -10
View File
@@ -7,7 +7,7 @@ import {
GLOBAL_INFRARED_MAX_GAP_MS,
LIGHTNING_MAX_GAP_MS,
} from './clock.js';
import { t } from '../../i18n/index.js';
import { t, msg } from '../../i18n/index.js';
import { formatScaleLabel } from '../../ui/unitLabels.js';
const STOPS = [
@@ -229,12 +229,14 @@ export function createWeatherLayer({
rendering.setHidden?.(false);
void warmNext(time);
}
error = ok ? null : 'Frame unavailable; previous observation retained';
error = ok
? null
: msg('Frame unavailable; previous observation retained');
if (!ok) stop();
return ok;
} catch {
if (owner === generation && !controller.signal.aborted) {
error = 'Weather imagery unavailable';
error = msg('Weather imagery unavailable');
stop();
}
return false;
@@ -340,7 +342,9 @@ export function createWeatherLayer({
if (!enabled || controller.signal.aborted || request !== controller)
return false;
if (snapshot.unavailable) {
error = 'Weather source unavailable; previous observation retained';
error = msg(
'Weather source unavailable; previous observation retained',
);
stop();
return true;
}
@@ -370,7 +374,7 @@ export function createWeatherLayer({
return !controller.signal.aborted && request === controller && enabled;
} catch (cause) {
if (controller.signal.aborted || request !== controller) return false;
error = cause?.message || 'Weather unavailable';
error = cause?.message || msg('Weather unavailable');
stop();
return true;
} finally {
@@ -524,6 +528,10 @@ export function createWeatherLayer({
})
: t('{minutes}m ago', { minutes: age });
const diagnostic = rendering?.getDiagnostics();
// Errors are stored in English (stats, diagnostics); shown translated.
const problem = error || diagnostic?.error;
const shownProblem = problem ? t(problem) : null;
const shownHost = hostStatus ? t(hostStatus) : null;
const b = manifest?.bounds;
const canvas = viewer?.scene?.canvas;
const point =
@@ -618,9 +626,8 @@ export function createWeatherLayer({
detail,
status:
missing ||
hostStatus ||
error ||
diagnostic?.error ||
shownHost ||
shownProblem ||
(observationDelayed()
? t('Source observations delayed')
: manifest?.stale
@@ -701,8 +708,8 @@ export function createWeatherLayer({
}))
: [],
info: hostHidden
? hostStatus
: `${infoHeading}\n${infoTime}${missing ? `\n${missing}` : ''}${manifest?.stale ? `\n${t('STALE · cached source metadata')}` : ''}${error || diagnostic?.error ? '\n' + (error || diagnostic.error) : ''}\n${infoCoverage}${outside ? `\n${t('Map center is outside source coverage')}` : ''}${infoMotion ? `\n${infoMotion}` : ''}`,
? shownHost
: `${infoHeading}\n${infoTime}${missing ? `\n${missing}` : ''}${manifest?.stale ? `\n${t('STALE · cached source metadata')}` : ''}${shownProblem ? `\n${shownProblem}` : ''}\n${infoCoverage}${outside ? `\n${t('Map center is outside source coverage')}` : ''}${infoMotion ? `\n${infoMotion}` : ''}`,
infoTitle: lightning
? t(
'NOAA/NWS 15-minute lightning density derived from Vaisala NLDN/GLD360. Coverage 110°E across the Pacific/Americas to 0°, 25°S–80°N. Not a live strike count, global coverage or a safety warning.',
+5 -2
View File
@@ -3,6 +3,7 @@ import { orderWeatherImagery } from './imageryOrder.js';
import { imageryHostStatus } from './imageryHost.js';
import { createRasterTileProvider } from './rasterTiles.js';
import { createWeatherShell } from './shellRendering.js';
import { msg } from '../../i18n/index.js';
import { readResponseBytesCapped } from '../../sources/httpBody.js';
import {
acquireInfraredMosaic,
@@ -294,7 +295,9 @@ function createGlobeRendering({
);
}
} else {
lastError = 'Weather tiles unavailable · previous frame retained';
lastError = msg(
'Weather tiles unavailable · previous frame retained',
);
remove(frame);
}
frame.resolve(ok);
@@ -413,7 +416,7 @@ function createGlobeRendering({
return;
}
frame.failed = true;
lastError = 'Some weather tiles unavailable';
lastError = msg('Some weather tiles unavailable');
onChange();
});
// A shown, transparent layer lets Cesium request staging tiles while the last
+2 -1
View File
@@ -1,6 +1,7 @@
import { acquireWeatherImage } from './infraredImage.js';
import { imageryHostStatus } from './imageryHost.js';
import { WEATHER_DETAIL_SIZE, WEATHER_IMAGE_SIZES } from './source.js';
import { msg } from '../../i18n/index.js';
/** Metres above the ellipsoid. Lower shells draw first, so lightning draws last. */
export const WEATHER_SHELL_HEIGHTS = Object.freeze({
@@ -630,7 +631,7 @@ export function createWeatherShell({
if (incoming !== frame) return;
incoming = null;
close(frame);
lastError = 'Weather tiles unavailable · previous frame retained';
lastError = msg('Weather tiles unavailable · previous frame retained');
frame.resolve(false);
scene.requestRender();
onChange();
+8 -5
View File
@@ -11,7 +11,7 @@ import {
createWindInspectionMarker,
WIND_UNITS,
} from './inspection.js';
import { t } from '../../i18n/index.js';
import { t, msg } from '../../i18n/index.js';
import { unitLabel } from '../../ui/unitLabels.js';
/** Format a forecast timestamp explicitly in UTC. */
@@ -174,7 +174,7 @@ export function createWindLayer({
return true;
} catch (cause) {
if (controller.signal.aborted || request !== controller) return false;
error = cause?.message || 'Wind source unavailable';
error = cause?.message || msg('Wind source unavailable');
return true;
} finally {
signal?.removeEventListener('abort', abort);
@@ -260,8 +260,11 @@ export function createWindLayer({
requestedScalar() !== 'none' &&
manifest &&
(!manifest.scalar || manifest.scalar.kind !== overlay);
// Stored in English for diagnostics; shown translated.
const imageryError =
enabled && !loading ? diagnostic?.imageryError : null;
enabled && !loading && diagnostic?.imageryError
? t(diagnostic.imageryError)
: null;
const label = {
none: t('Wind motion'),
speed: t('Wind speed'),
@@ -303,7 +306,7 @@ export function createWindLayer({
? t('Loading forecast')
: preparing
? t('Preparing flow')
: error ||
: (error && t(error)) ||
manifest?.reason ||
imageryError ||
(scalarMissing ? t('Selected field unavailable') : null) ||
@@ -384,7 +387,7 @@ export function createWindLayer({
model: model === 'ifs' ? 'ECMWF IFS' : 'GFS',
field: label,
unit: unitLabel(legendUnit),
})}${historyStatus ? `\n${historyStatus}` : ''}\n${t('Valid: {time}', { time: valid || t('Unavailable') })}${loading ? ` · ${t('loading')}` : preparing ? ` · ${t('preparing')}` : ''}\n${t('Issued: {time}', { time: run || t('Unavailable') })}${manifest?.stale ? ' · STALE' : ''}${error ? '\n' + error : ''}${scalarMissing ? `\n${t('Selected field unavailable · wind remains visible')}` : ''}${imageryError && !scalarMissing ? `\n${t('{error} · wind remains visible', { error: imageryError })}` : ''}`,
})}${historyStatus ? `\n${historyStatus}` : ''}\n${t('Valid: {time}', { time: valid || t('Unavailable') })}${loading ? ` · ${t('loading')}` : preparing ? ` · ${t('preparing')}` : ''}\n${t('Issued: {time}', { time: run || t('Unavailable') })}${manifest?.stale ? ' · STALE' : ''}${error ? `\n${t(error)}` : ''}${scalarMissing ? `\n${t('Selected field unavailable · wind remains visible')}` : ''}${imageryError && !scalarMissing ? `\n${t('{error} · wind remains visible', { error: imageryError })}` : ''}`,
infoTitle: t(
'Surface wind at 10 m. Approximately 1° global grid. Curves follow the 10 m wind field, lifted 12 km for visibility; display height is not weather altitude. View lighting is for readability. Animation shows flow through one fixed forecast; it does not advance time. Color fields drape the globe basemap or the active photorealistic 3D Tiles.',
),
+4 -3
View File
@@ -5,6 +5,7 @@ import {
import { createWindRelief } from './relief.js';
import { orderWeatherImagery } from '../weather/imageryOrder.js';
import { NO_IMAGERY_HOST } from '../weather/imageryHost.js';
import { msg } from '../../i18n/index.js';
import { createWindGpuRendering } from './gpuRendering.js';
import { advectParticle, sampleWind } from './model.js';
import {
@@ -344,7 +345,7 @@ export function createWindRendering({
? !cesium.Primitive
: !collection || !cesium.SingleTileImageryProvider
) {
imageryError = 'Globe imagery unavailable';
imageryError = msg('Globe imagery unavailable');
return;
}
try {
@@ -384,12 +385,12 @@ export function createWindRendering({
imagery.alpha = overlay === 'temperature' ? 1 : 0.85;
if (gpuActive) updateImageryFade(viewer.scene.camera, true);
imageryErrorRemove = provider.errorEvent?.addEventListener(() => {
imageryError = 'Globe field image unavailable';
imageryError = msg('Globe field image unavailable');
});
viewer.scene?.requestRender?.();
} catch {
removeImagery();
imageryError = 'Globe field image unavailable';
imageryError = msg('Globe field image unavailable');
}
}