fix(i18n): keep values the AI tools read in English

- Satellite class in analyst records and voice payloads stays English
  ("STATION", "COMMS"), so voice filters on the class work in every
  language; cards and the legend still show the translated label.
- Layer source names (OSM routing, ALPR sources) stay English in layer
  state; the layer row translates them.
- New translateKnown(): catalog text when there is an entry, the given text
  otherwise, with no warning. The layer row uses it for loading labels and
  errors, Recent Imagery for its notices.
- Mark the remaining Recent Imagery and wind messages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-13 20:09:45 +04:00
1 parent 0c647a791b
commit 2c74057b5e
13 files changed
+60 -20

No files matched your search

+7 -2
View File
@@ -167,9 +167,14 @@ export function satelliteClassColor(group) {
* @param {{ isIss?: boolean }} [options] Whether this is the ISS.
* @returns {string} Display label.
*/
export function satelliteClassLabel(group, { isIss = false } = {}) {
export function satelliteClassLabel(
group,
{ isIss = false, localize = true } = {},
) {
const { klass, subtype } = satelliteClassOf(group, { isIss });
const base = t(CLASS_DISPLAY_KEYS[klass].label);
// localize: false keeps the English label for data the AI tools query.
const label = CLASS_DISPLAY_KEYS[klass].label;
const base = localize ? t(label) : label;
return subtype ? `${base} · ${subtype}` : base;
}
+12
View File
@@ -109,6 +109,18 @@ export function tn(count, one, other, values) {
return interpolate(pattern, { ...values, count });
}
/**
* Translate text that may be ours or may come from a provider: a catalog
* entry is used when there is one, anything else shows as given, without a
* missing-translation warning.
*/
export function translateKnown(text) {
const catalog = CATALOGS[currentLocale];
if (!catalog || typeof text !== 'string') return text;
const entry = catalog[text];
return typeof entry === 'string' ? entry : text;
}
/** Mark English text that is stored in data and translated later by t(). */
export function msg(text) {
return text;
+3 -1
View File
@@ -2030,5 +2030,7 @@
"subject": "sujeto",
"{value}m": "{value} m",
"Google 3D tiles unavailable — check the key's API restrictions, quota, or network": "Teselas de Google 3D no disponibles: revisa las restricciones de API de la clave, la cuota o la red",
"Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Necesita {envVars} — añádela en Ajustes de proveedores — o un token de Cesium ion para la ruta alojada en ion"
"Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Necesita {envVars} — añádela en Ajustes de proveedores — o un token de Cesium ion para la ruta alojada en ion",
"Comparison in use by another scene": "La comparación está en uso por otra escena",
"A location reading needs a loaded forecast and the Earth at the center of the view.": "Una lectura en un punto necesita un pronóstico cargado y la Tierra en el centro de la vista."
}
+3 -1
View File
@@ -2062,5 +2062,7 @@
"subject": "объект",
"{value}m": "{value} м",
"Google 3D tiles unavailable — check the key's API restrictions, quota, or network": "Тайлы Google 3D недоступны — проверьте ограничения API ключа, квоту или сеть",
"Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Нужен {envVars} — добавьте его в «Настройках провайдеров» — или токен Cesium ion для маршрута через ion"
"Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Нужен {envVars} — добавьте его в «Настройках провайдеров» — или токен Cesium ion для маршрута через ion",
"Comparison in use by another scene": "Сравнение занято другой сценой",
"A location reading needs a loaded forecast and the Earth at the center of the view.": "Для показания в точке нужен загруженный прогноз и Земля в центре вида."
}
+2 -1
View File
@@ -294,7 +294,8 @@ export function createAlprCamerasLayer({ source, services } = {}) {
id: LAYER_ID,
name: 'ALPR Cameras',
icon: '📷',
source: t(source.label || msg('Mapped camera locations')),
// English in layer state; the layer row translates it.
source: source.label || msg('Mapped camera locations'),
updateInterval: 0,
statsRefreshInterval: 1000,
init(viewer) {
+2 -1
View File
@@ -557,7 +557,8 @@ export function directionsStats(state) {
armed,
pointerBlocked,
} = state;
const source = t('OSM routing');
// English in layer state; the layer row translates it.
const source = 'OSM routing'; // i18n-ignore: source name, translated by layerPanel
if (pointerBlocked) {
return {
count: route?.steps.length || 0,
+1 -1
View File
@@ -47,7 +47,7 @@ export const SPLIT_PUBLISH_MS = 400;
/** The three modes, in share-link order (`m` = index). */
export const MODES = Object.freeze(['image', 'basemap', 'ab']);
const COMPARISON_IN_USE = 'Comparison in use by another scene';
const COMPARISON_IN_USE = msg('Comparison in use by another scene');
// Kept English in the snapshot (AI tools read it); the card translates them.
const COMPARISON_UNAVAILABLE = msg('Esri map unavailable · no swipe');
const COMPARISON_NEEDS_GLOBE = msg('Swipe needs a globe map');
+3 -1
View File
@@ -37,7 +37,9 @@ export function mapAnalystRecord(raw) {
// Empty records stay class-less; the class table's fallback is VISUAL and
// must not leak onto a row that has no satellite identity.
satelliteClass:
noradId || group ? satelliteClassLabel(group, { isIss }) : null,
noradId || group
? satelliteClassLabel(group, { isIss, localize: false })
: null,
group,
};
}
+4 -1
View File
@@ -239,7 +239,10 @@ export function createTracking({ state: layerState, services, parts, source }) {
name,
operator: '',
noradId: String(noradId),
class: satelliteClassLabel(sat.group, { isIss: noradId === ISS_NORAD }),
class: satelliteClassLabel(sat.group, {
isIss: noradId === ISS_NORAD,
localize: false,
}),
altitude:
altitudeKm === null ? '' : `${altitudeKm.toLocaleString('en-US')} km`,
},
+2 -1
View File
@@ -65,8 +65,9 @@ export function inspectWindAtCenter(
model,
validTime,
status,
explanation:
explanation: msg(
'A location reading needs a loaded forecast and the Earth at the center of the view.',
),
};
const location = cesium.Cartographic.fromCartesian(point, ellipsoid);
const lon = cesium.Math.toDegrees(location.longitude);
+7 -1
View File
@@ -6,7 +6,13 @@
*/
/** Guidance shown when the active map stack cannot host draped imagery. */
export const NO_IMAGERY_HOST = 'Hidden by this map source · choose a globe map';
// Marks text for i18n extraction without importing i18n here; the UI
// translates it where it is shown.
const msg = (text) => text;
export const NO_IMAGERY_HOST = msg(
'Hidden by this map source · choose a globe map',
);
/**
* Resolve the imagery layer collection for the active map stack.
+12 -7
View File
@@ -5,7 +5,7 @@ export { layerFeedState } from '../data/feedState.js';
import { GUIDANCE_STATUSES } from '../loadingFeedback.js';
import { keySetupRequirementEnvVars } from '../keySetupCore.mjs';
import { createWeatherPanel } from './weatherPanel.js';
import { t, msg } from '../i18n/index.js';
import { msg, t, translateKnown } from '../i18n/index.js';
// Feed-state enum VALUES are machine identifiers (layerFeedState contract, QA
// hooks, CSS feed-* classes); only the final label value translates, at this
@@ -176,6 +176,8 @@ const SOURCE_LABEL_KEYS = new Map(
msg('NOAA nowCOAST · OBSERVED'),
msg('NASA FIRMS · LIVE'),
msg('OSM routing'),
msg('Mapped camera locations'),
msg('OpenStreetMap · community mapped'),
].map((key) => [key, key]),
);
@@ -721,8 +723,9 @@ export class LayerPanel {
{ source },
);
}
const presentedError =
stats.error || stats.lastError || stats.managerRefreshError;
const presentedError = translateKnown(
stats.error || stats.lastError || stats.managerRefreshError,
);
if (presentedError) {
if (typeof stats.retryInSec === 'number' && stats.retryInSec > 0) {
return t('{state} · {source} · {detail} · retry {seconds}s', {
@@ -751,14 +754,14 @@ export class LayerPanel {
if (stats.loading) {
const loadingLabel =
typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
? stats.loadingLabel.trim()
? translateKnown(stats.loadingLabel.trim())
: t('loading...');
return t('{source} · {loadingLabel}', { source, loadingLabel });
}
if (feedState === 'fallback') {
const detail =
typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
? stats.loadingLabel.trim()
? translateKnown(stats.loadingLabel.trim())
: stats.coverage || ago;
return t('{state} · {source} · {detail}', {
state: stateLabel,
@@ -803,7 +806,7 @@ export class LayerPanel {
if (typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()) {
return t('{source} · {loadingLabel}', {
source,
loadingLabel: stats.loadingLabel.trim(),
loadingLabel: translateKnown(stats.loadingLabel.trim()),
});
}
return t('{source} · {ago}', { source, ago });
@@ -861,7 +864,9 @@ export class LayerPanel {
? t('UNAVAILABLE', { context: 'status chip' })
: stateText;
const keyGuidance = unavailable
? String(layer.stats.error || t('Data source unavailable'))
? String(
translateKnown(layer.stats.error) || t('Data source unavailable'),
)
: layerKeyRequirementTooltip(layer);
// Name the missing key on the control itself: a row reading KEY REQUIRED
// without saying WHICH key leaves a dead control and no next step. Empty
+2 -2
View File
@@ -8,7 +8,7 @@
* strip and selection bar are this adapter's own keyed DOM. Source toggles
* live on the data-panel row.
*/
import { t, tn, msg, formatNumber } from '../i18n/index.js';
import { formatNumber, msg, t, tn, translateKnown } from '../i18n/index.js';
import { createRailCardBlocks } from './railCardBlocks.js';
import { createRailCards } from './railCards.js';
import { createImagerySplit } from './imagerySplit.js';
@@ -39,7 +39,7 @@ function localizeNotice(text) {
if (!text) return '';
if (LAYER_NOTICES.has(text)) return t(text);
const tooLarge = TOO_LARGE.exec(text);
if (!tooLarge) return text;
if (!tooLarge) return translateKnown(text);
const values = {
size: formatQuantity(Number(tooLarge[2].replace(/,/g, '')), 'km', {
useGrouping: true,