mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
i18n: fix the final pre-merge review findings
Fixes the P1 and the confirmed P2 findings of the final review of the
i18n branch.
- CI: the air-quality model no longer imports i18n (portable module),
so check:boundaries passes again.
- Air quality layer: named in the layer panel, HUD and voice; its
Weather card, bands and errors translate. A test keeps the layer-name
table in step with the layer registry.
- Layer statuses that still reached ru/es in English: GNSS, CelesTrak,
Transit loading and feed errors, launches, perimeters, ALPR, ECCC,
wind "field unavailable", Local ADS-B streaming lines.
- Voice and HUD: preset ids show city names ("Fly to San Francisco"),
the HUD city translates, cockpit refusals cover frame, aircraft and
satellite; empty Transit notes translate on the analyst card.
- tn() keeps a caller's formatted count ("12,345 map objects").
- Cyber HUD: the language button no longer covers the bottom-right
card or the cockpit position readout.
- CCTV buttons wrap only in non-English locales; English layout matches
main again.
- Switching language asks first when Provider Settings holds unsaved
keys.
Catalogs: es/ru 2611 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fada8b161b
commit
3adce33cc6
43 files changed
+1350
-97
No files matched your search
@@ -713,3 +713,24 @@ test('KML and KMZ rows say they keep their own styles instead of showing a color
|
||||
assert.ok(!gpx.children[0].classList.contains('own-styles'));
|
||||
assert.ok(gpx.children[0].style.background);
|
||||
});
|
||||
|
||||
// N-08: tn() replaces values.count with the raw number, so the
|
||||
// formatNumber(...) passed by the import list is lost: "12345 map objects"
|
||||
// (main: "12,345"). Append to src/ui/geoFileImport.test.mjs (uses its setup(),
|
||||
// file(), rows() helpers).
|
||||
test('the map-object count in a row title keeps digit grouping', async () => {
|
||||
const t = setup({
|
||||
load: async (f) => ({
|
||||
dataSource: {},
|
||||
name: f.name,
|
||||
format: 'geojson',
|
||||
entityCount: 12345,
|
||||
removed: 0,
|
||||
revoke() {},
|
||||
}),
|
||||
});
|
||||
await t.tool.importFiles([file('big.geojson')]);
|
||||
const [row] = rows(t.els);
|
||||
assert.match(row.children[1].title, /12,345 map objects/);
|
||||
t.tool.destroy();
|
||||
});
|
||||
@@ -39,6 +39,7 @@ export const LAYER_NAME_KEYS = Object.freeze({
|
||||
'weather-satellite': msg('Satellite clouds'),
|
||||
'weather-lightning': msg('Lightning density'),
|
||||
'weather-cyclones': msg('Cyclone advisories'),
|
||||
'air-quality': msg('Air quality'),
|
||||
directions: msg('Directions'),
|
||||
'alpr-cameras': msg('ALPR Cameras'),
|
||||
});
|
||||
@@ -116,6 +117,8 @@ const SOURCE_LABEL_KEYS = new Map(
|
||||
msg('OSM routing'),
|
||||
msg('Mapped camera locations'),
|
||||
msg('OpenStreetMap · community mapped'),
|
||||
msg('Decoder feeds'),
|
||||
msg('WebUSB + decoder feeds'),
|
||||
].map((key) => [key, key]),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { APPLICATION_LAYER_METADATA } from '../app/constructCatalog.js';
|
||||
import { LAYER_NAME_KEYS, PANEL_LABEL_KEYS } from './layerNames.js';
|
||||
|
||||
// Layers whose module name is a proper name and stays as it is in every
|
||||
// language: the Bhote Koshi pair belong to a scripted scene.
|
||||
const UNTRANSLATED_NAMES = new Set(['bhote-koshi-2026', 'bhote-koshi-locator']);
|
||||
|
||||
const catalogIds = APPLICATION_LAYER_METADATA.map((layer) => layer.id);
|
||||
|
||||
test('every catalog layer has a translatable name', () => {
|
||||
const missing = catalogIds.filter(
|
||||
(id) =>
|
||||
!UNTRANSLATED_NAMES.has(id) &&
|
||||
!Object.hasOwn(LAYER_NAME_KEYS, id) &&
|
||||
!Object.hasOwn(PANEL_LABEL_KEYS, id),
|
||||
);
|
||||
assert.deepEqual(
|
||||
missing,
|
||||
[],
|
||||
'add the layer to LAYER_NAME_KEYS in src/ui/layerNames.js',
|
||||
);
|
||||
});
|
||||
|
||||
test('the name tables list only catalog layers', () => {
|
||||
const unknown = [
|
||||
...Object.keys(LAYER_NAME_KEYS),
|
||||
...Object.keys(PANEL_LABEL_KEYS),
|
||||
...UNTRANSLATED_NAMES,
|
||||
].filter((id) => !catalogIds.includes(id));
|
||||
assert.deepEqual(unknown, []);
|
||||
});
|
||||
+97
-5
@@ -19,6 +19,7 @@ import {
|
||||
getLocale,
|
||||
msg,
|
||||
t,
|
||||
tn,
|
||||
translateKnown,
|
||||
} from '../i18n/index.js';
|
||||
|
||||
@@ -187,6 +188,27 @@ const FEED_ERROR_PATTERNS = [
|
||||
/^Vector tiles unavailable \(HTTP (\d+)\)$/,
|
||||
([, status]) => t('Vector tiles unavailable (HTTP {status})', { status }),
|
||||
],
|
||||
// Layer statuses with a number or a feed name in them: GNSS
|
||||
// (gnss/source.js), CelesTrak groups (satellites/ingestion.js) and transit
|
||||
// feeds (transit/ingestion.js).
|
||||
[
|
||||
/^GNSS feed unavailable \(HTTP (\d{3})\)$/,
|
||||
([, status]) => t('GNSS feed unavailable (HTTP {status})', { status }),
|
||||
],
|
||||
[
|
||||
/^(\d+) CelesTrak groups? unavailable$/,
|
||||
([, groups]) =>
|
||||
tn(
|
||||
Number(groups),
|
||||
'{count} CelesTrak group unavailable',
|
||||
'{count} CelesTrak groups unavailable',
|
||||
{ count: formatNumber(Number(groups)) },
|
||||
),
|
||||
],
|
||||
[
|
||||
/^(.+) feed unavailable$/,
|
||||
([, feed]) => t('{feed} feed unavailable', { feed: translateKnown(feed) }),
|
||||
],
|
||||
[
|
||||
/^Source snapshot (\d+) min old$/,
|
||||
([, minutes]) => t('Source snapshot {minutes} min old', { minutes }),
|
||||
@@ -222,9 +244,8 @@ const FEED_ERROR_PATTERNS = [
|
||||
],
|
||||
];
|
||||
|
||||
/** A layer's error text in the UI language (see FEED_ERRORS). */
|
||||
export function translateFeedError(text) {
|
||||
if (typeof text !== 'string') return text;
|
||||
/** One error text in the UI language, or null when nothing matches. */
|
||||
function feedErrorText(text) {
|
||||
if (FEED_ERRORS.has(text)) return t(text);
|
||||
const known = translateKnown(text);
|
||||
if (known !== text) return known;
|
||||
@@ -232,17 +253,88 @@ export function translateFeedError(text) {
|
||||
const match = pattern.exec(text);
|
||||
if (match) return format(match);
|
||||
}
|
||||
return text;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A layer's error text in the UI language (see FEED_ERRORS). An error made of
|
||||
* several statuses joined by " · " (Local ADS-B) translates piece by piece.
|
||||
*/
|
||||
export function translateFeedError(text) {
|
||||
if (typeof text !== 'string') return text;
|
||||
const whole = feedErrorText(text);
|
||||
if (whole !== null) return whole;
|
||||
if (!text.includes(ROW_TEXT_SEPARATOR)) return text;
|
||||
return text
|
||||
.split(ROW_TEXT_SEPARATOR)
|
||||
.map((piece) => feedErrorText(piece) ?? translateStatusPiece(piece))
|
||||
.join(ROW_TEXT_SEPARATOR);
|
||||
}
|
||||
|
||||
const ROW_TEXT_SEPARATOR = ' · ';
|
||||
|
||||
// Feed states named in a Local ADS-B feed problem ("feeds 1090, 978 stale").
|
||||
const FEED_PROBLEM_FORMS = Object.freeze({
|
||||
stale: (count, names) =>
|
||||
tn(count, 'feed {names} stale', 'feeds {names} stale', { names }),
|
||||
unreachable: (count, names) =>
|
||||
tn(count, 'feed {names} unreachable', 'feeds {names} unreachable', {
|
||||
names,
|
||||
}),
|
||||
invalid: (count, names) =>
|
||||
tn(count, 'feed {names} invalid', 'feeds {names} invalid', { names }),
|
||||
});
|
||||
|
||||
// Working texts with a number or a name in them arrive as English data, one
|
||||
// piece of a " · " joined line each: transit (transit/queries.js) and Local
|
||||
// ADS-B (localAdsb/status.js). Keep these in step with the producers.
|
||||
const STATUS_PIECE_PATTERNS = [
|
||||
[/^Loading (.+)$/, ([, feeds]) => t('Loading {feeds}', { feeds })],
|
||||
[
|
||||
/^(\d+) heard$/,
|
||||
([, heard]) =>
|
||||
tn(Number(heard), '{count} heard', '{count} heard', {
|
||||
count: formatNumber(Number(heard)),
|
||||
}),
|
||||
],
|
||||
[
|
||||
/^(USB )?(\d+(?:\.\d+)?) msg\/s$/,
|
||||
([, usb, rate]) =>
|
||||
usb
|
||||
? t('USB {rate} msg/s', { rate: formatNumber(Number(rate)) })
|
||||
: t('{rate} msg/s', { rate: formatNumber(Number(rate)) }),
|
||||
],
|
||||
[
|
||||
/^(\d+) feeds? live$/,
|
||||
([, live]) =>
|
||||
tn(Number(live), '{count} feed live', '{count} feeds live', {
|
||||
count: formatNumber(Number(live)),
|
||||
}),
|
||||
],
|
||||
[
|
||||
/^feeds? (.+) (stale|unreachable|invalid)$/,
|
||||
([, names, status]) =>
|
||||
FEED_PROBLEM_FORMS[status](names.split(', ').length, names),
|
||||
],
|
||||
];
|
||||
|
||||
/** One piece of a status line in the UI language, or as it came. */
|
||||
function translateStatusPiece(piece) {
|
||||
const known = translateKnown(piece);
|
||||
if (known !== piece) return known;
|
||||
for (const [pattern, format] of STATUS_PIECE_PATTERNS) {
|
||||
const match = pattern.exec(piece);
|
||||
if (match) return format(match);
|
||||
}
|
||||
return piece;
|
||||
}
|
||||
|
||||
/** A loading label is English data, at times several joined by " · ". */
|
||||
function translateLoadingLabel(label) {
|
||||
return label
|
||||
.trim()
|
||||
.split(ROW_TEXT_SEPARATOR)
|
||||
.map(translateKnown)
|
||||
.map(translateStatusPiece)
|
||||
.join(ROW_TEXT_SEPARATOR);
|
||||
}
|
||||
// A short Latin code joined by hyphens. Longer runs (a URL, a token in an
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// N-03: layer status texts produced without msg()/catalog entries (GNSS,
|
||||
// Transit, CelesTrak, AQHI) stay English in the es/ru DATA LAYERS row.
|
||||
// Place: src/ui/layerStatusLocale.test.mjs
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { setLocale } from '../i18n/index.js';
|
||||
import { LayerPanel } from './layerPanel.js';
|
||||
|
||||
const row = (id, stats) =>
|
||||
LayerPanel.prototype._buildMetaText({ id, enabled: true, stats });
|
||||
const now = Date.now();
|
||||
|
||||
// [layer id, stats, English fragment that must not reach the row]
|
||||
const cases = [
|
||||
[
|
||||
'gnss-integrity',
|
||||
{
|
||||
count: 0,
|
||||
error: 'GNSS feed rate limited (HTTP 429)',
|
||||
source: 'GNSS',
|
||||
lastUpdate: now,
|
||||
},
|
||||
'rate limited',
|
||||
],
|
||||
[
|
||||
'gnss-integrity',
|
||||
{
|
||||
count: 0,
|
||||
error: 'GNSS feed unavailable (HTTP 503)',
|
||||
source: 'GNSS',
|
||||
lastUpdate: now,
|
||||
},
|
||||
'feed unavailable',
|
||||
],
|
||||
[
|
||||
'transit',
|
||||
{
|
||||
count: 0,
|
||||
error: 'GO Transit feed unavailable',
|
||||
source: 'GTFS-RT',
|
||||
lastUpdate: now,
|
||||
},
|
||||
'feed unavailable',
|
||||
],
|
||||
[
|
||||
'transit',
|
||||
{
|
||||
count: 0,
|
||||
loading: true,
|
||||
loadingLabel: 'Loading GO Transit, Metro Transit',
|
||||
source: 'GTFS-RT',
|
||||
},
|
||||
'Loading',
|
||||
],
|
||||
[
|
||||
'satellites',
|
||||
{
|
||||
count: 10,
|
||||
error: '1 CelesTrak group unavailable',
|
||||
source: 'CelesTrak',
|
||||
lastUpdate: now,
|
||||
},
|
||||
'group unavailable',
|
||||
],
|
||||
[
|
||||
'air-quality',
|
||||
{
|
||||
count: 0,
|
||||
error: 'ECCC request timed out',
|
||||
source: 'ECCC',
|
||||
lastUpdate: now,
|
||||
},
|
||||
'request timed out',
|
||||
],
|
||||
];
|
||||
|
||||
for (const locale of ['es', 'ru']) {
|
||||
test(`${locale}: layer status texts are translated in the row`, () => {
|
||||
setLocale(locale);
|
||||
try {
|
||||
const english = cases
|
||||
.map(([id, stats, marker]) => [id, row(id, stats), marker])
|
||||
.filter(([, text, marker]) => text.includes(marker))
|
||||
.map(([id, text]) => `${id}: ${text}`);
|
||||
assert.deepEqual(english, []);
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -79,9 +79,9 @@ html[lang='es'] .cctv-controls {
|
||||
}
|
||||
|
||||
/* A translated label longer than its button wraps inside the button instead
|
||||
of running past the panel edge. English labels fit on one line, so the
|
||||
English layout is unchanged. */
|
||||
.cctv-controls .scene-btn {
|
||||
of running past the panel edge. English keeps its one-line buttons (unguarded,
|
||||
this rule broke AUTO HOP OFF onto two lines). */
|
||||
html:not([lang='en']) .cctv-controls .scene-btn {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -1836,9 +1836,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Sits above the language picker row in the bottom-right corner
|
||||
(locale-picker.css: 0.9rem inset + one ~2rem button + a gap). */
|
||||
#intel-hud[data-variant='cyber'] .hud-bottom-right {
|
||||
right: 28px;
|
||||
bottom: 28px;
|
||||
bottom: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2735,6 +2737,9 @@
|
||||
|
||||
:root[data-ui-theme='cyber'] body.cockpit-mode .cockpit-position-readout {
|
||||
right: 30px;
|
||||
/* Clears the language picker row it shares the corner with, as in the
|
||||
other themes (locale-picker.css). */
|
||||
bottom: 3.5rem;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] body.cockpit-mode .cockpit-help {
|
||||
|
||||
@@ -210,3 +210,10 @@ test('K-44: Recent Imagery readout keeps its English layout', () => {
|
||||
assert.equal(declaration(swap, 'width'), '40px');
|
||||
assert.equal(declaration(swap, 'min-width'), undefined);
|
||||
});
|
||||
|
||||
test('N-13: CCTV control buttons stay on one line in English', () => {
|
||||
assert.equal(
|
||||
unguardedRules(css('cctv.css'), '.cctv-controls .scene-btn').length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user