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

+9 -9
View File
@@ -1137,14 +1137,14 @@ export function createAnnotationEngine({
{
type: 'highlight',
target: 'Palace of Fine Arts, San Francisco',
// i18n-ignore: dev demo standing in for voice-agent labels, which are not translated
// i18n-ignore: dev demo of voice-agent labels (not translated)
label: 'Palace of Fine Arts',
color: 'amber',
},
{
type: 'area',
target: 'Presidio of San Francisco',
// i18n-ignore: dev demo standing in for voice-agent labels, which are not translated
// i18n-ignore: dev demo of voice-agent labels (not translated)
label: 'The Presidio (former Army base)',
color: 'green',
footprint: true,
@@ -1152,7 +1152,7 @@ export function createAnnotationEngine({
{
type: 'pin',
target: 'Letterman Digital Arts Center, San Francisco',
// i18n-ignore: dev demo standing in for voice-agent labels, which are not translated
// i18n-ignore: dev demo of voice-agent labels (not translated)
label: 'ILM / Lucasfilm',
color: 'cyan',
},
@@ -1160,7 +1160,7 @@ export function createAnnotationEngine({
type: 'arrow',
target: 'Palace of Fine Arts, San Francisco',
toTarget: 'Marina District, San Francisco',
// i18n-ignore: dev demo standing in for voice-agent labels, which are not translated
// i18n-ignore: dev demo of voice-agent labels (not translated)
label: 'next to the Marina',
},
],
@@ -1190,7 +1190,7 @@ export function createAnnotationEngine({
{
type: 'highlight',
target: 'Palace of Fine Arts, San Francisco',
// i18n-ignore: dev demo standing in for voice-agent labels, which are not translated
// i18n-ignore: dev demo of voice-agent labels (not translated)
label: 'Palace of Fine Arts',
color: 'amber',
},
@@ -1205,7 +1205,7 @@ export function createAnnotationEngine({
type: 'arrow',
target: 'Palace of Fine Arts, San Francisco',
toTarget: 'Marina Green, San Francisco',
// i18n-ignore: dev demo standing in for voice-agent labels, which are not translated
// i18n-ignore: dev demo of voice-agent labels (not translated)
label: 'next to the Marina',
color: 'cyan',
},
@@ -1229,7 +1229,7 @@ export function createAnnotationEngine({
{
type: 'area',
target: 'Presidio of San Francisco',
// i18n-ignore: dev demo standing in for voice-agent labels, which are not translated
// i18n-ignore: dev demo of voice-agent labels (not translated)
label: 'The Presidio — a former Army base',
color: 'green',
footprint: true,
@@ -1244,7 +1244,7 @@ export function createAnnotationEngine({
{
type: 'pin',
target: 'Letterman Digital Arts Center, San Francisco',
// i18n-ignore: dev demo standing in for voice-agent labels, which are not translated
// i18n-ignore: dev demo of voice-agent labels (not translated)
label: 'ILM / Lucasfilm',
color: 'red',
},
@@ -1258,7 +1258,7 @@ export function createAnnotationEngine({
{
type: 'route',
color: 'amber',
// i18n-ignore: dev demo standing in for voice-agent labels, which are not translated
// i18n-ignore: dev demo of voice-agent labels (not translated)
label: 'Crissy Field shoreline',
points: [
{ target: 'Palace of Fine Arts, San Francisco' },
+2 -3
View File
@@ -241,8 +241,7 @@ const LEFT_DOUBLE_CLICK = Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK;
/* ── the tests ─────────────────────────────────────────────────────────── */
// K-27. First in the file: the tools other tests leave behind would answer the
// same registry.
// First in the file: tools left by later tests would answer the same registry.
test('a lasting mark or a half-drawn shape makes the language switch ask first', async () => {
const h = harness();
try {
@@ -288,7 +287,7 @@ test('Draw refuses to start when something else holds the pointer', async () =>
h.element('draw-toggle').emit('click');
assert.equal(h.tool.active, false, 'it must not half-start');
assert.equal(pointerOwner(), 'directions', 'and must not steal the claim');
// Named by its tool, not by the internal owner id (E3/E12).
// Named by its tool, not by the internal owner id.
assert.equal(
h.element('draw-hint').textContent,
'Directions is using the pointer — close it first.',
+5 -7
View File
@@ -541,7 +541,7 @@ export function panelRuntime(config) {
const notice = document.createElement('div');
notice.id = 'status';
notice.textContent =
// i18n-ignore: panel runtime runs as source text in the host page, with no i18n module or app locale
// i18n-ignore: host-panel script, no app catalogs
"This panel lost its 3D graphics, likely because other God's Eye " +
"View panels in this conversation hold them. Use Open in God's " +
'Eye View above, or show it in a new conversation.';
@@ -569,20 +569,18 @@ export function panelRuntime(config) {
queued = view;
if (started) return;
started = true;
// i18n-ignore: this script runs as source text in the host panel, without the app catalogs
// i18n-ignore: host-panel script, no app catalogs
say("Loading God's Eye View…");
startApp(url, view).catch((error) =>
// i18n-ignore: this script runs as source text in the host panel, without the app catalogs
// i18n-ignore: host-panel script, no app catalogs
say(`God's Eye View could not load here: ${error?.message || error}.`),
);
setTimeout(() => {
// i18n-ignore: this script runs as source text in the host panel
if (!ready)
say(
// i18n-ignore: see above
"God's Eye View did not load here" +
(blocked ? ` (${blocked})` : '') +
// i18n-ignore: see above
". Use Open in God's Eye View above.",
);
}, config.loadTimeoutMs);
@@ -622,7 +620,7 @@ export function panelRuntime(config) {
if (context?.displayMode) displayMode = context.displayMode;
if (context?.availableDisplayModes)
expand.hidden = !context.availableDisplayModes.includes('fullscreen');
// i18n-ignore: panel runtime runs as source text in the host page, with no i18n module or app locale
// i18n-ignore: host-panel script, no app catalogs
expand.textContent = displayMode === 'fullscreen' ? 'Collapse' : 'Expand';
};
expand.addEventListener('click', () => {
@@ -663,7 +661,7 @@ export function panelRuntime(config) {
},
(error) =>
say(
// i18n-ignore: this script runs as source text in the host panel, without the app catalogs
// i18n-ignore: host-panel script, no app catalogs
"This client did not accept the God's Eye View panel" +
(error?.message ? `: ${error.message}` : '.'),
),
+2 -2
View File
@@ -3,7 +3,7 @@ import * as credits from '../../data/dataCredits.js';
import * as sprites from '../../data/spriteOrder.js';
import * as activation from '../../cctvFocusRequest.js';
import * as overlays from '../../overlays/worldOverlay.js';
import * as locations from '../../locations.js';
import { CITY_POIS } from '../../cityPois.js';
import * as picking from '../../data/pickRegistry.js';
import * as focus from '../../data/focusDeemphasis.js';
import * as render from '../../renderGovernor.js';
@@ -18,7 +18,7 @@ export function createApplicationCctv({ surface, source }) {
sprites,
activation,
overlays,
locations,
locations: { CITY_POIS },
picking,
terrain,
ground,
-1
View File
@@ -2,7 +2,6 @@
* @module cityPois
* @description The preset cities and their landmarks — plain data with no
* Cesium, so modules that only need the names (voice cards) can import it.
* `locations.js` re-exports it.
*/
/**
+2 -1
View File
@@ -8,7 +8,8 @@
// say nothing about whether a destination is framed well.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { CITY_POIS, LOCATIONS } from './locations.js';
import { CITY_POIS } from './cityPois.js';
import { LOCATIONS } from './locations.js';
const DESTINATIONS = Object.entries(CITY_POIS);
const REQUIRED_POI_KEYS = ['name', 'lat', 'lon', 'alt', 'pitch', 'heading'];
+1 -4
View File
@@ -119,10 +119,7 @@ export function formatCockpitEmergency(emergency, format = ENGLISH_EMERGENCY) {
});
}
/**
* The English Context scope line. The cockpit passes its own formatter so the
* page shows it in the active language; this module stays i18n-free.
*/
/** The English Context scope line; see formatCockpitContextScope. */
const ENGLISH_CONTEXT_SCOPE = Object.freeze({
window: ({ label, radiusKm }) =>
`${label} · ${radiusKm ?? '—'} KM AIR/SEA WINDOW`,
+4 -6
View File
@@ -1,3 +1,5 @@
import { msg } from './i18n/marker.js';
const CONTEXT_DEPENDENCIES = Object.freeze({
flights: new Set([
'military-awareness',
@@ -9,10 +11,6 @@ const CONTEXT_DEPENDENCIES = Object.freeze({
'space-missions': new Set(['rocket-launches', 'satellites']),
});
const CONTEXT_COMPANIONS = new Set(['radio']);
// This module is shared with voice actions and scene policy, so the refusal
// reason stays English data; the Context shell translates it with t() at the
// toast (src/ui/contextLayerChanges.js).
const msg = (text) => text;
/** The Space Missions refusal, with the blocked layer's name as {layer}. */
export const SPACE_MISSIONS_LAYER_BLOCK_REASON = msg(
'Space Missions isolates replay data. Exit the mode to enable {layer}.',
@@ -350,8 +348,8 @@ export function contextLayerEnableBlockReason({
}
/**
* Split a Space Missions refusal back into its template and layer name so
* the presentation can translate the sentence and localize the name.
* The layer name from a Space Missions refusal, so the UI can rebuild the
* sentence in its language.
* @param {string} reason
* @returns {{layer: string}|null} The blocked layer's name, or null when the
* reason is not a Space Missions refusal.
+2 -4
View File
@@ -7,6 +7,8 @@
* integer category (states index 17) and ADS-B emitter strings ("A1".."B1").
*/
import { msg } from '../i18n/marker.js';
const HELI = new Set([
'EC20',
'EC25',
@@ -438,10 +440,6 @@ export function classifyAircraft({ typeCode, category } = {}) {
return 'airliner';
}
// Portable flight records import this module, so it has no i18n: msg() only
// marks the labels, and the card translates them where it shows them.
const msg = (text) => text;
/** Readable name of each class, for cards and readouts (English data). */
export const AIRCRAFT_CLASS_LABELS = Object.freeze({
light: msg('Light aircraft'),
+2 -3
View File
@@ -13,12 +13,11 @@
* broadcast, which does not confirm an incident or its resolution.
*/
import { msg } from '../i18n/marker.js';
/** @constant {string} Tint for aircraft broadcasting an emergency (not a priority). */
export const EMERGENCY_TINT_CSS = '#FF4444';
// Marks catalog text without importing i18n: tools and the server load this module.
const msg = (text) => text;
const SQUAWK_KINDS = { 7500: 'unlawful', 7600: 'nordo', 7700: 'general' };
const KINDS = {
+4 -4
View File
@@ -249,7 +249,7 @@ export function createAisWatchdog(options = {}) {
if (!env.hasKey) {
const actions = terminateOwned('key-absent');
status = 'missing-key';
// i18n-ignore: server detail; the chip shows a translated reason (vessels/queries.js)
// i18n-ignore: server detail; layers/vessels/queries.js shows a translated reason
error = 'AISSTREAM_API_KEY is not set';
reconnectAttempt = 0;
keyFingerprint = null;
@@ -258,7 +258,7 @@ export function createAisWatchdog(options = {}) {
if (env.hasTransport === false) {
const actions = terminateOwned('no-transport');
status = 'unsupported';
// i18n-ignore: server detail; the chip shows a translated reason (vessels/queries.js)
// i18n-ignore: server detail; layers/vessels/queries.js shows a translated reason
error = 'Node WebSocket transport is unavailable';
return actions;
}
@@ -331,7 +331,7 @@ export function createAisWatchdog(options = {}) {
if (!silenceWatchArmed) return [];
const silentFor = monoNow - silenceSinceMono;
if (silentFor >= recycleAfterMs) {
// i18n-ignore: server detail; the chip shows a translated reason (vessels/queries.js)
// i18n-ignore: server detail; layers/vessels/queries.js shows a translated reason
error = `AISStream delivered no data for ${Math.round(silentFor / 1000)}s`;
const actions = terminateOwned('silent');
scheduleRetry('transport');
@@ -404,7 +404,7 @@ export function createAisWatchdog(options = {}) {
function onClose(eventGeneration) {
if (!ownsGeneration(eventGeneration)) return [];
release();
// i18n-ignore: server detail; the chip shows a translated reason (vessels/queries.js)
// i18n-ignore: server detail; layers/vessels/queries.js shows a translated reason
if (!error) error = 'AISStream websocket closed';
scheduleRetry('transport');
return [];
+2 -4
View File
@@ -40,10 +40,8 @@ const MEDIA_PRECONNECT_ORIGINS = Object.freeze({
facebook: Object.freeze(['https://www.facebook.com']),
});
// Authored English in public/events/bhote-koshi-2026/event.json. The pack is
// data and stays English; these known strings translate at display. Anything
// else (a newer pack, a credit, a source's own title) shows as authored. A
// string that is not known whole translates by its " · " parts.
// Known English from public/events/bhote-koshi-2026/event.json, translated at
// display (whole or by " · " parts). Anything else shows as authored.
const EVENT_PACK_TEXT = new Set([
// Field reports: our labels (credits after " · " stay) and their statuses.
msg('Rasuwagadhi witness video'),
+3 -5
View File
@@ -826,11 +826,9 @@ export function updateEvidenceFrame(
const EVIDENCE_SOURCE_MIN_FONT_SCALE = 0.8;
/**
* Set the font for the «{publisher} / OPEN» line and return the line so it
* fits `maxWidth`: a translation («ИСТОЧНИК YOUTUBE / ОТКРЫТЬ») is longer than
* the English and the portrait card cut its action word. A line that fits is
* drawn exactly as before; one that does not first shrinks its font (to 80%),
* then shortens the publisher — the «/ OPEN» action always stays whole.
* Set the font for the «{publisher} / OPEN» line and return a line that fits
* `maxWidth`. Translations run longer: first shrink the font (to 80%), then
* shorten the publisher; «/ OPEN» always stays whole.
* @param {CanvasRenderingContext2D} ctx
* @param {string} publisher Display publisher, already upper-cased.
* @param {string} font CSS font with a px size.
+3 -4
View File
@@ -1,7 +1,6 @@
// Proposal 5 / E4: the evidence frame's «{publisher} / OPEN» line is drawn on
// a canvas, so CSS cannot widen it; on the portrait card the Russian
// «ИСТОЧНИК YOUTUBE / ОТКРЫТЬ» lost its last letter. The line is measured and
// shrunk to fit, keeping the action word whole; English draws as before.
// The «{publisher} / OPEN» line is drawn on a canvas, so CSS cannot wrap it:
// a long translation shrinks to fit the portrait card, action word whole.
// English is drawn at full size.
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
@@ -2,8 +2,8 @@ import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
// K-30: the publisher on the evidence frame ("YouTube source") is pack text
// and is shown in the UI language; English stays "YOUTUBE SOURCE / OPEN".
// The publisher on the evidence frame ("YouTube source") is pack text shown
// in the UI language; English stays "YOUTUBE SOURCE / OPEN".
import { setLocale } from '../i18n/index.js';
import { createEvidenceFrame } from './bhoteKoshiEvent.js';
+2 -3
View File
@@ -52,9 +52,8 @@ const TRIGGER_ORBIT_DURATION_SEC = 2.8;
const TRIGGER_SOURCE_URL =
'https://earthquake.usgs.gov/earthquakes/eventpage/us7000tbwb/executive';
// Overview pin titles that are more than a place name translate at display,
// through the same entries the event panel uses. The data stays English and
// place names stay Latin; everything else shows as authored.
// Descriptive pin titles translate at display (same entries as the event
// panel); place names stay Latin, anything else shows as authored.
const PLACE_TITLE_TEXT = new Map(
[
msg('Collapse viewpoint'),
+1 -3
View File
@@ -2,9 +2,7 @@
import * as Cesium from 'cesium';
import { createImportImageBudget } from './geoImportImages.js';
import { createImportedPins } from './geoImportPins.js';
// Marks catalog text without importing i18n; the UI translates the message.
const msg = (text) => text;
import { msg } from '../i18n/marker.js';
/** Largest file accepted, in bytes. */
export const MAX_IMPORT_BYTES = 50 * 1024 * 1024;
+2 -4
View File
@@ -1,8 +1,6 @@
// Portable node/browser module (the HUD summary in loadingFeedback.js reads
// it too), so it marks its copy with a local msg() instead of importing i18n;
// installations/controls.js translates describeInstallationFeedback().
const msg = (text) => text;
import { msg } from '../i18n/marker.js';
// layers/installations/controls.js translates describeInstallationFeedback().
const REASONS = Object.freeze({
rate_limited: msg('Overpass rate-limited'),
timeout: msg('Overpass timed out'),
+5 -9
View File
@@ -1,14 +1,10 @@
import { msg } from '../i18n/marker.js';
/**
* The English names the data gives a military installation that has no
* mapped name. They stay English in data — the server provider, the AI tools
* and the voice context read the records — and the display translates them
* (installationTitle in src/layers/installations/classLabel.js and
* formatAwarenessLabel in src/data/militaryAwarenessEngine.js).
*
* The server, tools and source modules load this file, so it marks the names
* with a local msg() instead of importing i18n.
* English names for installations without a mapped name. Data keeps them
* English (server, AI tools, voice context); classLabel.js and
* militaryAwarenessEngine.js translate them.
*/
const msg = (text) => text;
/** Name of an unnamed feature, by installation class. */
export const UNNAMED_INSTALLATION_LABEL_BY_CLASS = Object.freeze({
+2 -2
View File
@@ -1,5 +1,5 @@
// Marks restore labels for i18n extraction; shareRestoration translates them.
// Kept local so this module stays free of the i18n import.
// Restore labels are marked with msg(); ui/shareRestoration.js translates them.
// Local, not i18n/marker.js: the token-ledger tests run this file in isolation.
const msg = (text) => text;
import { normalizeIdSwitches, patchRequestedIdSwitches } from './idSwitches.js';
import reservationRows from './layerStateTokenReservations.json' with { type: 'json' };
-7
View File
@@ -32,13 +32,6 @@ const CLASS_BY_MILITARY_TAG = {
const LABEL_BY_CLASS = UNNAMED_INSTALLATION_LABEL_BY_CLASS;
const UNNAMED_FALLBACK = UNNAMED_INSTALLATION_FALLBACK;
/** Every name humanizeInstallationClass gives a record of a class
* CLASS_BY_MILITARY_TAG can produce, for the display to translate. */
export const UNNAMED_INSTALLATION_NAMES = Object.freeze([
...Object.values(LABEL_BY_CLASS),
UNNAMED_FALLBACK,
]);
const MAX_FOOTPRINT_POINTS = 400;
/**
+5 -11
View File
@@ -1,15 +1,9 @@
/**
* The tool name to show for a pointer owner.
*
* Pointer-owner ids (src/data/inputOwnership.js) are stable internal
* identifiers — 'draw', 'recent-imagery-box' — and never belong on screen.
* This maps each to the English name of the tool, which the caller translates
* with translateKnown() where it is shown. Pure and stateless, so any package
* can carry it without a second copy of the pointer claim.
*/
import { msg } from '../i18n/marker.js';
// English marker for the i18n extractor; this module imports no i18n.
const msg = (text) => text;
/**
* Tool name for a pointer owner. Owner ids (inputOwnership.js) never go on
* screen; callers translate the English name with translateKnown().
*/
const POINTER_OWNER_NAMES = Object.freeze({
draw: msg('Draw'),
+6 -18
View File
@@ -20,53 +20,43 @@
import { t, msg } from '../i18n/index.js';
/**
* Class registry. `label` is the card/legend token, `color` the point color,
* `blurb` the plain-language gloss for the legend tooltip.
* @type {Readonly<Record<string, { label: string, color: string, blurb: string }>>}
* Class registry: the point color of each class. The card and legend text is
* in CLASS_DISPLAY_KEYS.
* @type {Readonly<Record<string, { color: string }>>}
*/
export const SATELLITE_CLASSES = Object.freeze({
station: Object.freeze({
label: 'STATION', // i18n-ignore: English token; shown through CLASS_DISPLAY_KEYS
// Warm white — "crewed", and the brightest class in the sky. Deliberately
// NOT amber: 40-48deg amber is the app's known-military convention
// (flights.js MIL_TINT / militaryFlights.js MIL_ICON_COLOR) and must not
// leak into another domain. Slightly warm so it also separates from the
// pure white the civilian air fleet draws in.
color: '#fff6e5',
blurb: 'Crewed stations and their visiting vehicles', // i18n-ignore: shown through CLASS_DISPLAY_KEYS
}),
nav: Object.freeze({
label: 'NAV', // i18n-ignore: English token; shown through CLASS_DISPLAY_KEYS
// Cyan — the strongest accent goes to the class users actually name
// ("how many GPS satellites are up?"). Takes over the slot the catch-all
// VISUAL group used to occupy, so this adds no new hue to the app.
color: '#4fd8ff',
blurb: 'GNSS navigation — GPS, GLONASS, Galileo', // i18n-ignore: shown through CLASS_DISPLAY_KEYS
}),
geo: Object.freeze({
label: 'GEO', // i18n-ignore: English token; shown through CLASS_DISPLAY_KEYS
// Violet, unchanged. Already the app's "space" semantic (the detection
// overlay paints the SAT tier #bda4ff), and the geostationary belt draws
// as one clean equatorial ring at globe scale.
color: '#c89bff',
blurb: 'Geostationary belt — comms and weather, fixed over the equator', // i18n-ignore: shown through CLASS_DISPLAY_KEYS
}),
visual: Object.freeze({
label: 'VISUAL', // i18n-ignore: English token; shown through CLASS_DISPLAY_KEYS
// Muted blue-gray. This is the catch-all bucket, so it gives up the bright
// cyan it used to hold and recedes behind the three classes that mean
// something. Still well clear of the dense shell by luminance.
color: '#9fb3c4',
blurb: 'Brightest naked-eye objects — CelesTrak visual group', // i18n-ignore: shown through CLASS_DISPLAY_KEYS
}),
comms: Object.freeze({
label: 'COMMS', // i18n-ignore: English token; shown through CLASS_DISPLAY_KEYS
// Dim slate — thousands of these appear in DENSE mode. They must read as
// texture behind the core catalog, never compete with it. Rec.601 luma
// ~0.40 against VISUAL's ~0.69 keeps the two separable even when the
// NVG/FLIR shaders collapse the scene to a single channel.
color: '#54697f',
blurb: 'Broadband constellation shell — shown only in DENSE mode', // i18n-ignore: shown through CLASS_DISPLAY_KEYS
}),
});
@@ -79,9 +69,7 @@ export const SATELLITE_CLASS_ORDER = Object.freeze([
'comms',
]);
// Display keys for the class table above: `label`/`blurb` stay the English
// source-of-truth tokens (tests read them), while the card and legend accessors
// translate through these keys at the presentation boundary.
// What the card and legend show for each class.
const CLASS_DISPLAY_KEYS = Object.freeze({
station: Object.freeze({
label: msg('STATION'),
@@ -164,7 +152,8 @@ export function satelliteClassColor(group) {
* The ISS is always a STATION regardless of which group ingested it — see
* satelliteClassOf, which owns that rule for every consumer.
* @param {string|undefined|null} group CelesTrak group tag.
* @param {{ isIss?: boolean }} [options] Whether this is the ISS.
* @param {{ isIss?: boolean, localize?: boolean }} [options] isIss: the ISS;
* localize: false keeps the English label (data for the AI tools).
* @returns {string} Display label.
*/
export function satelliteClassLabel(
@@ -172,7 +161,6 @@ export function satelliteClassLabel(
{ isIss = false, localize = true } = {},
) {
const { klass, subtype } = satelliteClassOf(group, { isIss });
// 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;
+2 -4
View File
@@ -48,9 +48,8 @@ function run(locale) {
return details;
}
test('K-32 es: tracked ISS card', () => {
test('es: tracked ISS card is localized', () => {
const details = run('es');
console.log('es details:', JSON.stringify(details));
assert.equal(details[0], 'ESTACIÓN · ISS');
assert.doesNotMatch(
details[2],
@@ -59,9 +58,8 @@ test('K-32 es: tracked ISS card', () => {
);
});
test('K-32 ru: tracked ISS card', () => {
test('ru: tracked ISS card is localized', () => {
const details = run('ru');
console.log('ru details:', JSON.stringify(details));
assert.equal(details[0], 'СТАНЦИЯ · ISS');
assert.match(details[1], /км/, 'ru glossary: km → км');
assert.doesNotMatch(
+2 -3
View File
@@ -68,9 +68,8 @@ const NUMERIC_TYPE_FAMILIES = {
9: 'OTHER',
};
// Display keys for the normalized English type tokens above. The tokens stay
// machine values — TYPE_STYLES matches on them and tests pin them — so card
// text translates only through this map at the presentation boundary.
// Card text for the type tokens above. The tokens stay English: TYPE_STYLES
// and tests match on them.
const TYPE_DISPLAY_KEYS = Object.freeze({
FISHING: msg('FISHING'),
TOWING: msg('TOWING'),
+1 -1
View File
@@ -1,4 +1,4 @@
// K-12: a translated vessel type is never cut mid-word on the card.
// A translated vessel type is never cut mid-word on the card.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
+2 -3
View File
@@ -1,8 +1,7 @@
import { msg } from '../../i18n/marker.js';
import { BUNDLE_SOURCE } from './bundle.js';
// Pack statuses stay English in the report; the import dialog shows them
// through t() (scenes/sharing.js).
const msg = (text) => text;
// Statuses stay English; scenes/sharing.js translates them for the dialog.
/** Describe an import without fetching assets, applying state or exposing source configuration. */
export function describeSceneShare(
+2 -7
View File
@@ -36,8 +36,6 @@ export const FIRST_RUN_SESSION_KEY = 'gev:first-run-mission-session:v1';
*/
export const ENVIRONMENTAL_LABEL_CHOICE = 'ENVIRONMENTAL';
// Catalog keys per label choice. Resolved through t() at CALL time (init),
// never at module load — the locale is not resolved until main.js's body runs.
const ENVIRONMENTAL_LABEL_KEYS = Object.freeze({
ENVIRONMENTAL: msg('ENVIRONMENTAL'),
EARTH_WATCH: msg('EARTH WATCH'),
@@ -361,11 +359,8 @@ export function initFirstRunExperience({
}
// The tile name is owner-switchable from one constant, so paint it from the
// module rather than trusting the markup to have been edited to match. The
// persuasive description line and the tile's feed subcopy are the two
// phase-2 exceptions whose static sites are pinned verbatim by
// firstRunExperience.test.mjs — the runtime write is their only legal
// localization point (docs/TRANSLATORS.md, runtime-only static sites).
// module rather than trusting the markup to match. The description and the
// tile subcopy are data-i18n-runtime in welcome.html and translated here.
const environmentalTitle = root.querySelector(
'[data-first-run-environmental-title]',
);
+11 -12
View File
@@ -16,7 +16,7 @@ import { applicationServices } from './services/application.js';
import * as Cesium from 'cesium';
import { forward as toMGRS } from 'mgrs';
import { CITY_POIS } from './locations.js';
import { CITY_POIS } from './cityPois.js';
import { composeLocalityTag } from './hudLocality.js';
import { hudQuantity, LOCALIZED_LOCALITY } from './hudLocalityFormat.js';
import {
@@ -64,9 +64,8 @@ const HUD_COLORS = {
};
/**
* Mode names shown for each style. Same English as STYLE_STATUS_LABELS in
* ui/visualPresets.js, so the top-left mode and the dock's active style
* translate alike.
* Mode names per style: the same English as STYLE_STATUS_LABELS
* (ui/visualPresets.js), so both translate alike.
*/
const MODE_LABELS = Object.freeze({
normal: msg('NORMAL'),
@@ -262,7 +261,7 @@ export class IntelHUD {
this._el = document.getElementById('intel-hud');
if (!this._el) return;
// i18n-ignore: sensor readout codes (MGRS, GSD, NIIRS, ONA, BAND…) and a classification banner; translated parts use t()
// i18n-ignore: sensor codes and a classification banner; the rest uses t()
this._el.innerHTML = `
<div class="hud-sonar" aria-hidden="true"></div>
@@ -433,11 +432,11 @@ export class IntelHUD {
const formatted = this._formatMGRS(mgrsStr);
mgrsLabel = formatted;
const el = document.getElementById('hud-mgrs');
// i18n-ignore: sensor readout codes, same in every language
// i18n-ignore: sensor codes
if (el) el.textContent = `MGRS: ${formatted}`;
} catch {
const el = document.getElementById('hud-mgrs');
// i18n-ignore: sensor readout codes, same in every language
// i18n-ignore: sensor codes
if (el) el.textContent = 'MGRS: ---';
}
@@ -446,7 +445,7 @@ export class IntelHUD {
if (llEl) llEl.textContent = `${latDMS} ${lonDMS}`;
const bottomEl = document.getElementById('hud-bottom-line');
if (bottomEl) {
// i18n-ignore: sensor readout codes, same in every language
// i18n-ignore: sensor codes
bottomEl.textContent = `MGRS: ${mgrsLabel} LAT: ${latDMS} LON: ${lonDMS}`;
}
@@ -462,7 +461,7 @@ export class IntelHUD {
);
const gsdEl = document.getElementById('hud-gsd');
if (gsdEl)
// i18n-ignore: sensor readout codes, same in every language
// i18n-ignore: sensor codes
gsdEl.textContent = `GSD: ${gsd.toFixed(2)}m NIIRS: ${niirs.toFixed(1)}`;
// Altitude — reported as height above MEAN SEA LEVEL. `altM` is the raw
@@ -475,7 +474,7 @@ export class IntelHUD {
const altMslM = ellipsoidalToMslDisplayM(altM, geoidN);
const sunEl = this._estimateSunElevation(latDeg, lonDeg);
if (altEl)
// i18n-ignore: sensor readout codes, same in every language
// i18n-ignore: sensor codes
altEl.textContent = `ALT: ${Math.round(altMslM)}m SUN: ${sunEl.toFixed(1)}° EL`;
// Collection timestamp
@@ -485,7 +484,7 @@ export class IntelHUD {
const h = String(now.getUTCHours()).padStart(2, '0');
const m = String(now.getUTCMinutes()).padStart(2, '0');
const s = String(now.getUTCSeconds()).padStart(2, '0');
// i18n-ignore: sensor readout codes, same in every language
// i18n-ignore: sensor codes
collEl.textContent = `COLL: ${h}:${m}:${s}Z`;
}
@@ -494,7 +493,7 @@ export class IntelHUD {
const pitchDeg = Cesium.Math.toDegrees(camera.pitch);
const ona = Math.max(0, 90 + pitchDeg);
const onaEl = document.getElementById('hud-ona');
// i18n-ignore: sensor readout codes, same in every language
// i18n-ignore: sensor codes
if (onaEl) onaEl.textContent = `ONA: ${ona.toFixed(1)}°`;
// `altM` stays the raw ellipsoidal camera height the sensor model reads
+3 -7
View File
@@ -23,10 +23,7 @@
*/
export const NEAR_POI_MAX_KM = 150;
/**
* The English locality line. The HUD passes its own formatter so the page
* shows it in the active language; this default stays i18n-free.
*/
/** The English locality line; the HUD passes hudLocalityFormat.js instead. */
const ENGLISH_LOCALITY = Object.freeze({
near: ({ poi, city, distKm }) =>
`NEAR ${poi.toUpperCase()} (${city.toUpperCase()}) ${Math.round(distKm)}KM`,
@@ -54,9 +51,8 @@ function coordinateTag(value, positive, negative) {
* @param {number} latDeg Camera latitude in decimal degrees.
* @param {number} lonDeg Camera longitude in decimal degrees.
* @param {{near: Function, sector: Function}} [format] Builds the line from
* `{ poi, city, distKm }` or `{ latDeg, lonDeg }`. The names arrive as
* catalogued (`Tokyo`), so a formatter can translate them first and
* upper-case the result in the page's language.
* `{ poi, city, distKm }` or `{ latDeg, lonDeg }`; names arrive as
* catalogued (`Tokyo`), not upper-cased.
* @returns {string} `NEAR <POI> (<CITY>) <N>KM` within the bound (inclusive),
* otherwise `SECTOR <lat> <lon>`.
*/
-2
View File
@@ -124,8 +124,6 @@ test('a missing or malformed nearest POI never crashes the summary', () => {
assert.match(composeLocalityTag({ ...LINCOLN }, 10, 10), /^SECTOR /);
});
// The names reach a formatter as catalogued, so the HUD can translate the
// city before upper-casing it: the place-name table knows `Tokyo`, not `TOKYO`.
test('a formatter builds the line from the same parts', () => {
const format = {
near: ({ poi, city, distKm }) => `near:${poi}|${city}|${distKm}`,
+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';
+6 -11
View File
@@ -23,9 +23,7 @@ import {
import { t, msg, tn, registerUnsavedWork } from './i18n/index.js';
/**
* Chip label — pure, exported for tests. The exact English values are pinned
* by keySetup.test.mjs; t() resolves them from the catalog, so the default
* locale renders byte-identical text and 'es' localizes at the same site.
* Chip label — pure, exported for tests (keySetup.test.mjs pins the English).
* @param {{total?: number, setCount?: number}|null} status
* @returns {string}
*/
@@ -55,10 +53,9 @@ export function collectKeyUpdates(fields) {
}
/**
* Why /api/setup/keys refused a save, by the `code` the server sends with its
* English `error` (src/keySetupCore.mjs, server/standalone/key-setup.js). The
* English here is the server's own sentence, so the default locale reads the
* same as before.
* Why /api/setup/keys refused a save, by the `code` the server sends beside
* its English `error` (src/keySetupCore.mjs, server/standalone/key-setup.js).
* The English matches the server's text.
*/
const SAVE_REFUSALS = Object.freeze({
proxied: msg('Provider Settings does not answer proxied requests'),
@@ -141,10 +138,8 @@ function sleepWithSignal(ms, signal) {
}
/**
* An OAuth check error. A server-sent reason is kept as-is and later wrapped
* in «OAuth check failed: …»; when there is none, the error carries our own
* already-translated sentence and is flagged so the toast shows it unwrapped
* (the wrapper would only repeat it).
* An OAuth check error. A server reason is later wrapped in «OAuth check
* failed: …»; our own translated fallback is flagged to show unwrapped.
* @param {unknown} serverError
* @param {string} fallback Already-translated fallback sentence.
* @returns {Error}
+1
View File
@@ -174,6 +174,7 @@ test('aborting pending setup removes its surface and ignores a late response', a
resolveResponse({ ok: true, json: async () => ({ keys: [] }) });
assert.equal(await pending, null);
});
test('a pasted, unsaved key makes the language switch ask first until teardown', async () => {
const node = () => ({
dataset: {},
+8 -11
View File
@@ -1,8 +1,9 @@
/**
* Key setup ("POWER UP") — the pure core.
*
* One registry and pure functions; its only import is the proxy-header check
* shared with the other local gates (localRequestGate.mjs). The dev server's
* One registry and pure functions; it imports only the proxy-header check
* shared with the other local gates (localRequestGate.mjs) and the msg()
* marker (src/i18n/marker.js). The dev server's
* /api/setup endpoints (vite.config.js) and the in-app panel (keySetup.js)
* are both thin shells over this module, so what a key is called, what it
* unlocks, and how a .env line is written each live in exactly one place.
@@ -12,6 +13,7 @@
* behavior below unit-testable.
*/
import { msg } from './i18n/marker.js';
import { hasProxySignals } from './localRequestGate.mjs';
/** Longest accepted key/token value. Real provider keys are all far shorter. */
@@ -32,10 +34,6 @@ export const KEY_SETUP_APPEND_HEADER =
* bundle by design (restrict them at the provider, per SECURITY.md).
* `hidden` keeps advanced configuration out of the panel and missing-key count.
*/
// Marks `unlocks` copy for i18n extraction; Provider Settings translates it
// with t(). Kept local so the server can import this module without i18n.
const msg = (text) => text;
export const KEY_SETUP_KEYS = Object.freeze([
Object.freeze({
id: 'google-maps',
@@ -397,13 +395,11 @@ export function knownKeySetupEnvVars() {
return names;
}
/** Tooltip guidance for a control gated by one registry entry. */
/**
* Env-var requirements for a registry entry, as machine values for
* presentation layers that localize the guidance sentence themselves
* (this module also runs in node-side key-setup builds, which stay English).
* Env-var names a registry entry needs, joined with ' + ', for callers that
* build the guidance sentence in the UI language (src/ui/keyGuidance.js).
* @param {string} id Registry entry id.
* @returns {string|null} Joined env-var names, or null when nothing is required.
* @returns {string|null} null for an unknown or hidden entry.
*/
export function keySetupRequirementEnvVars(id) {
const entry = KEY_SETUP_KEYS.find((candidate) => candidate.id === id);
@@ -411,6 +407,7 @@ export function keySetupRequirementEnvVars(id) {
return entry.envVars.join(' + ');
}
/** English tooltip guidance for a control gated by one registry entry. */
export function keySetupRequirement(id) {
const entry = KEY_SETUP_KEYS.find((candidate) => candidate.id === id);
if (!entry || entry.hidden) return '';
+3 -5
View File
@@ -1,7 +1,5 @@
// K-25: a refused key save reads in the panel's language. The server sends a
// `code` (and `params`) beside its English `error`; the panel builds the
// sentence through t(), so English stays the server's own text and es/ru get
// the catalog's.
// A refused key save reads in the panel's language: the server sends `code`
// and `params` beside its English `error`, the panel builds the sentence.
import test from 'node:test';
import assert from 'node:assert/strict';
import {
@@ -94,7 +92,7 @@ test('in English the panel shows exactly the server sentence', () => {
assert.equal(saveRefusalText(refusal, 400), refusal.error);
});
test('an unknown code or a bare error falls back as before', () => {
test('an unknown code or a bare error falls back to the server text or the status', () => {
setLocale('en');
assert.equal(
saveRefusalText({ error: 'Something new', code: 'new' }, 400),
+2 -4
View File
@@ -1,7 +1,5 @@
// C9: a failed ChatGPT OAuth check whose only reason is our own fallback
// sentence shows that sentence alone — «OAuth check failed: Could not check
// ChatGPT sign-in. Try again.» repeated itself. A server-sent reason keeps the
// wrapper.
// A failed ChatGPT OAuth check whose only reason is our own fallback sentence
// shows it alone; a server-sent reason keeps the «OAuth check failed» wrapper.
import test from 'node:test';
import assert from 'node:assert/strict';
import { oauthCheckFailureText, waitForChatGptOAuth } from './keySetup.js';
+4 -4
View File
@@ -1,4 +1,4 @@
// K-26: every status line of the ChatGPT OAuth flow in Provider Settings goes
// Every status line of the ChatGPT OAuth flow in Provider Settings goes
// through t(), and the i18n check treats say() as a place UI text lands.
import { test } from 'node:test';
import assert from 'node:assert/strict';
@@ -28,7 +28,7 @@ const OAUTH_STATUS_TEXTS = [
'ChatGPT sign-in complete. OAuth will be used for the next cloud voice session.',
];
test('K-26: OAuth status-line texts are extractable messages with es/ru entries', () => {
test('OAuth status-line texts are extractable messages with es/ru entries', () => {
const messages = extractMessages();
const missing = OAUTH_STATUS_TEXTS.filter((text) => !messages.has(text));
assert.deepEqual(missing, [], 'texts not wrapped in t() in src/keySetup.js');
@@ -39,7 +39,7 @@ test('K-26: OAuth status-line texts are extractable messages with es/ru entries'
}
});
test('K-26: the i18n check sees an unmarked literal passed to say()', () => {
test('the i18n check sees an unmarked literal passed to say()', () => {
const sample =
"const say = (x) => { el.textContent = x; };\nsay('Checking local ChatGPT OAuth sign-in…');\n";
const found = unmarkedScript('sample.js', sample);
@@ -49,7 +49,7 @@ test('K-26: the i18n check sees an unmarked literal passed to say()', () => {
);
});
test('K-26: src/keySetup.js has no unmarked say() literals once say is a sink', () => {
test('src/keySetup.js has no unmarked say() literals', () => {
const source = readFileSync(
new URL('./keySetup.js', import.meta.url),
'utf8',
@@ -103,6 +103,5 @@ for (const locale of ['en', 'es', 'ru']) {
card,
inLocale(locale, () => translateFeedError('ECCC HTTP 503')),
);
assert.equal(card, locale === 'en' ? 'ECCC HTTP 503' : 'ECCC: HTTP 503');
});
}
+2 -3
View File
@@ -217,9 +217,8 @@ export function createAirQualityLayer({ source, overlayHost } = {}) {
0,
);
// Readings and errors stay English data (analyst records, stats); the
// card shows them in the UI language, errors as the Layers panel row
// does. A provider's own error text has no catalog entry and shows as
// it came.
// card translates them as the Layers panel row does. A provider's own
// error text has no catalog entry and shows as it came.
return {
readout: true,
summary: {
+2 -2
View File
@@ -15,8 +15,8 @@
* Only ECCC's AQHI is implemented here; `AIR_QUALITY_PROVIDERS` is the seam a
* later provider registers against.
*/
// A portable export (package.json): no i18n here. The labels stay English data
// and the Layers panel shows them translated.
// A portable export (package.json): no i18n here. The labels stay English data;
// the Weather card (index.js) shows them translated.
const msg = (text) => text;
/** Band identifiers shared across providers, in ascending severity. */
+1 -2
View File
@@ -1,8 +1,7 @@
import { AWARENESS_RADIUS_M } from '../../data/militaryAwarenessEngine.js';
// The reason stays English for the voice analyst; the Contacts panel shows it
// in the UI language from `reasonTemplate`. Kept local so this module stays
// free of the i18n import.
// in the UI language from `reasonTemplate`.
const msg = (text) => text;
const fillTemplate = (template, values = {}) =>
template.replace(/\{(\w+)\}/g, (_, name) => String(values[name]));
+3 -4
View File
@@ -11,10 +11,9 @@ import {
AWARENESS_PAGE_SIZE,
} from './policy.js';
// installations/controls.js getStats composes coverageLabel inside the layer
// model (machine-composed English, pinned by militaryInstallations.test.mjs).
// This is the presentation boundary: map the closed vocabulary to the
// catalog, passing anything unknown through verbatim (docs/TRANSLATORS.md).
// coverageLabel is machine-composed English from installations/controls.js
// (pinned by src/data/militaryInstallations.test.mjs); translate the known
// forms here, pass anything else through.
const WITHIN_KM_RE = /^WITHIN (\d+) KM$/;
function localizeCoverageLabel(label) {
-2
View File
@@ -45,8 +45,6 @@ export function isHeadingEstimated(camera) {
*/
export function headingHudToken(camera) {
const hdg = Math.round(Number(camera?.headingDeg) || 0);
// The token copy lives in the layers catalog (en is byte-identical to the
// literals these replace); presentation stays here, translation at read.
return isHeadingEstimated(camera)
? t('HDG {value}° (ESTIMATED)', { value: hdg })
: t('HDG {value}°', { value: hdg });
+14 -14
View File
@@ -66,7 +66,7 @@ const RECORDS = [
type: 'bullet',
url: 'https://www.cctv-database.com/camera/hikvision-ds-2cd2087g3-li2uy/',
fov_deg: '108.8 horizontal (2.8mm) / 93.3 horizontal (4mm)',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 8, w: 3840, h: 2160, label: '8MP (4K)' },
night_vision: { type: 'color', range_m: 40 },
environment: ['outdoor'],
@@ -79,7 +79,7 @@ const RECORDS = [
type: 'dome',
url: 'https://www.cctv-database.com/camera/axis-f4105-lre/',
fov_deg: '110 horizontal, 60 vertical',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD' },
night_vision: { type: 'ir', range_m: 10 },
environment: ['outdoor'],
@@ -97,7 +97,7 @@ const RECORDS = [
mp: 8,
w: 1920,
h: 1080,
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
label: '4x 2MP (8MP total; 4x 1080p)',
},
lens_count: 4,
@@ -120,7 +120,7 @@ const RECORDS = [
mp: 8,
w: 1920,
h: 1080,
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
label: '4x 2MP (8MP total; 4x 1080p)',
},
lens_count: 4,
@@ -134,7 +134,7 @@ const RECORDS = [
url: 'https://www.cctv-database.com/camera/axis-q6054-e/',
aliases: ['AXIS Q6054-E', 'Q6054-E 50 Hz', 'Q6054-E 60 Hz'],
fov_deg: '62.9-2.2 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 1, w: 1280, h: 720, label: '720p HD' },
ptz: true,
autotracking: true,
@@ -149,7 +149,7 @@ const RECORDS = [
url: 'https://www.cctv-database.com/camera/axis-q6155-e/',
aliases: ['AXIS Q6155-E', 'Q6155-E 50 Hz', 'Q6155-E 60 Hz'],
fov_deg: '66.7-2.36 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD' },
ptz: true,
autotracking: true,
@@ -171,7 +171,7 @@ const RECORDS = [
'VG4-500',
],
fov_deg: '57.8-1.7 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 0.4, label: '540 TVL (NTSC/PAL)' },
ptz: true,
autotracking: true,
@@ -186,7 +186,7 @@ const RECORDS = [
url: 'https://www.cctv-database.com/camera/bosch-autodome-ip-dynamic-7000-hd/',
aliases: ['AUTODOME IP dynamic 7000 HD', 'AUTODOME 7000 dynamic HD'],
fov_deg: '65-2.3 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD (2MP)' },
ptz: true,
autotracking: true,
@@ -206,7 +206,7 @@ const RECORDS = [
'VG5-7130',
],
fov_deg: '59-2.1 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 1, w: 1280, h: 720, label: '720p HD (1.3MP sensor)' },
ptz: true,
autotracking: true,
@@ -226,7 +226,7 @@ const RECORDS = [
'NDP-5502-30',
],
fov_deg: '60.9-2.4 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD (2MP)' },
ptz: true,
varifocal: true,
@@ -245,7 +245,7 @@ const RECORDS = [
'NDP-7512-Z30K',
],
fov_deg: '64.7-2.3 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD (2MP)' },
ptz: true,
autotracking: true,
@@ -279,7 +279,7 @@ const RECORDS = [
'iDome 3925',
'Cohu iDome 3920 Series',
],
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 0.4, label: 'NTSC / 470 TVL' },
ptz: true,
varifocal: true,
@@ -292,7 +292,7 @@ const RECORDS = [
type: 'ptz',
url: 'https://www.cctv-database.com/camera/cohu-3950-iview/',
aliases: ['3950', '3950 Series', 'Cohu 3950 Series iVIEW'],
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 0.4, label: 'NTSC / 470 TVL' },
ptz: true,
varifocal: true,
@@ -305,7 +305,7 @@ const RECORDS = [
type: 'ptz',
url: 'https://www.cctv-database.com/camera/cohu-3960/',
aliases: ['3960 Series', '3964', '3965', 'Cohu 3960 iView'],
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 0.4, label: 'NTSC/PAL / 470 TVL' },
ptz: true,
varifocal: true,
+5 -5
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { createCyclonesLayer } from './index.js';
import { setLocale, t } from '../../i18n/index.js';
// The exact string the server sends (server/providers/cyclones.js:8).
// The exact string the server sends (COVERAGE in server/providers/cyclones.js).
const SERVER_COVERAGE =
'Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.';
const time = '2026-09-16T03:00:00.000Z';
@@ -59,7 +59,7 @@ function harness(feed) {
const translated = () => t(SERVER_COVERAGE);
test('K-35a: es, server snapshot without storms -> coverage line in Spanish', async () => {
test('es: server snapshot without storms -> coverage line in Spanish', async () => {
setLocale('es');
try {
assert.notEqual(translated(), SERVER_COVERAGE, 'es.json has a translation');
@@ -77,7 +77,7 @@ test('K-35a: es, server snapshot without storms -> coverage line in Spanish', as
}
});
test('K-35b: es, server snapshot with a storm -> coverage line in Spanish', async () => {
test('es: server snapshot with a storm -> coverage line in Spanish', async () => {
setLocale('es');
try {
const layer = harness({ getSnapshot: async () => snapshot([storm]) });
@@ -94,7 +94,7 @@ test('K-35b: es, server snapshot with a storm -> coverage line in Spanish', asyn
}
});
test('K-35c: es, unavailable without reason -> fallback status not the English literal', async () => {
test('es: unavailable without reason -> fallback status not the English literal', async () => {
setLocale('es');
try {
const layer = harness({
@@ -115,7 +115,7 @@ test('K-35c: es, unavailable without reason -> fallback status not the English l
}
});
test('K-35: en coverage and fallback status read as before', async () => {
test('en: coverage and fallback status read as before', async () => {
const layer = harness({ getSnapshot: async () => snapshot([]) });
layer.enable();
await layer.update();
+3 -5
View File
@@ -99,9 +99,8 @@ export const STEP_ANCHOR_FAST_ATTEMPTS = 5;
export const FLIGHT_PROGRESS_MS = 250;
// ---------------------------------------------------------------------------
// Display text: distances, durations and maneuvers in the page's language.
// The proxy's English `instruction` stays the machine value; the row and the
// cards phrase each step from its OSRM type/modifier/road instead.
// Display text in the page's language. The proxy's English `instruction` stays
// the machine value; steps are phrased from their OSRM type/modifier/road.
// ---------------------------------------------------------------------------
/**
@@ -639,8 +638,7 @@ export function directionsStats(state) {
armed,
pointerBlocked,
} = state;
// English in layer state; the layer row translates it.
const source = 'OSM routing'; // i18n-ignore: source name, translated by layerPanel
const source = 'OSM routing'; // English in layer state; the layer row translates it
if (pointerBlocked) {
return {
count: route?.steps.length || 0,
+5 -6
View File
@@ -1,13 +1,12 @@
// K-34: AIS degradation chip text and the traffic "SIMULATED — <reason>" text
// must be in the UI language (they stayed English in es before the fix);
// English keeps its wording.
// The AIS degradation chip and the traffic "SIMULATED — <reason>" text read in
// the UI language; English keeps its wording.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { setLocale, translateKnown } from '../i18n/index.js';
import { deriveAisFeedError } from '../data/aisLiveVessels.js';
import { createModel } from './traffic/model.js';
test('K-34: AIS degraded-feed reasons are localized in es', () => {
test('AIS degraded-feed reasons are localized in es', () => {
setLocale('es');
try {
const cases = [
@@ -34,7 +33,7 @@ test('K-34: AIS degraded-feed reasons are localized in es', () => {
}
});
test('K-34: traffic "SIMULATED — <reason>" is localized in es', () => {
test('traffic "SIMULATED — <reason>" is localized in es', () => {
setLocale('es');
try {
const { trafficFeedPresentation } = createModel({
@@ -55,7 +54,7 @@ test('K-34: traffic "SIMULATED — <reason>" is localized in es', () => {
}
});
test('K-34: English degraded-feed wording is unchanged', () => {
test('English degraded-feed wording is unchanged', () => {
setLocale('en');
assert.equal(
deriveAisFeedError({ status: 'down', reconnectAttempt: 3 }, 0),
+1
View File
@@ -45,6 +45,7 @@ export function createIngestion({
const sourceStale = snapshot.stale || snapshot.freshness === 'unknown';
feed._backoff = sourceStale;
feed._retryAt = 0;
// i18n-ignore: English data; the panel shows it through FEED_ERROR_PATTERNS (ui/statusText.js)
feed._lastError = sourceStale
? sourceAgeMs == null
? msg('Source snapshot time unavailable')
+1 -2
View File
@@ -41,8 +41,7 @@ export function createAdsbGnssSource({
);
throw Object.assign(
new Error(
// The status code form is matched by FEED_ERROR_PATTERNS
// (ui/statusText.js) and gnss/index.js.
// HTTP errors are matched by FEED_ERROR_PATTERNS in ui/statusText.js.
limited
? msg('GNSS feed rate limited (HTTP 429)')
: `GNSS feed unavailable (HTTP ${response.status})`,
@@ -1,4 +1,4 @@
// K-50: every class the bundled OSM names pack can put on a record has a
// Every class the bundled OSM names pack can put on a record has a
// catalog entry, so the selected card shows it in the UI language.
import { test } from 'node:test';
import assert from 'node:assert/strict';
@@ -22,7 +22,7 @@ const englishLabel = Object.fromEntries(
);
for (const locale of ['es', 'ru']) {
test(`every names-pack class is in the ${locale} catalog (K-50)`, () => {
test(`every names-pack class is in the ${locale} catalog`, () => {
const catalog = getCatalog(locale);
const missing = pack.classes.filter(
(c) => catalog[`installation class|${englishLabel[c]}`] === undefined,
+2 -4
View File
@@ -7,10 +7,8 @@ import {
} from './policy.js';
import { formatNumber, t, tn } from '../../i18n/index.js';
// installationFeedback.js composes its guidance inside the portable
// node/browser graph (the HUD summary in loadingFeedback.js reads the same
// module), so it hands back English marked with msg() plus the values;
// getStats is the browser presentation boundary that translates them.
// installationFeedback.js is shared with the HUD (src/loadingFeedback.js) and
// hands back msg()-marked English plus values; getStats translates them here.
function localizeInstallationFeedback(feedback) {
if (feedback.text !== undefined) {
const values = {};
@@ -6,7 +6,7 @@ import { setLocale } from '../../i18n/index.js';
import { installationTitle } from './classLabel.js';
import { formatAwarenessLabel } from '../../data/militaryAwarenessEngine.js';
// K-33: unnamed installations get an English label in data ("Firing range",
// Unnamed installations get an English label in data ("Firing range",
// "Military land", "Military airfield"; OpenFreeMap polygons: "Military area").
// The data stays English; the map card title reads in the UI language.
const { records } = normalizeMilitaryInstallations({
@@ -24,7 +24,7 @@ const { records } = normalizeMilitaryInstallations({
});
const names = [...records.map((r) => r.name), UNNAMED_MILITARY_AREA];
test('K-33: unnamed installations keep English names in data', () => {
test('unnamed installations keep English names in data', () => {
assert.deepEqual(names, [
'Firing range',
'Military land',
@@ -34,7 +34,7 @@ test('K-33: unnamed installations keep English names in data', () => {
]);
});
test('K-33: unnamed installation card title reads in the UI language', () => {
test('unnamed installation card title reads in the UI language', () => {
try {
for (const locale of ['es', 'ru']) {
setLocale(locale);
@@ -55,13 +55,13 @@ test('K-33: unnamed installation card title reads in the UI language', () => {
}
});
test('K-33: English card titles are unchanged', () => {
test('English card titles are unchanged', () => {
setLocale('en');
for (const name of names) assert.equal(installationTitle(name), name);
assert.equal(installationTitle(''), '');
});
test('K-33: the Contacts line names an unnamed installation in the UI language', () => {
test('the Contacts line names an unnamed installation in the UI language', () => {
try {
setLocale('en');
for (const name of names)
@@ -0,0 +1,85 @@
// A launch without a Launch Library status gets our 'Unknown'. The record keeps
// it English; the mission panel translates it and shows provider statuses as sent.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../../i18n/index.js';
import {
launchStatusAllowsOrbit,
normalizeRocketLaunches,
} from '../../data/rocketLaunches.js';
import { UNKNOWN_LAUNCH_STATUS } from './model.js';
import { createPanel } from './panel.js';
const NOW = new Date('2026-07-27T00:00:00Z');
function shownStatus(status) {
const outputs = new Map();
const state = {
_missionPanel: {
querySelector(selector) {
if (!outputs.has(selector))
outputs.set(selector, {
textContent: '',
closest: () => null,
removeAttribute() {},
});
return outputs.get(selector);
},
closest: () => null,
},
_selectedLaunchId: 'mission-1',
_launches: [
{
id: 'mission-1',
name: 'Example',
status,
payloads: [],
recoveryStages: [],
},
],
_replayTracks: new Map(),
};
createPanel({
state,
parts: {
overlays: { shortMissionLabel: (name) => name },
policyHelpers: { missionPathPresentation: () => ({}) },
replay: { syncReplayButton() {} },
},
}).renderMissionPanel();
return outputs.get('[data-mission-status]').textContent;
}
test('a launch without a status keeps the English fallback in its record', () => {
const [launch] = normalizeRocketLaunches(
{
results: [
{
id: 'no-status',
name: 'Test Flight',
net: '2026-07-20T10:00:00Z',
pad: {
name: 'Pad A',
location: { name: 'Test Range', coordinates: '12.5,45.5' },
},
},
],
},
NOW,
);
assert.equal(UNKNOWN_LAUNCH_STATUS, 'Unknown');
assert.equal(launch.status, 'Unknown');
assert.equal(launchStatusAllowsOrbit(launch.status), true);
});
test('the mission panel translates only the fallback status', (t) => {
t.after(() => setLocale('en'));
setLocale('en');
assert.equal(shownStatus(UNKNOWN_LAUNCH_STATUS), 'Unknown');
setLocale('ru');
assert.equal(shownStatus(UNKNOWN_LAUNCH_STATUS), 'Неизвестно');
assert.equal(shownStatus('Go for Launch'), 'Go for Launch');
setLocale('es');
assert.equal(shownStatus(UNKNOWN_LAUNCH_STATUS), 'Desconocido');
assert.equal(shownStatus('Launch Successful'), 'Launch Successful');
});
+8 -2
View File
@@ -1,6 +1,13 @@
import * as Cesium from 'cesium';
import { msg } from '../../i18n/index.js';
import { WINDOW_DAYS } from './policy.js';
/**
* Fallback when Launch Library sends no status. English in the record
* (launchStatusAllowsOrbit, analyst records); the mission panel translates it.
*/
export const UNKNOWN_LAUNCH_STATUS = msg('Unknown');
export function createModel({ state: layerState, services, parts, source }) {
function finiteCoordinate(value) {
const number = Number(value);
@@ -192,8 +199,7 @@ export function createModel({ state: layerState, services, parts, source }) {
launch.id || launch.slug || launch.name || `launch-${date}`,
),
name: launch.name || 'Unnamed launch',
// i18n-ignore: Launch Library status names are provider data, shown as sent
status: launch.status?.name || 'Unknown',
status: launch.status?.name || UNKNOWN_LAUNCH_STATUS,
launchTime: Number.isFinite(date)
? new Date(date).toISOString()
: null,
+9 -1
View File
@@ -4,6 +4,7 @@ import {
MISSION_CLOSE_VIEW_RANGE_M,
MISSION_GLOBE_VIEW_RANGE_M,
} from './policy.js';
import { UNKNOWN_LAUNCH_STATUS } from './model.js';
export function createPanel({ state: layerState, services, parts, source }) {
/**
@@ -191,7 +192,14 @@ export function createPanel({ state: layerState, services, parts, source }) {
layerState._missionPanel.querySelector('[data-mission-title]').textContent =
parts.overlays.shortMissionLabel(launch.name, 32).toUpperCase();
setMissionPanelField('[data-mission-provider]', launch.provider);
setMissionPanelField('[data-mission-status]', launch.status);
// Launch Library status names are provider data, shown as sent; only the
// fallback for a launch without one is ours to translate.
setMissionPanelField(
'[data-mission-status]',
launch.status === UNKNOWN_LAUNCH_STATUS
? t(UNKNOWN_LAUNCH_STATUS)
: launch.status,
);
setMissionPanelField(
'[data-mission-site]',
launch.launchSite && launch.launchSite !== 'Unknown launch site'
+2 -2
View File
@@ -1,4 +1,4 @@
// K-37: the Local ADS-B click card follows the UI language; English keeps its
// The Local ADS-B click card follows the UI language; English keeps its
// wording (index.test.mjs pins the full English card).
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -86,7 +86,7 @@ test('the class line names the aircraft class in the UI language', async () => {
}
});
// C8: the receiver bands follow the UI language too ("1090 МГц").
// The receiver bands follow the UI language too ("1090 МГц").
test('ru: the receiver line names the bands in Russian', async () => {
const { localAdsbReceiverLine } = await import('./card.js');
setLocale('ru');
-4
View File
@@ -58,10 +58,6 @@ export function localAdsbReceiverLine(record) {
.join(' · ');
}
// policy.js keeps these labels in English (HEARD_BY_RECEIVER, BAND_LABELS,
// SOURCE_LABELS); the card writes them out here so that it can show them
// translated.
/** A receiver band (policy.js BAND_LABELS) in the UI language: "1090 МГц". */
function bandLabel(band) {
switch (String(band)) {
@@ -1,41 +0,0 @@
// A Local ADS-B feed problem can be the whole error (nothing heard, WebUSB
// not producing); the row shows it in the UI language like any other piece.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../../i18n/index.js';
import { LayerPanel } from '../../ui/layerPanel.js';
import { localAdsbStatus } from './status.js';
const receiver = {
webUsbSupported: true,
connected: false,
mode: 'adsb',
status: 'idle',
};
function rowFor(status) {
return LayerPanel.prototype._buildMetaText({
id: 'local-adsb',
enabled: true,
stats: { count: 0, lastUpdate: Date.now(), ...status },
});
}
for (const locale of ['es', 'ru']) {
for (const status of ['unreachable', 'invalid']) {
test(`${locale}: a lone ${status} decoder feed with nothing heard is translated`, () => {
setLocale(locale);
try {
const st = localAdsbStatus({
receiver,
feedState: {
configured: true,
feeds: [{ id: 'a', label: '1090', band: '1090', status }],
},
heard: 0,
});
const row = rowFor(st);
assert.doesNotMatch(row, /\b(unreachable|invalid)\b/, row);
} finally {
setLocale('en');
}
});
}
}
+42 -12
View File
@@ -1,13 +1,12 @@
// The Local ADS-B row's working texts ("3 heard · 5.8 msg/s",
// "2 feeds live · 14 heard", "listening", "USB error", feed problems) read in
// the UI language.
// The Local ADS-B row's working texts ("3 heard · 5.8 msg/s", "listening",
// "USB error", feed problems, even as the whole error) read in the UI language.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../../i18n/index.js';
import { LayerPanel } from '../../ui/layerPanel.js';
import { localAdsbStatus } from './status.js';
const receiver = {
const streaming = {
webUsbSupported: true,
connected: true,
mode: 'adsb',
@@ -15,24 +14,55 @@ const receiver = {
messagesPerSecond: 5.8,
};
function rowFor(status) {
const idle = {
webUsbSupported: true,
connected: false,
mode: 'adsb',
status: 'idle',
};
function rowFor(status, count) {
return LayerPanel.prototype._buildMetaText({
id: 'local-adsb',
enabled: true,
stats: { count: 3, lastUpdate: Date.now(), ...status },
stats: { count, lastUpdate: Date.now(), ...status },
});
}
function inLocale(locale, run) {
setLocale(locale);
try {
run();
} finally {
setLocale('en');
}
}
for (const locale of ['es', 'ru']) {
test(`${locale}: the streaming Local ADS-B row is translated`, () => {
setLocale(locale);
try {
inLocale(locale, () => {
const row = rowFor(
localAdsbStatus({ receiver, feedState: null, heard: 3 }),
localAdsbStatus({ receiver: streaming, feedState: null, heard: 3 }),
3,
);
assert.doesNotMatch(row, /\bheard\b/, row);
} finally {
setLocale('en');
}
});
});
for (const status of ['unreachable', 'invalid']) {
test(`${locale}: a lone ${status} decoder feed with nothing heard is translated`, () => {
inLocale(locale, () => {
const st = localAdsbStatus({
receiver: idle,
feedState: {
configured: true,
feeds: [{ id: 'a', label: '1090', band: '1090', status }],
},
heard: 0,
});
const row = rowFor(st, 0);
assert.doesNotMatch(row, /\b(unreachable|invalid)\b/, row);
});
});
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
// K-37 / K-29: the selected-fire card of Fire Perimeters and its accessible
// The selected-fire card of Fire Perimeters and its accessible
// label follow the UI language, like the legend of the same layer
// (perimeters/index.js). cards.test.mjs pins the English card.
import test from 'node:test';
@@ -82,7 +82,7 @@ for (const locale of ['es', 'ru']) {
});
}
// C5 / C11 / E7: known WFIGS values, the cost and the unnamed title follow the
// Known WFIGS values, the cost and the unnamed title follow the
// UI language; the row keeps the feed's English for the AI tools.
const expected = {
es: {
+2 -4
View File
@@ -146,10 +146,8 @@ function formatAge(deltaMs) {
* @returns {Object} World-overlay entry without `position`.
*/
export function buildIncidentCard(row, nowMs, { link = null } = {}) {
// Feed values stay as WFIGS sends them in the row (and in what the AI tools
// read); the card translates the known cause, behavior and complexity values
// and the wording around them. State, category, county and complex names
// are shown as sent.
// The row keeps WFIGS values as sent (AI tools read them); the card translates
// the known cause, behavior and complexity values and the wording around them.
const facts = [];
if (Number.isFinite(row.acres)) {
const acres = Math.round(row.acres);
+2 -2
View File
@@ -62,8 +62,8 @@ export const DEFAULT_RADIO_FILTER = 'all';
export const GLOBAL_RADIO_ALTITUDE_M = 2_000_000;
// Genre labels are marked for i18n extraction; categories.js translates them.
// Kept local so this module stays free of the i18n import.
// Genre labels stay English data (the radio source, shared with the AI tools,
// imports this module); categories.js translates them.
const msg = (text) => text;
export const MUSIC_GENRES = Object.freeze([
+1 -1
View File
@@ -47,8 +47,8 @@ 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 = msg('Comparison in use by another scene');
// Kept English in the snapshot (AI tools read it); the card translates them.
const COMPARISON_IN_USE = msg('Comparison in use by another scene');
const COMPARISON_UNAVAILABLE = msg('Esri map unavailable · no swipe');
const COMPARISON_NEEDS_GLOBE = msg('Swipe needs a globe map');
const ESRI_KEPT = msg('Imagery stays on Esri · CLEAR to use Google 3D');
+2 -2
View File
@@ -71,8 +71,8 @@ export function createIngestion({
// At least one group loaded — proceed with a fresh rebuild, but keep the
// partial outage visible at the layer control instead of presenting the
// reduced catalog as a fully healthy refresh. English data; the panel
// shows it through FEED_ERROR_PATTERNS (ui/statusText.js).
// reduced catalog as a fully healthy refresh.
// i18n-ignore: English data; the panel shows it through FEED_ERROR_PATTERNS (ui/statusText.js)
layerState._lastError = failed.length
? `${failed.length} CelesTrak group${failed.length === 1 ? '' : 's'} unavailable`
: null;
+1 -1
View File
@@ -704,7 +704,7 @@ export function createLineCoverage({
state.coverage.pending.get(key) !== controller
)
return;
state.coverage.lastError = error?.message || 'Coverage tile failed';
state.coverage.lastError = error?.message || msg('Coverage tile failed');
if (error?.keyRequired) state.keyRequired = true;
if (error?.keyRejected) {
// Every other tile would be refused too: stop asking.
+1 -1
View File
@@ -1,4 +1,4 @@
// K-37: the traffic road-status notes follow the UI language; English keeps
// The traffic road-status notes follow the UI language; English keeps
// its wording.
import test from 'node:test';
import assert from 'node:assert/strict';
+1 -2
View File
@@ -476,8 +476,7 @@ export function createIngestion({ state, services, parts, source }) {
`[Data:Transit] ${feed.id} poll failed:`,
error?.message || error,
);
// English data; the panel shows it through FEED_ERROR_PATTERNS
// i18n-ignore: translated on display (ui/statusText.js)
// i18n-ignore: English data, translated on display (ui/statusText.js)
status.error = `${feed.name} feed unavailable`;
status.retryInSec = Number.isFinite(error?.retryInSec)
? error.retryInSec
+2 -4
View File
@@ -321,10 +321,8 @@ export function aggregateTransitFeedHealth(statuses, nowMs) {
};
}
// AI tools (src/tools/queries/mobility.js) import this module, so it stays
// free of i18n: the card copy below calls the translate function `t` that the
// caller hands in. The browser selection passes the real one from src/i18n;
// without it the copy is English.
// AI tools (src/tools/queries/mobility.js) import this module, so no i18n here:
// the card copy takes `t` from the caller (selection.js); without it, English.
const msg = (text) => text;
/** English {placeholder} interpolation, the default `t` of the card copy. */
+1 -3
View File
@@ -278,10 +278,8 @@ export function createQueries({ state, parts }) {
: {}),
degraded: count > 0 && health.degraded,
loading: health.loading && count === 0,
// English data; the panel shows it through STATUS_PIECE_PATTERNS
// (ui/statusText.js).
loadingLabel:
// i18n-ignore: translated on display
// i18n-ignore: English data, translated on display (ui/statusText.js)
health.loading && count === 0
? `Loading ${active.map((feed) => feed.name).join(', ')}`
: undefined,
@@ -1,4 +1,4 @@
// K-37: the Transit selection card follows the UI language. The copy builder
// The Transit selection card follows the UI language. The copy builder
// stays free of i18n (AI tools import policy.js); the browser hands it t().
import test from 'node:test';
import assert from 'node:assert/strict';
+2 -4
View File
@@ -1,10 +1,8 @@
import * as Cesium from 'cesium';
import { t, msg } from '../../i18n/index.js';
// The vessels ingestion owner is a portable module graph (it also runs in the
// server-side source builds), so it writes stable English feed labels into
// state. Localize those known labels here, at the presentation read; anything
// else (a bespoke manager label) passes through verbatim.
// ingestion.js is a portable export (package.json) and writes English labels
// into state; translate the known ones here, pass anything else through.
const FEED_STATE_LABEL_KEYS = Object.freeze({
'refreshing...': msg('refreshing...'),
'loading...': msg('loading...'),
@@ -3,9 +3,8 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { setLocale, t } from '../../i18n/index.js';
import { WIND_FIELDS } from './fields.js';
import { SCALAR_UNAVAILABLE, WIND_FIELDS } from './fields.js';
import { fieldUnavailableText } from './rendering.js';
import { SCALAR_UNAVAILABLE } from './source.js';
for (const locale of ['es', 'ru']) {
test(`${locale}: every field's unavailable status has a translation`, () => {
+11
View File
@@ -1,5 +1,16 @@
import { msg } from '../../i18n/marker.js';
import { normalizeLongitude, sampleWind } from './model.js';
// Imported by src/tools: the catalog-free marker, not src/i18n/index.js. The
// texts stay English data and the wind layer and layer panel show them
// translated.
/** Status of a scalar overlay the provider could not supply, by overlay. */
export const SCALAR_UNAVAILABLE = Object.freeze({
temperature: msg('Temperature field unavailable'),
pressure: msg('Mean sea level pressure field unavailable'),
});
/** Fixed physical scales shared by imagery and the field legend. */
export const WIND_FIELDS = Object.freeze({
speed: Object.freeze({
+3 -4
View File
@@ -7,17 +7,16 @@ 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 { SCALAR_UNAVAILABLE } from './source.js';
import { advectParticle, sampleWind } from './model.js';
import {
createFieldRaster,
SCALAR_UNAVAILABLE,
trailEraseAlpha,
windTrailColor,
} from './fields.js';
// A scalar overlay that cannot be drawn, in English for diagnostics; the
// panel status shows it translated (index.js). The scalar texts are the ones
// the source reports. setOptions only accepts these overlays.
// English for diagnostics; the row status shows it translated (index.js). The
// scalar texts are the source's own; setOptions accepts only these overlays.
const FIELD_UNAVAILABLE = Object.freeze({
speed: msg('Wind speed field unavailable'),
...SCALAR_UNAVAILABLE,
+1 -6
View File
@@ -1,15 +1,10 @@
import { readWindBody } from '../../sources/windBody.js';
import { SCALAR_UNAVAILABLE } from './fields.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;
/** Status of a scalar overlay the provider could not supply, by overlay. */
export const SCALAR_UNAVAILABLE = Object.freeze({
temperature: msg('Temperature field unavailable'),
pressure: msg('Mean sea level pressure field unavailable'),
});
/** Create a bounded, cancellable source for the same-origin wind provider. */
export function createWindSource({
fetchImpl = (...args) => globalThis.fetch(...args),
+1 -1
View File
@@ -3,7 +3,7 @@ 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 () => {
test('stale wind forecast is marked in the UI language', async () => {
const rendering = Object.fromEntries(
['attach', 'start', 'stop', 'clear', 'destroy', 'setField'].map((n) => [
n,
+1 -1
View File
@@ -1,4 +1,4 @@
// K-38: wind source failures are English data (src/layers/wind/source.js) and
// 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';
+1 -1
View File
@@ -39,7 +39,7 @@ async function serverManifestOnUpstreamFailure() {
return JSON.parse(body);
}
test('en: the upstream failure reason reads as before (K-17)', async () => {
test('en: the upstream failure reason reads as before', async () => {
const manifest = await serverManifestOnUpstreamFailure();
const layer = harness({ getSnapshot: async () => manifest });
await layer.update();
+4 -4
View File
@@ -244,7 +244,7 @@ export function reduceTrafficSyncFeedback(
// Neutral default: the layer always supplies its own LIVE/SIMULATED
// label, and a fallback string must never claim a live feed on a
// keyless build.
// i18n-ignore: portable node/browser status vocabulary; src/ui/shellFeedback.js translates it at the presentation boundary
// i18n-ignore: translated in src/ui/shellFeedback.js
label: label || 'syncing road network',
progressText: hasProgress ? `${progressPct}%` : '...',
};
@@ -393,7 +393,7 @@ export function presentLoadingFeedback(state, summary, nowMs) {
);
return {
state: 'retry',
// i18n-ignore: portable node/browser status vocabulary; src/ui/shellFeedback.js translates it at the presentation boundary
// i18n-ignore: translated in src/ui/shellFeedback.js
label: (
camera.cameraRetry.error || 'Overpass temporarily unavailable'
).toUpperCase(),
@@ -440,7 +440,7 @@ export function presentLoadingFeedback(state, summary, nowMs) {
if (active.length === 1 && active[0].cameraRetry && !summary.disabling) {
return {
state: 'loading',
// i18n-ignore: portable node/browser status vocabulary; src/ui/shellFeedback.js translates it at the presentation boundary
// i18n-ignore: translated in src/ui/shellFeedback.js
label: active[0].cameraRetry.retrying
? 'RETRYING ALPR CAMERAS'
: 'FETCHING ALPR CAMERAS',
@@ -455,7 +455,7 @@ export function presentLoadingFeedback(state, summary, nowMs) {
) {
return {
state: 'loading',
// i18n-ignore: portable node/browser status vocabulary; src/ui/shellFeedback.js translates it at the presentation boundary
// i18n-ignore: translated in src/ui/shellFeedback.js
label: active[0].installationRetry.retrying
? 'RETRYING MAPPED SITES'
: 'FETCHING MAPPED SITES',
-2
View File
@@ -12,8 +12,6 @@ import { cancelCameraArrival } from './data/cameraArrival.js';
import { guardCameraAboveGround } from './cameraGroundGuard.js';
import { CITY_POIS } from './cityPois.js';
export { CITY_POIS };
/** Newest landmark arrival; an older post-arrival ground guard yields to it. */
let arrivalGeneration = 0;
+1 -2
View File
@@ -38,8 +38,7 @@ export function mapStackChipModel(stack, activeId) {
const available = stack?.available !== false;
const label = String(stack?.label ?? stack?.id ?? '');
const requiresIon = stack?.requiresIon === true;
// Stack labels are provider/dataset names (keep-English); the availability
// sentence around them is copy and localizes here at the model boundary.
// Stack labels are provider names and stay English; the sentence translates.
const fallbackReason = requiresIon
? t('Needs {envVars} — add it in Provider Settings', {
envVars: keySetupRequirementEnvVars('cesium-ion'),
+2 -4
View File
@@ -5,11 +5,9 @@
* neither exists there is nowhere to drape and the caller says so.
*/
/** Guidance shown when the active map stack cannot host draped imagery. */
// Marks text for i18n extraction without importing i18n here; the UI
// translates it where it is shown.
const msg = (text) => text;
import { msg } from '../i18n/marker.js';
/** Guidance shown when the active map stack cannot host draped imagery. */
export const NO_IMAGERY_HOST = msg(
'Hidden by this map source · choose a globe map',
);
-1
View File
@@ -133,7 +133,6 @@ test('cockpit refuses without stamping or releasing anything', () => {
});
test('weather layers name their destination in the Cockpit refusal', () => {
// «Exit cockpit to fly to a weather» was the template with a bare noun.
const expected = {
weather: 'Exit cockpit to view weather coverage',
cyclone: 'Exit cockpit to fly to a cyclone',
+10 -10
View File
@@ -357,7 +357,7 @@ function buildWorkload(count) {
id: `mover-${index}`,
position: () => position,
variant: 'label',
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
title: `TRK ${1000 + index}`,
details: [`ALT ${10_000 + index * 25}`],
priority: index % 7,
@@ -468,7 +468,7 @@ function buildPhase3FirmsWorkload(count) {
position,
gapPx: 10,
accent: '224, 82, 82',
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
title: `▲ ${50 + fireIndex} MW`,
details: ['high · 2h · N20'],
selected: false,
@@ -559,7 +559,7 @@ function buildPhase3VesselsWorkload(count) {
position,
gapPx: 10,
accent: '224, 82, 82',
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
title: `▲ ${50 + fireIndex} MW`,
details: ['high · 2h · N20'],
selected: false,
@@ -579,7 +579,7 @@ function buildPhase3VesselsWorkload(count) {
position,
gapPx: selected ? 12 : 10,
accent: '57, 213, 255',
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
title: selected ? 'SELECTED VESSEL' : `VESSEL ${vesselIndex}`,
details: selected
? ['CARGO · 14.5KT · 231°', 'MMSI 353136000 · POS: LIVE']
@@ -654,7 +654,7 @@ function buildPhase3TrackedWorkload(count) {
gevTrackedId: 'flights:allocation-probe',
gevDisplayPosition: selected.position,
gevLabelModel: {
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
title: 'ALLOC01',
details: ['FL350 · 451 kts', 'TEST AIR · A320'],
accent: '#39d0ff',
@@ -705,7 +705,7 @@ function buildPhase4CctvWorkload(count) {
const entry = createCctvThumbnailOverlayEntry({
id: active ? 'cctv:active' : `cctv:${index}`,
position,
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
title: active ? 'ACTIVE CAMERA' : `CAMERA ${index}`,
frameSlot: slot,
rank: index,
@@ -879,7 +879,7 @@ function buildPhase5CivilWorkload(count) {
const entry = registration?.entries[0];
if (!entry)
throw new Error('phase5-civil requires the protected tracked entry');
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
entry.title = 'ALLOC01 · FL350 · 451 kts';
entry.details = ['TEST AIR · A320', 'AUS → LAX'];
return workload;
@@ -894,7 +894,7 @@ function buildPhase5MilitaryWorkload(count) {
if (!entry)
throw new Error('phase5-military requires the protected tracked entry');
entry.id = 'military:allocation-probe';
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
entry.title = 'RCH451';
entry.details = ['C17 · 05-8152', 'USAF · 28000 ft · 400 kt'];
entry.accent = '#ffd166';
@@ -1081,7 +1081,7 @@ function appendSubmarineCableWorkload(workload, count) {
id: `${kind}-reference-${index}-alloc`,
kind,
label:
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
kind === 'cable' ? `Cable System ${index}` : `Landing Point ${index}`,
tip: position,
distanceM: 200_000 + index * 40_000,
@@ -1160,7 +1160,7 @@ function buildAirQualityWorkload(count) {
const entry = createAirQualityOverlayEntry({
id: `air-${index}`,
position,
// i18n-ignore: synthetic allocation-benchmark fixture, not shown as UI
// i18n-ignore: benchmark fixture
title: `Station ${index} ${1 + (index % 10)}`,
accent: '#4cc9f0',
value: 1 + (index % 10),
+2 -9
View File
@@ -65,12 +65,6 @@ import {
} from '../director/document.js';
import { sceneSaveFailureReason } from './sceneSaveError.js';
// Built-in recipe DISPLAY names. Recipe ids, the URL 'scene' param, stored
// project titles, and voice-matching titles all stay English (keep-English
// boundary + persisted data); only the presentation of an UNRENAMED built-in
// localizes, so an operator's renamed scene is never masked by the catalog.
// Recipe and built-in shot titles are marked with msg() in recipes.js.
/** @constant {string} localStorage key for the serialized project */
const STORAGE_KEY = 'godsEyeView.sceneProject.v2';
const STORAGE_CHECKPOINT_KEY = 'godsEyeView.sceneProject.checkpoint.v1';
@@ -413,9 +407,8 @@ export class SceneDirector {
this._controls = new SceneControls({
subscribe: (listener) => this.subscribe(listener),
read: () => ({
// Scene titles surface in the panel through SceneControls; built-in
// recipe and shot titles display their localized name until renamed
// by the user. Shots keep the stored title for editing and matching.
// The panel shows display titles; shots keep the stored title for
// editing and matching.
scenes: this._project.scenes.map((scene) => ({
...scene,
title: displaySceneTitle(scene),
+12 -12
View File
@@ -7,7 +7,7 @@ import { SOURCE_PATH_BEAT_IDS } from '../data/bhoteKoshiShotPaths.js';
const ADDITIONAL_SHOTS = [
{
id: 'second-landslide',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Second landslide',
lat: 28.399234,
lon: 85.535806,
@@ -18,7 +18,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'dhunche',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Dhunche',
lat: 28.161431,
lon: 85.324278,
@@ -29,7 +29,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'mailung-upper-trishuli',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Mailung / Upper Trishuli bridge',
lat: 28.140642,
lon: 85.265996,
@@ -40,7 +40,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'mailung-bazzar',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Mailung Bazzar, Dandaguan',
lat: 28.126526,
lon: 85.248027,
@@ -51,7 +51,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'dandagaun',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Dandagaun, Rasuwa',
lat: 28.124424,
lon: 85.245154,
@@ -62,7 +62,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'dandagaun-viewpoint',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Dandagaun viewpoint restaurant',
lat: 28.086663,
lon: 85.229914,
@@ -73,7 +73,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'betrawati-bazaar',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Betrawati Bazaar',
lat: 28.029827,
lon: 85.227694,
@@ -84,7 +84,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'bhainse',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhainse',
lat: 28.023087,
lon: 85.229449,
@@ -95,7 +95,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'devighat-taadi-khola-bridge',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Devighat / Taadi Khola Bridge',
lat: 27.921132,
lon: 85.177113,
@@ -106,7 +106,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'charaudi',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Charaudi',
lat: 27.865095,
lon: 84.838424,
@@ -117,7 +117,7 @@ const ADDITIONAL_SHOTS = [
},
{
id: 'final-view',
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Final view',
lat: 28.0834,
lon: 85.206,
@@ -351,7 +351,7 @@ export function expandNepalEvidencePack(base) {
shotPatches: [
...(base.shotPatches || []),
{
// i18n-ignore: shot title (mostly a place name), matched by title on upgrade
// i18n-ignore: stored shot title, matched verbatim
title: 'Final view',
layers: finalView.layers,
},
+30 -31
View File
@@ -27,7 +27,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
fromShotTitles: ['Shot 1', 'Shot 2', 'Shot 3'],
cameraPath: [
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Global Incident Context',
lat: 27.5962,
lon: 78.5718,
@@ -45,7 +45,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Nepal-Focused Globe Rotation',
lat: 28.7248,
lon: 81.3226,
@@ -63,7 +63,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Regional Approach',
lat: 27.0354,
lon: 84.6287,
@@ -81,7 +81,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Nearby Cities',
lat: 27.891,
lon: 85.428,
@@ -99,7 +99,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Incident Corridor',
lat: 28.0529,
lon: 85.2189,
@@ -117,7 +117,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Flood Path',
lat: 28.1691,
lon: 85.352,
@@ -135,7 +135,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Corridor Overview',
lat: 27.8986,
lon: 84.8723,
@@ -153,7 +153,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Upper Valley',
lat: 28.4187,
lon: 85.4341,
@@ -230,7 +230,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
shotPatches: [
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Global Incident Context',
visual: { mapStack: 'photoreal' },
layers: {
@@ -246,7 +246,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Nepal-Focused Globe Rotation',
visual: { mapStack: 'photoreal' },
layers: {
@@ -262,7 +262,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Regional Approach',
visual: { mapStack: 'photoreal' },
layers: {
@@ -278,7 +278,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Nearby Cities',
visual: { mapStack: 'photoreal' },
layers: {
@@ -294,7 +294,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Incident Corridor',
camera: {
lat: 28.0529,
@@ -319,7 +319,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Flood Path',
visual: { mapStack: 'photoreal' },
layers: {
@@ -335,7 +335,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Corridor Overview',
visual: { mapStack: 'photoreal' },
layers: {
@@ -351,7 +351,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bhote Koshi Upper Valley',
holdSec: 8,
visual: { mapStack: 'photoreal' },
@@ -374,7 +374,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Upper Valley Collapse',
camera: {
lat: 28.417,
@@ -398,29 +398,29 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Nepal-China Border Gate',
visual: { mapStack: 'photoreal' },
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Timure Evidence Cluster',
visual: { mapStack: 'photoreal' },
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Syabru Besi Passage',
visual: { mapStack: 'photoreal' },
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bidur / Trishuli Consequence',
visual: { mapStack: 'photoreal' },
},
],
cameraPath: [
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Upper Valley Collapse',
lat: 28.417,
lon: 85.3937,
@@ -444,7 +444,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Debris-Dammed Lake',
lat: 28.392914,
lon: 85.570921,
@@ -468,7 +468,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Nepal-China Border Gate',
mapStack: 'photoreal',
lat: 28.316789,
@@ -499,7 +499,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Timure Evidence Cluster',
mapStack: 'photoreal',
lat: 28.285968,
@@ -528,7 +528,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Syabru Besi Passage',
mapStack: 'photoreal',
lat: 28.210306,
@@ -557,7 +557,7 @@ const BHOTE_KOSHI_NEPAL_BASE_RECIPE = Object.freeze({
},
},
{
// i18n-ignore: shot title, matched by title when the recipe upgrades a saved project
// i18n-ignore: stored shot title, matched verbatim
title: 'Bidur / Trishuli Consequence',
mapStack: 'photoreal',
lat: 27.981737,
@@ -1057,10 +1057,9 @@ export function displaySceneTitle(scene) {
* Display title for a shot: built-in titles and the numbered "Shot N"
* fallback localize; any other title shows as stored.
*
* With the shot itself, a built-in title localizes only when the shot came
* from a recipe (`sourcePackId`), so an operator-typed title that happens to
* match a built-in one is never masked by the catalog. Without it, provenance
* is unknown and the title alone decides, as before.
* With `shot`, a built-in title localizes only for a recipe shot
* (`sourcePackId`), so a typed title that matches a built-in one stays as
* typed. Without `shot`, the title alone decides.
* @param {string} title
* @param {{sourcePackId?: string}} [shot]
* @param {{id?: string}} [scene] The shot's scene, for projects saved before
+4 -10
View File
@@ -1,14 +1,8 @@
/**
* The reason shown in «Scene not saved — {reason}» when the project fails its
* own validation on save.
*
* A SceneDocumentError names a JSON path and an English check
* («$.scenes[6].title: expected nonempty text, at most 4096 characters») —
* right for the console and for import diagnostics, meaningless in a toast.
* The failures an author can cause from the panel get a plain sentence with
* 1-based scene and shot numbers. Anything else keeps the technical text in
* English and gets a general sentence in a translation; director.js logs the
* error either way.
* The reason for «Scene not saved — {reason}» when the project fails
* validation. The validator's text («$.scenes[6].title: expected …») is for
* the console: known failures get a sentence with 1-based numbers, others a
* general one (English keeps the raw text). director.js logs the full error.
*/
import { formatNumber, getLocale, t } from '../i18n/index.js';
+2 -3
View File
@@ -1,6 +1,5 @@
// Proposal 8: «Scene not saved — {reason}» showed the validator's JSON path
// and English check. Known failures read as a sentence; the rest get a
// general reason (the technical text is logged by director.js).
// «Scene not saved — {reason}» never shows the validator's JSON path: known
// failures read as a sentence, the rest get a general reason.
import { test } from 'node:test';
import assert from 'node:assert/strict';
Loaded 100 of 178 files, more files were not shown because too many files have changed in this diff. Show more