mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
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:
1 parent
0ed05568f7
commit
20f4cf3763
178 files changed
+918
-948
No files matched your search
@@ -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' },
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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}` : '.'),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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`,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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' };
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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>`.
|
||||
*/
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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,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
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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. */
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "Неизвестно"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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`, () => {
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
@@ -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 '';
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,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]));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
Vendored
+2
-4
@@ -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');
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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`, () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,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),
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
Reference in new issue
Block a user