mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
i18n: fix review findings and polish ru/es strings
Fixes the 51 P0–P2 findings from the full review of the i18n branch and the issues found while checking every new string on screen in ru and es. - Translate errors and statuses that still reached ru/es in English: feed and tile errors, provider statuses, the ALPR chip, the traffic mode chip, voice plan step labels, fire feed values, receiver bands, scene save reasons and the Cesium attribution link. - Show preset city names in the UI language (src/ui/placeNames.js); data and everything sent to the model stay English. - Share layer names between the layer panel, HUD and voice card (src/ui/layerNames.js). - Keep long ru/es strings whole: voice card error lines, analyst titles, the voice tier status, the Nepal evidence button, the ALPR retry chip. English layout is unchanged. - Ask before a language switch drops unsaved work. - Catalog fixes: glossary terms (пуск, пресет), gender agreement and consistent "peticiones" in es, fewer calques in both languages. - Fix English text that was itself wrong (e.g. "Exit cockpit to fly to a weather", raw tool ids in voice plan steps). Catalogs: es/ru 2535 messages each, complete. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8029747b7e
commit
00704ddc89
212 files changed
+9871
-909
No files matched your search
@@ -11,7 +11,7 @@ import {
|
||||
createWindInspectionMarker,
|
||||
WIND_UNITS,
|
||||
} from './inspection.js';
|
||||
import { t, msg } from '../../i18n/index.js';
|
||||
import { t, msg, translateKnown } from '../../i18n/index.js';
|
||||
import { unitLabel } from '../../ui/unitLabels.js';
|
||||
|
||||
/** Format a forecast timestamp explicitly in UTC. */
|
||||
@@ -22,6 +22,25 @@ export function formatWindValidTime(value) {
|
||||
: null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Reasons the server puts in the manifest (server/providers/wind.js). They stay
|
||||
* English in data and are translated where they are shown.
|
||||
*/
|
||||
const SERVER_REASONS = new Set([msg('Wind upstream unavailable')]);
|
||||
const reasonText = (reason) =>
|
||||
SERVER_REASONS.has(reason) ? t(reason) : translateKnown(reason) || null;
|
||||
|
||||
/**
|
||||
* A wind problem in the UI language. Source errors are English data; the
|
||||
* HTTP status line carries a number, so it goes back onto a placeholder.
|
||||
*/
|
||||
function presentWindProblem(problem) {
|
||||
const http = /^Wind HTTP (\d+)$/.exec(problem);
|
||||
if (http)
|
||||
return t('{source} HTTP {status}', { source: t('Wind'), status: http[1] });
|
||||
return t(problem);
|
||||
}
|
||||
|
||||
/** Forecast issue time is source age; valid time is reported separately. */
|
||||
export function windStats(manifest) {
|
||||
const run = Date.parse(manifest?.cycle?.runIso);
|
||||
@@ -29,7 +48,8 @@ export function windStats(manifest) {
|
||||
count: manifest?.grid ? manifest.grid.nx * manifest.grid.ny : 0,
|
||||
lastUpdate: Number.isFinite(run) ? run : null,
|
||||
error:
|
||||
manifest?.reason || (manifest?.unavailable ? 'Wind unavailable' : null),
|
||||
manifest?.reason ||
|
||||
(manifest?.unavailable ? msg('Wind unavailable') : null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -306,8 +326,8 @@ export function createWindLayer({
|
||||
? t('Loading forecast')
|
||||
: preparing
|
||||
? t('Preparing flow')
|
||||
: (error && t(error)) ||
|
||||
manifest?.reason ||
|
||||
: (error && presentWindProblem(error)) ||
|
||||
reasonText(manifest?.reason) ||
|
||||
imageryError ||
|
||||
(scalarMissing ? t('Selected field unavailable') : null) ||
|
||||
(manifest?.stale ? t('Cached forecast · stale') : null),
|
||||
@@ -387,7 +407,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${t(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 ? ` · ${t('STALE')}` : ''}${error ? `\n${presentWindProblem(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.',
|
||||
),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { readWindBody } from '../../sources/windBody.js';
|
||||
|
||||
// Imported by src/tools: no i18n here. The texts stay English data and the
|
||||
// wind layer and layer panel show them translated.
|
||||
const msg = (text) => text;
|
||||
|
||||
/** Create a bounded, cancellable source for the same-origin wind provider. */
|
||||
export function createWindSource({
|
||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||
@@ -8,15 +12,15 @@ export function createWindSource({
|
||||
return {
|
||||
async getSnapshot({ signal, model = 'gfs', overlay = 'none' } = {}) {
|
||||
if (!['gfs', 'ifs'].includes(model))
|
||||
throw new Error('Unknown wind model');
|
||||
throw new Error(msg('Unknown wind model'));
|
||||
if (!['none', 'temperature', 'pressure'].includes(overlay))
|
||||
throw new Error('Unknown weather overlay');
|
||||
throw new Error(msg('Unknown weather overlay'));
|
||||
const query = `model=${model}${overlay === 'none' ? '' : `&overlay=${overlay}`}`;
|
||||
const controller = new AbortController();
|
||||
const abort = () => controller.abort(signal.reason);
|
||||
signal?.addEventListener('abort', abort, { once: true });
|
||||
const timer = setTimeout(
|
||||
() => controller.abort(new Error('Wind request timed out')),
|
||||
() => controller.abort(new Error(msg('Wind request timed out'))),
|
||||
timeoutMs,
|
||||
);
|
||||
const active = controller.signal;
|
||||
@@ -42,8 +46,8 @@ export function createWindSource({
|
||||
scalar === undefined &&
|
||||
scalarError ===
|
||||
(overlay === 'temperature'
|
||||
? 'Temperature field unavailable'
|
||||
: 'Mean sea level pressure field unavailable');
|
||||
? msg('Temperature field unavailable')
|
||||
: msg('Mean sea level pressure field unavailable'));
|
||||
const scalarValid =
|
||||
overlay === 'none'
|
||||
? scalar === undefined && scalarError === undefined
|
||||
@@ -73,7 +77,7 @@ export function createWindSource({
|
||||
`^/api/wind/grid/${model}-[\\w.-]+\\.bin\\?${query}$`,
|
||||
).test(manifest.gridUrl)
|
||||
)
|
||||
throw new Error('Malformed wind manifest');
|
||||
throw new Error(msg('Malformed wind manifest'));
|
||||
const count = grid.nx * grid.ny;
|
||||
const expectedBytes = count * (scalar ? 12 : 8);
|
||||
const gridResponse = await fetchImpl(manifest.gridUrl, {
|
||||
@@ -84,10 +88,10 @@ export function createWindSource({
|
||||
throw new Error(`Wind HTTP ${gridResponse.status}`);
|
||||
const bytes = await readWindBody(gridResponse, expectedBytes, active);
|
||||
if (bytes.byteLength !== expectedBytes)
|
||||
throw new Error('Malformed wind grid');
|
||||
throw new Error(msg('Malformed wind grid'));
|
||||
const values = new Float32Array(bytes.buffer);
|
||||
if (!values.every(Number.isFinite))
|
||||
throw new Error('Malformed wind grid');
|
||||
throw new Error(msg('Malformed wind grid'));
|
||||
return {
|
||||
...manifest,
|
||||
u: values.slice(0, count),
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setLocale } from '../../i18n/index.js';
|
||||
import { createWindLayer } from './index.js';
|
||||
|
||||
test('stale wind forecast is marked in the UI language (K-40)', async () => {
|
||||
const rendering = Object.fromEntries(
|
||||
['attach', 'start', 'stop', 'clear', 'destroy', 'setField'].map((n) => [
|
||||
n,
|
||||
() => {},
|
||||
]),
|
||||
);
|
||||
rendering.getDiagnostics = () => ({});
|
||||
const layer = createWindLayer({
|
||||
feed: {
|
||||
getSnapshot: async () => ({
|
||||
model: 'gfs',
|
||||
grid: { nx: 2, ny: 2 },
|
||||
cycle: {
|
||||
runIso: '2026-09-14T12:00:00Z',
|
||||
validIso: '2026-09-14T18:00:00Z',
|
||||
},
|
||||
stale: true,
|
||||
reason: 'Wind upstream unavailable',
|
||||
}),
|
||||
},
|
||||
createRendering: () => rendering,
|
||||
});
|
||||
layer.init({ container: {} });
|
||||
layer.enable();
|
||||
await layer.update();
|
||||
try {
|
||||
assert.match(layer.getRowControls().info, / · STALE(\n|$)/);
|
||||
for (const [locale, word] of [
|
||||
['es', 'DESACTUALIZADO'],
|
||||
['ru', 'УСТАРЕЛ'],
|
||||
]) {
|
||||
setLocale(locale);
|
||||
const info = layer.getRowControls().info;
|
||||
assert.doesNotMatch(info, /STALE/);
|
||||
assert.match(info, new RegExp(word));
|
||||
}
|
||||
} finally {
|
||||
setLocale('en');
|
||||
layer.destroy();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// K-38: wind source failures are English data (src/layers/wind/source.js) and
|
||||
// are shown in the UI language; the HTTP line keeps its status number.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createWindLayer } from './index.js';
|
||||
import { setLocale } from '../../i18n/index.js';
|
||||
|
||||
function harness(problem) {
|
||||
const rendering = Object.fromEntries(
|
||||
[
|
||||
'attach',
|
||||
'start',
|
||||
'stop',
|
||||
'clear',
|
||||
'destroy',
|
||||
'setField',
|
||||
'setOptions',
|
||||
].map((name) => [name, () => {}]),
|
||||
);
|
||||
rendering.getDiagnostics = () => ({});
|
||||
const layer = createWindLayer({
|
||||
feed: {
|
||||
getSnapshot: async () => {
|
||||
throw new Error(problem);
|
||||
},
|
||||
},
|
||||
createRendering: () => rendering,
|
||||
});
|
||||
layer.init({ container: {} });
|
||||
layer.enable();
|
||||
return layer;
|
||||
}
|
||||
|
||||
async function statusFor(problem) {
|
||||
const layer = harness(problem);
|
||||
await layer.update();
|
||||
return layer.getRowControls().summary.status;
|
||||
}
|
||||
|
||||
test('en: wind source failures read as before', async () => {
|
||||
assert.equal(await statusFor('Wind HTTP 503'), 'Wind HTTP 503');
|
||||
assert.equal(
|
||||
await statusFor('Wind request timed out'),
|
||||
'Wind request timed out',
|
||||
);
|
||||
});
|
||||
|
||||
for (const locale of ['es', 'ru']) {
|
||||
test(`${locale}: wind source failures are shown in the UI language`, async () => {
|
||||
setLocale(locale);
|
||||
try {
|
||||
const http = await statusFor('Wind HTTP 503');
|
||||
assert.notEqual(http, 'Wind HTTP 503');
|
||||
assert.match(http, /503/);
|
||||
for (const problem of [
|
||||
'Wind request timed out',
|
||||
'Malformed wind manifest',
|
||||
'Malformed wind grid',
|
||||
])
|
||||
assert.notEqual(await statusFor(problem), problem);
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createWindLayer, windStats } from './index.js';
|
||||
import { setLocale, translateKnown } from '../../i18n/index.js';
|
||||
import { windProxy } from '../../../server/providers/wind.js';
|
||||
|
||||
function harness(feed) {
|
||||
const rendering = Object.fromEntries(
|
||||
[
|
||||
'attach',
|
||||
'start',
|
||||
'stop',
|
||||
'clear',
|
||||
'destroy',
|
||||
'setField',
|
||||
'setOptions',
|
||||
].map((name) => [name, () => {}]),
|
||||
);
|
||||
rendering.getDiagnostics = () => ({});
|
||||
const layer = createWindLayer({ feed, createRendering: () => rendering });
|
||||
layer.init({ container: {} });
|
||||
layer.enable();
|
||||
return layer;
|
||||
}
|
||||
|
||||
async function serverManifestOnUpstreamFailure() {
|
||||
let handler;
|
||||
const failing = async () => {
|
||||
throw new Error('upstream down');
|
||||
};
|
||||
windProxy({ models: { gfs: failing, ifs: failing } }).configureServer({
|
||||
middlewares: { use: (_path, fn) => (handler = fn) },
|
||||
});
|
||||
let body = '';
|
||||
await handler(
|
||||
{ method: 'GET', url: '/manifest?model=gfs&overlay=none' },
|
||||
{ writeHead() {}, end: (b) => (body = b), once() {}, removeListener() {} },
|
||||
);
|
||||
return JSON.parse(body);
|
||||
}
|
||||
|
||||
test('en: the upstream failure reason reads as before (K-17)', async () => {
|
||||
const manifest = await serverManifestOnUpstreamFailure();
|
||||
const layer = harness({ getSnapshot: async () => manifest });
|
||||
await layer.update();
|
||||
assert.equal(
|
||||
layer.getRowControls().summary.status,
|
||||
'Wind upstream unavailable',
|
||||
);
|
||||
});
|
||||
|
||||
for (const locale of ['es', 'ru']) {
|
||||
test(`${locale}: upstream failure status is shown in the UI language`, async () => {
|
||||
const manifest = await serverManifestOnUpstreamFailure();
|
||||
assert.equal(manifest.reason, 'Wind upstream unavailable');
|
||||
setLocale(locale);
|
||||
try {
|
||||
const layer = harness({ getSnapshot: async () => manifest });
|
||||
await layer.update();
|
||||
const status = layer.getRowControls().summary.status;
|
||||
const stats = translateKnown(windStats(manifest).error);
|
||||
assert.notEqual(status, 'Wind upstream unavailable');
|
||||
assert.notEqual(stats, 'Wind upstream unavailable');
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
|
||||
test(`${locale}: a stale cached forecast is reported in the UI language`, async () => {
|
||||
// Shape the server returns when a cached forecast is kept after a failure.
|
||||
const manifest = {
|
||||
model: 'gfs',
|
||||
grid: { nx: 2, ny: 2 },
|
||||
cycle: {
|
||||
runIso: '2026-09-14T12:00:00Z',
|
||||
validIso: '2026-09-14T18:00:00Z',
|
||||
},
|
||||
stale: true,
|
||||
reason: 'Wind upstream unavailable',
|
||||
};
|
||||
setLocale(locale);
|
||||
try {
|
||||
const layer = harness({ getSnapshot: async () => manifest });
|
||||
await layer.update();
|
||||
const status = layer.getRowControls().summary.status;
|
||||
assert.doesNotMatch(status, /[A-Za-z]{4,} upstream unavailable/);
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user