i18n: fix the LGTM review findings and tidy the branch texts

- i18n scanner: lastError sink, no false sinks on defaults and ternaries
- shared src/i18n/marker.js for modules that only mark strings
- wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js
- drop review IDs, change history and stale references from comments and docs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-16 21:00:28 +04:00
1 parent 0ed05568f7
commit 20f4cf3763
178 files changed
+918 -948

No files matched your search

+67
View File
@@ -211,3 +211,70 @@ test('unmarkedScript sees human-readable status, loadingLabel and statusMessage
assert.deepEqual(texts("return { status: 'auth-failed' };"), []);
assert.deepEqual(texts("return { status: msg('Source unavailable') };"), []);
});
// The Layers panel shows `stats.error || stats.lastError`, so a
// lastError is status text like error.
test('unmarkedScript sees status text assigned to lastError', () => {
const texts = (js) => unmarkedScript('f.js', js).map((f) => f.text);
assert.deepEqual(
texts(
"state.coverage.lastError = error?.message || 'Coverage tile failed';",
),
['Coverage tile failed'],
);
assert.deepEqual(texts("layerState._lastError = 'CelesTrak unreachable';"), [
'CelesTrak unreachable',
]);
assert.deepEqual(
texts(
"state.coverage.lastError = error?.message || msg('Coverage tile failed');",
),
[],
);
assert.deepEqual(texts('this._lastError = message;'), []);
});
test('unmarkedScript skips default values and ternary branches, not later assignments', () => {
const texts = (js) => unmarkedScript('f.js', js).map((f) => f.text);
// A default value in a parameter list or destructuring is a local's.
assert.deepEqual(texts("function f(error = 'Feed went away') {}"), []);
assert.deepEqual(texts("const f = (status = 'Not ready', x) => x;"), []);
assert.deepEqual(
texts("function f(\n status = 'Not ready',\n x,\n) {}"),
[],
);
assert.deepEqual(texts("const { status = 'Ready now' } = options;"), []);
assert.deepEqual(
texts("const {\n error = 'No data yet',\n} = options;"),
[],
);
assert.deepEqual(
texts("for (const [id, status = 'Waiting'] of rows) {}"),
[],
);
// A name right after `?` is a ternary branch, not a `status:` property.
assert.deepEqual(texts("const s = ok ? status : 'Source unavailable';"), []);
assert.deepEqual(texts("const s = ok\n ? title\n : 'Untitled view';"), []);
// …while the property itself still is.
assert.deepEqual(texts("return { title: x ? title : 'Untitled view' };"), [
'Untitled view',
]);
// A later assignment is checked even when the name is a declared local:
// a layer keeps its status in `let error` (wind, AIS watchdog) and builds
// `loadingLabel` in a `let` before returning it (FIRMS).
assert.deepEqual(
texts(
"function f() {\n let error = null;\n if (x) error = 'Feed closed';\n}",
),
['Feed closed'],
);
assert.deepEqual(
texts(
"let loadingLabel = '';\nif (x) {\n loadingLabel = 'KEY REQUIRED';\n}",
),
['KEY REQUIRED'],
);
assert.deepEqual(texts("let a = 1,\n error = 'Feed closed';"), [
'Feed closed',
]);
});
+1 -1
View File
@@ -1,4 +1,4 @@
// K-21: an empty or blank translation must not count as done.
// An empty or blank translation must not count as done.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
+1 -1
View File
@@ -179,7 +179,7 @@ const UTC_DAY_MONTH = Object.freeze({
/**
* The UTC day and month of a time, for "MM-DD HH:MM UTC" readouts: "10-13" in
* English (as before), "13.10" in Russian, "13/10" in Spanish, never read as a
* English, "13.10" in Russian, "13/10" in Spanish, never read as a
* range. Takes an ISO string (its date part is used as written) or a time.
* @param {string|number|Date} value
* @returns {string} '' when the value is not a time.
+2 -4
View File
@@ -1,7 +1,5 @@
// K-194: the KartaView provider's failure and state texts are English data
// (msg() in kartaview/{policy,source,viewer,index}.js) that the Street Level
// panel and the chip tooltip show through translateKnown, so every one needs
// an entry in each language's catalog.
// KartaView texts are English data (msg() in providers/kartaview/) shown
// through translateKnown, so each needs an entry in every catalog.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { setLocale, translateKnown } from './index.js';
+5 -7
View File
@@ -3,10 +3,8 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { setLocale, translateKnown } from './index.js';
// Status and error texts the layers write straight into their state
// (loadingLabel / _error / street.error) as English data; the layer panel and
// the Street Level panel show them through translateKnown, so each needs a
// catalog entry in every language (K-36).
// Layers keep these status texts as English data, and the panels show them
// through translateKnown, so each needs an entry in every catalog.
const SITES = [
['../layers/streetLevel/index.js', 'loading coverage...'],
['../layers/firms/ingestion.js', 'live feed unavailable'],
@@ -33,8 +31,8 @@ const SITES = [
['../layers/wind/source.js', 'Wind request timed out'],
['../layers/wind/source.js', 'Malformed wind manifest'],
['../layers/wind/source.js', 'Malformed wind grid'],
['../layers/wind/source.js', 'Temperature field unavailable'],
['../layers/wind/source.js', 'Mean sea level pressure field unavailable'],
['../layers/wind/fields.js', 'Temperature field unavailable'],
['../layers/wind/fields.js', 'Mean sea level pressure field unavailable'],
];
// A fallback that reaches the panel must be marked, or `npm run i18n` cannot
@@ -55,7 +53,7 @@ for (const locale of ['es', 'ru']) {
for (const [file, text] of SITES) {
test(`${locale}: "${text}" from ${file} is translated`, () => {
const source = readFileSync(new URL(file, import.meta.url), 'utf8');
assert.ok(source.includes(`'${text}'`), `literal still in ${file}`);
assert.ok(source.includes(`'${text}'`), `literal not found in ${file}`);
setLocale(locale);
try {
assert.notEqual(translateKnown(text), text);
+1 -5
View File
@@ -7,11 +7,7 @@ export const LOCALE_STORAGE_KEY = 'gev:locale:v1';
/** Source language and fallback. */
export const DEFAULT_LOCALE = 'en';
/**
* Shipped locales. The picker (src/ui/localePicker.js) lists the active one
* first and the rest by LOCALE_NAMES. Adding one: locales/<code>.json, an
* import in index.js, and an entry here (see docs/TRANSLATORS.md).
*/
/** Shipped locales; order does not matter. Adding one: docs/TRANSLATORS.md. */
export const LOCALES = Object.freeze(['en', 'es', 'ru']);
/** Each locale named in its own language, for the selector. */
+3 -1
View File
@@ -2883,5 +2883,7 @@
"Included in bundle": "Incluido en el paquete",
"Missing bundle file — reimport its bundle": "Falta el archivo en el paquete — vuelve a importar su paquete",
"Source configured; file checked when loaded": "Fuente configurada; el archivo se comprobará al cargarlo",
"Source unavailable": "Fuente no disponible"
"Source unavailable": "Fuente no disponible",
"Coverage tile failed": "No se pudo cargar la tesela de cobertura",
"Unknown": "Desconocido"
}
+3 -1
View File
@@ -2950,5 +2950,7 @@
"Included in bundle": "Входит в архив",
"Missing bundle file — reimport its bundle": "Файла нет в архиве — импортируйте этот архив снова",
"Source configured; file checked when loaded": "Источник настроен; файл будет проверен при загрузке",
"Source unavailable": "Источник недоступен"
"Source unavailable": "Источник недоступен",
"Coverage tile failed": "Не удалось загрузить тайл покрытия",
"Unknown": "Неизвестно"
}
+7
View File
@@ -0,0 +1,7 @@
// msg() for modules the server, tools or other portable code also import:
// marks English that stays in data for `npm run i18n`, without loading the
// catalogs (src/i18n/index.js). The UI shows it later through t().
// Browser-only modules import msg from src/i18n/index.js instead.
/** Mark English text that is stored in data and translated later by t(). */
export const msg = (text) => text;
+3 -6
View File
@@ -6,9 +6,7 @@ import {
missingTemplate,
} from '../../scripts/i18n.mjs';
// A plural entry whose every form is still the English text must be reported
// the same way a string entry is ("is still English"). Today the plural
// branch of checkCatalog never compares forms with the English.
// A plural entry with forms left in English is reported like a string entry.
const fixture = new Map([
['{count} cameras', { plural: true, one: '{count} camera' }],
['Save keys', { plural: false }],
@@ -64,9 +62,8 @@ for (const [locale, entry] of [
});
}
// `npm run i18n -- --missing <code>` prints a template; pasted without edits,
// none of its entries may pass as a translation (K-21, K-52), and a plural
// entry carries every form the language needs (K-97).
// The `--missing` template, pasted without edits, must fail entry by entry,
// and its plural entries carry every form the language needs.
const messages = extractMessages();
for (const locale of ['de', 'ja', 'es', 'ru']) {
test(`${locale}: an untouched --missing template fails the check entry by entry`, () => {
+2 -2
View File
@@ -1,5 +1,5 @@
// K-23: scripts/i18n.mjs must skip index entries that are gone from the
// working tree (plain `mv`/`rm` without `git mv`/`git rm`).
// scripts/i18n.mjs must skip index entries gone from the working tree
// (plain `mv`/`rm` without `git mv`/`git rm`).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';