mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24: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
+5
-6
@@ -168,12 +168,11 @@
|
||||
`t()`, `tn()` (plurals) and `msg()` in code and `data-i18n*` in markup, keyed
|
||||
by the English text itself; each language is one flat JSON catalog
|
||||
(`src/i18n/locales/ru.json`). The language comes from `?lang=`, the language
|
||||
switch in the Visual Presets dock, then the browser language; English stays
|
||||
the default and the fallback. `npm run i18n`, also run by `npm test`, fails on
|
||||
missing, unused or broken translations. Built on the i18n foundation by
|
||||
@meirm (#601), with the Russian catalog started from #312. It also fails on UI text that is
|
||||
not marked at all, so the whole interface, including surfaces added after
|
||||
the original extraction, is translated (1,679 messages).
|
||||
picker (the globe button in the bottom-right corner), then the browser
|
||||
language; English stays the default and the fallback. `npm run i18n`, also
|
||||
run by `npm test`, fails on missing, unused or broken translations. Built on
|
||||
the i18n foundation by @meirm (#601), with the Russian catalog started
|
||||
from #312. It also fails on UI text that is not marked at all.
|
||||
- Street Level counts unique tiles before ranking so overlapping dateline halves
|
||||
in polar views do not waste slots in the 16-tile overview budget.
|
||||
|
||||
|
||||
@@ -3241,14 +3241,13 @@ Shipped locales: **en** (source and fallback), **es** and **ru**. The code lives
|
||||
value). English has no catalog; every other locale is
|
||||
`src/i18n/locales/<code>.json`, and a missing entry falls back to English.
|
||||
- **Locale choice.** `?lang=` → stored choice (`gev:locale:v1`) →
|
||||
`navigator.languages` → English. `?lang=` is not stored: it applies while it
|
||||
stays in the address, and the copied share link drops it, so a recipient keeps
|
||||
their own language. The language picker in the bottom-right corner (a globe
|
||||
button beside POWER UP, `src/ui/localePicker.js`) stores the choice, removes `?lang=` (keeps it only where storage is blocked) and reloads,
|
||||
so nothing re-renders live; it asks first when the reload would lose work
|
||||
(unsaved CCTV calibration, the open cockpit, Draw marks on the board,
|
||||
imported map files, an active voice session — each registered through
|
||||
`registerUnsavedWork`).
|
||||
`navigator.languages` → English. `?lang=` is not stored, and share links drop
|
||||
it, so a recipient keeps their own language. The globe picker in the
|
||||
bottom-right corner (`src/ui/localePicker.js`) stores the choice, drops
|
||||
`?lang=` (keeps it only when storage is blocked) and reloads. It asks first
|
||||
when the reload would lose work registered with `registerUnsavedWork`:
|
||||
unsaved keys or CCTV calibration, Draw marks, imported map files, the open
|
||||
cockpit, a voice session.
|
||||
- **Gate.** `npm run i18n` (and `src/i18n/catalog.test.mjs` under `npm test`)
|
||||
extracts every message and fails on missing, empty, unused,
|
||||
placeholder-mismatched, still-English (plural forms included) or
|
||||
|
||||
+8
-5
@@ -23,6 +23,8 @@ const FEED_STATE_LABELS = { loading: msg('LOADING'), stale: msg('STALE') };
|
||||
label.textContent = t(FEED_STATE_LABELS[state]);
|
||||
```
|
||||
|
||||
A module the server, tools or other portable code also imports must not load the catalogs: it imports `msg` from `src/i18n/marker.js`, which only marks the text, and the UI translates it with `t()` where it shows it. (Older modules keep a local `const msg = (text) => text`, which works the same.)
|
||||
|
||||
`msg()` exists for two reasons. `npm run i18n` finds messages only as string literals inside `t(…)`, `tn(…)` and `msg(…)`, so `t(FEED_STATE_LABELS[state])` alone tells it nothing. And a `t()` call at module load runs before the page has picked its language, so it would freeze the English.
|
||||
|
||||
## Markup
|
||||
@@ -42,7 +44,7 @@ label.textContent = t(FEED_STATE_LABELS[state]);
|
||||
- **Use `tn()` for counts**, not `count === 1 ? … : …`. Russian has three forms.
|
||||
- **Translate the label, not the value.** Layer ids, enums, share-link parameters, API fields, voice tool schemas and feed data (callsigns, vessel and place names) stay as they are; translate only the text shown for them.
|
||||
- **No `t()` at module top level.** Keep the English in `msg()` and call `t()` when rendering.
|
||||
- **Numbers and dates** go through `formatNumber`, `formatDate` and `formatRelativeTime` from `src/i18n/index.js`, which follow the active language.
|
||||
- **Numbers and dates** go through `formatNumber`, `formatDate`, `formatRelativeTime` and `formatUtcDayMonth` from `src/i18n/index.js`, which follow the active language.
|
||||
- **Identical English means identical translation,** unless you give it a context: `t('CLEAR', { context: 'action' })` (a button) is its own catalog entry, `action|CLEAR`, while `t('CLEAR')` (the sky) stays separate. English output is the same.
|
||||
- **Text that should stay English** (a brand, a code, a unit symbol) is simply not wrapped.
|
||||
|
||||
@@ -54,18 +56,18 @@ npm run i18n -- --missing ru # the missing ru entries as JSON
|
||||
npm run i18n -- --prune # drop entries whose English no longer exists
|
||||
```
|
||||
|
||||
Changing the English changes the key, so the old translation goes unused and the new text shows as missing. Add the translation to `src/i18n/locales/ru.json` (or ask a translator), prune, and run `npm test`, which runs the same check and fails until every language is complete.
|
||||
Changing the English changes the key: the old translation goes unused and the new text shows as missing. Add the translation to every `src/i18n/locales/<code>.json` (or ask a translator), prune, and run `npm test`; it fails until every language is complete.
|
||||
|
||||
To see a language, open the app with `?lang=ru`, or use the language picker (the globe button in the bottom-right corner).
|
||||
|
||||
An empty translation is reported, and so is one that is exactly the English ("Bloom" → "Bloom"), including any plural form left as the English: it is reported as still English. When it is right on purpose, such as a code (HDG, CRT), a product name or a word spelled the same in that language, list it under the language in `src/i18n/same-as-english.json`.
|
||||
An empty translation is reported, and so is one left as the English, plural forms included. When English is right on purpose (a code such as CRT, a product name), list it under the language in `src/i18n/same-as-english.json`.
|
||||
|
||||
## Text the check reports as unmarked
|
||||
|
||||
`npm run i18n` also lists UI text that is not marked at all, and fails while there is any; `npm test` runs the same check:
|
||||
`npm run i18n` and `npm test` fail on UI text that is not marked:
|
||||
|
||||
- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`.
|
||||
- **Code:** string literals that reach the page without `t()`, `tn()` or `msg()`: assigned to `textContent`, `innerText`, `innerHTML`, `title`, `placeholder`; passed to `setAttribute('title' | 'aria-label' | 'aria-valuetext' | 'placeholder' | 'alt', …)`, `createTextNode`, `insertAdjacentHTML`, `showToast` (also `showToast?.(…)` and `this._showToast(…)`), `toast`, `setHint`, `announce`, `say`; assigned to a layer's `error`, `_error`, `loadingLabel`, `statusMessage` or `status` (`this._error = …`, `stats.loadingLabel = …`; a `const`/`let` local is not checked); or kept in `label:`, `title:`, `tooltip:`, `placeholder:`, `hint:`, `ariaLabel:`, `accessibilityLabel:`, `caption:`, `subtitle:`, `kicker:`, `emptyText:`, `blurb:`, `countLabel:`, `status:`, `loadingLabel:`, `statusMessage:` properties. Lower-case codes without spaces, such as `status: 'auth-failed'`, are not reported.
|
||||
- **Code:** string literals that reach the page without `t()`, `tn()` or `msg()`: assigned to `textContent`, `title` and the like; passed to `setAttribute('title' | 'aria-label' | …)`, `createTextNode`, `showToast` and other notice helpers; assigned to a layer's `error`, `lastError`, `loadingLabel`, `statusMessage` or `status` (a plain `error = …` too); or kept in `label:`, `title:`, `status:` and similar properties. Not reported: declarations (`let status = …`), default values, ternary branches and lower-case codes (`'auth-failed'`). The full list is `JS_SINKS` in `scripts/i18n.mjs`.
|
||||
|
||||
Fix it by marking the text. When the text must stay as it is, say so where it lives:
|
||||
|
||||
@@ -82,6 +84,7 @@ Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowerca
|
||||
| File | What it holds |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `src/i18n/index.js` | `t`, `tn`, `msg`, the Intl formatters, markup translation, switching language |
|
||||
| `src/i18n/marker.js` | `msg` alone, for modules the server or tools import (no catalogs) |
|
||||
| `src/i18n/locale.js` | shipped languages, their names, choosing the language at boot |
|
||||
| `src/i18n/locales/<code>.json` | one catalog per language, keyed by English text |
|
||||
| `src/i18n/same-as-english.json` | per language, the entries that stay as in English on purpose |
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ npm run i18n -- --missing ru # missing ru entries as JSON, ready to transla
|
||||
npm run i18n -- --prune # drop entries the code no longer uses
|
||||
```
|
||||
|
||||
`npm test` runs the same check (`src/i18n/catalog.test.mjs`). It fails when a translation is missing or empty, when an entry is no longer used, when placeholders differ from the English, when a plural entry lacks a form, or when a translation, or any form of a plural entry, is still the English text. The `--missing` template is not a translation: its strings are empty and its plural forms are the English, so the check fails until every one is translated. If an entry is right as it is in English (a code such as HDG, a product name, a word spelled the same in your language), add it under your language code in `src/i18n/same-as-english.json`.
|
||||
`npm test` runs the same check. It fails on a missing, empty or unused entry, on placeholders that differ from the English, on a missing plural form, and on text or plural forms still in English. The `--missing` template fails until every entry is translated. If an entry is right in English (a code such as CRT, a product name), add it under your language code in `src/i18n/same-as-english.json`.
|
||||
|
||||
Changing English text changes its key: the old translation becomes unused and the new text shows as missing. Translate it and prune.
|
||||
|
||||
|
||||
+28
-10
@@ -4,9 +4,8 @@
|
||||
// npm run i18n -- --prune also drop entries the code no longer uses
|
||||
// npm run i18n -- --missing ru print what ru lacks, as JSON to translate
|
||||
//
|
||||
// A translation identical to the English is reported too, unless it is
|
||||
// listed for that locale in src/i18n/same-as-english.json (codes, product
|
||||
// names, words spelled the same in both languages).
|
||||
// A translation left as the English fails unless src/i18n/same-as-english.json
|
||||
// lists it.
|
||||
//
|
||||
// Messages are the English literals passed to t(), tn() and msg() in src/,
|
||||
// plus the markup opted in with data-i18n* in src/ui/templates and index.html.
|
||||
@@ -220,12 +219,30 @@ const JS_SINKS = [
|
||||
// Also showToast?.(…) and this._showToast(…).
|
||||
/\b_?(?:showToast|toast|setHint|announce|say)(?:\?\.)?\(\s*/g,
|
||||
// `status` values are often codes ('ok', 'auth-failed'); visibleText drops
|
||||
// those, so only readable text is reported.
|
||||
/(?:^|[{,\s])(?:label|title|tooltip|placeholder|hint|ariaLabel|accessibilityLabel|caption|subtitle|kicker|emptyText|blurb|countLabel|status|loadingLabel|statusMessage)\s*:\s*/gm,
|
||||
// Layer status set by assignment: this._error = …, stats.loadingLabel = ….
|
||||
// A declared local (`const error = …`) is scratch, not a status field.
|
||||
/(?<![\w$])(?<!\b(?:const|let|var)\s+)_?(?:error|loadingLabel|statusMessage|status)\s*(?:\?\?|\|\|)?=(?![=>])\s*/g,
|
||||
// those, so only readable text is reported. A name right after `?` is a
|
||||
// ternary branch (`ok ? status : 'Fallback'`), not a property.
|
||||
/(?:^|[{,\s])(?<!\?\s*)(?:label|title|tooltip|placeholder|hint|ariaLabel|accessibilityLabel|caption|subtitle|kicker|emptyText|blurb|countLabel|status|loadingLabel|statusMessage)\s*:\s*/gm,
|
||||
];
|
||||
// Layer status set by assignment: this._error = …, stats.lastError = …,
|
||||
// error = … (a layer factory keeps its status in `let error`). Not checked:
|
||||
// the declaration itself (`const error = …`, `let status = …`) and a default
|
||||
// value in a parameter list or destructuring (see defaultValue).
|
||||
const ASSIGNMENT_SINK =
|
||||
/(?<![\w$])(?<!\b(?:const|let|var)\s+)_?(?:error|lastError|loadingLabel|statusMessage|status)\s*(?:\?\?|\|\|)?=(?![=>])\s*/g;
|
||||
JS_SINKS.push(ASSIGNMENT_SINK);
|
||||
|
||||
/**
|
||||
* Whether `name = value` at `nameStart` is a default (`(status = 'idle') =>`,
|
||||
* `const { error = '…' } = options`): it follows `(`, `[`, `{` or `,` and its
|
||||
* value ends at `,` or a closing bracket. An assignment statement ends at `;`.
|
||||
*/
|
||||
function defaultValue(source, nameStart, valueStart) {
|
||||
const before = source.slice(0, nameStart).trimEnd().at(-1);
|
||||
if (!before || !'([{,'.includes(before)) return false;
|
||||
const end = valueStart + expressionAt(source, valueStart).length;
|
||||
const after = source.slice(end).trimStart()[0];
|
||||
return Boolean(after) && ',)]}'.includes(after);
|
||||
}
|
||||
const MARKERS = /\b(?:t|tn|msg)\(/y;
|
||||
|
||||
/** The expression starting at `start`, up to its end at bracket depth 0. */
|
||||
@@ -364,6 +381,8 @@ export function unmarkedScript(file, original) {
|
||||
const start = match.index + match[0].length;
|
||||
const line = lineAt(source, start);
|
||||
if (ignored(line)) continue;
|
||||
if (sink === ASSIGNMENT_SINK && defaultValue(source, match.index, start))
|
||||
continue;
|
||||
const html = /HTML/.test(match[0]);
|
||||
for (const literal of valueLiterals(expressionAt(source, start))) {
|
||||
const text = visibleText(literal, html);
|
||||
@@ -377,8 +396,7 @@ export function unmarkedScript(file, original) {
|
||||
/**
|
||||
* UI text that is not marked for translation: markup without data-i18n* and
|
||||
* string literals that reach the page without t(), tn() or msg().
|
||||
* Opt out with translate="no" in markup or an `// i18n-ignore` comment on
|
||||
* (or just above) the line in code.
|
||||
* Opt-outs: see unmarkedMarkup; in code, `// i18n-ignore` on or above the line.
|
||||
* @returns {{where: string, text: string}[]}
|
||||
*/
|
||||
export function findUnmarked() {
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
"src/data/adsbLolFallback.js",
|
||||
"src/data/aisStreamAdapter.js",
|
||||
"src/data/aisWatchdog.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": ["ws"]
|
||||
},
|
||||
@@ -79,6 +80,7 @@
|
||||
"src/localRequestGate.mjs",
|
||||
"src/data/routeSteps.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/rateLimit.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -97,7 +99,8 @@
|
||||
"server/providers/space/launch-library.js",
|
||||
"server/providers/common/http.js",
|
||||
"src/data/spaceProviderRequests.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -127,6 +130,7 @@
|
||||
"server/providers/traffic.js",
|
||||
"server/providers/common/http.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/data/tomtomTiles.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -148,6 +152,7 @@
|
||||
"server/providers/firms.js",
|
||||
"server/providers/common/http.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/data/firmsCsv.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -161,7 +166,8 @@
|
||||
"server/providers/common/rate-limit.js",
|
||||
"src/sources/rateLimit.js",
|
||||
"src/data/gbfsSource.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -192,7 +198,8 @@
|
||||
"scripts/google-server-key.mjs",
|
||||
"src/data/cctvFootprint.js",
|
||||
"src/sources/cctvTypes.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -210,7 +217,8 @@
|
||||
"server/providers/common/rate-limit.js",
|
||||
"src/sources/rateLimit.js",
|
||||
"server/providers/common/http.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": ["pbf"]
|
||||
},
|
||||
@@ -226,7 +234,8 @@
|
||||
"src/data/radioCountry.js",
|
||||
"server/providers/common/http.js",
|
||||
"src/sources/radioBrowser.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -248,6 +257,7 @@
|
||||
"src/data/placeProviderPayloads.js",
|
||||
"src/data/routeSteps.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/overpass.js",
|
||||
"src/sources/rateLimit.js"
|
||||
],
|
||||
@@ -268,6 +278,7 @@
|
||||
"server/providers/overpass/geometry.js",
|
||||
"server/providers/overpass/transport.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/rateLimit.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -290,6 +301,7 @@
|
||||
"src/data/regionalModel.js",
|
||||
"src/nominatimGeocode.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/rateLimit.js",
|
||||
"src/data/local_data/natural_earth/marine.json",
|
||||
"src/data/local_data/natural_earth/regions.json",
|
||||
@@ -303,6 +315,7 @@
|
||||
"runtime": "node",
|
||||
"exports": ["./server/providers/openai"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/hudLayoutPolicy.js",
|
||||
"server/providers/common/rate-limit.js",
|
||||
"server/providers/common/request.js",
|
||||
@@ -335,6 +348,7 @@
|
||||
"runtime": "node",
|
||||
"exports": ["./server/standalone/key-setup"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"scripts/pinokio-environment.mjs",
|
||||
"server/providers/common/source-root.js",
|
||||
"server/standalone/key-setup.js",
|
||||
@@ -367,6 +381,7 @@
|
||||
"src/data/local_data/natural_earth/regions.json",
|
||||
"src/data/local_data/natural_earth/marine.json",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/nominatim.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -435,6 +450,7 @@
|
||||
"map-source-controls": {
|
||||
"exports": ["./ui/maps"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/ui/mapSource.js",
|
||||
"src/ui/mapSourceControls.js",
|
||||
"src/mapStackChips.js",
|
||||
@@ -450,6 +466,7 @@
|
||||
"layer-panel": {
|
||||
"exports": ["./ui/layers", "./ui/layers/feedback"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/ui/layers.js",
|
||||
"src/ui/layerPanel.js",
|
||||
"src/ui/layerNames.js",
|
||||
@@ -528,6 +545,7 @@
|
||||
"context-controls": {
|
||||
"exports": ["./ui/context", "./ui/context/policy"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/ui/context.js",
|
||||
"src/ui/contextActions.js",
|
||||
"src/ui/contextBindings.js",
|
||||
@@ -598,6 +616,7 @@
|
||||
"ui-shell": {
|
||||
"exports": ["./ui/shell"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/ui/placeNames.js",
|
||||
"src/hudLayouts.js",
|
||||
"src/hudLayoutPolicy.js",
|
||||
@@ -794,6 +813,7 @@
|
||||
"src/director/interactions/session.js",
|
||||
"src/director/sharing/bundle.js",
|
||||
"src/director/sharing/preview.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/director/sharing/lifetime.js",
|
||||
"src/director/authoring.js"
|
||||
],
|
||||
@@ -844,6 +864,7 @@
|
||||
"src/data/inputOwnership.js",
|
||||
"src/director/sharing/bundle.js",
|
||||
"src/director/sharing/preview.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/director/sharing/lifetime.js",
|
||||
"src/director/authoring.js",
|
||||
"src/scenes/sharing.js",
|
||||
@@ -860,6 +881,7 @@
|
||||
"live-observations": {
|
||||
"exports": ["./sources/live"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/data/aircraftEmergency.js",
|
||||
"src/sources/live/aircraft.js",
|
||||
"src/sources/live/contract.js",
|
||||
@@ -872,6 +894,7 @@
|
||||
"civil-flights": {
|
||||
"exports": ["./layers/flights"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/cyberSonar.js",
|
||||
"src/data/aircraftClass.js",
|
||||
"src/data/aircraftEmergency.js",
|
||||
@@ -921,6 +944,7 @@
|
||||
"military-flights": {
|
||||
"exports": ["./layers/military"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/cyberSonar.js",
|
||||
"src/data/aircraftClass.js",
|
||||
"src/data/aircraftEmergency.js",
|
||||
@@ -965,6 +989,7 @@
|
||||
"aircraft-mechanics": {
|
||||
"exports": ["./aircraft"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/data/aircraftClass.js",
|
||||
"src/data/aircraftIcons.js",
|
||||
"src/data/aircraftMeta.js",
|
||||
@@ -1032,6 +1057,7 @@
|
||||
"recent-imagery": {
|
||||
"exports": ["./layers/recent-imagery"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/recentImagery/index.js",
|
||||
"src/layers/recentImagery/source.js",
|
||||
"src/layers/recentImagery/model.js",
|
||||
@@ -1159,6 +1185,7 @@
|
||||
"src/data/tomtomTiles.js",
|
||||
"src/sources/capability.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/openFreeMap.js",
|
||||
"src/sources/militaryTileGeometry.js",
|
||||
"src/sources/vectorTiles.js",
|
||||
@@ -1181,6 +1208,7 @@
|
||||
"awareness-layer": {
|
||||
"exports": ["./layers/awareness"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/cyberSonar.js",
|
||||
"src/cockpitMath.js",
|
||||
"src/data/iconOrientation.js",
|
||||
@@ -1244,6 +1272,7 @@
|
||||
"src/sources/overpassRoads.js",
|
||||
"src/sources/capability.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/openFreeMap.js",
|
||||
"src/data/installationNames.js",
|
||||
"src/sources/militaryTileGeometry.js",
|
||||
@@ -1411,6 +1440,7 @@
|
||||
"src/layers/alpr/tileRecords.js",
|
||||
"src/sources/capability.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/vectorTiles.js",
|
||||
"src/sources/phaseTiming.js",
|
||||
"src/layers/alpr/floors.js",
|
||||
@@ -1513,6 +1543,7 @@
|
||||
"./maps/defaults"
|
||||
],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/maps/availability.js",
|
||||
"src/maps/catalog.js",
|
||||
"src/maps/controller.js",
|
||||
@@ -2237,6 +2268,7 @@
|
||||
"src/search/nominatim.js",
|
||||
"src/nominatimGeocode.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/nominatim.js",
|
||||
"src/app/layers/transit.js",
|
||||
"src/data/transit.js",
|
||||
@@ -2863,6 +2895,7 @@
|
||||
"src/nominatimGeocode.js",
|
||||
"src/data/regionalModel.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/nominatim.js",
|
||||
"src/app/layers/transit.js",
|
||||
"src/data/transitFeeds.js",
|
||||
@@ -3012,9 +3045,7 @@
|
||||
"src/layers/airQuality/model.js",
|
||||
"src/layers/airQuality/source.js",
|
||||
"src/ui/statusText.js",
|
||||
"src/ui/layerNames.js",
|
||||
"src/layers/wind/source.js",
|
||||
"src/sources/windBody.js"
|
||||
"src/ui/layerNames.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -3074,6 +3105,7 @@
|
||||
"src/nominatimGeocode.js",
|
||||
"src/data/regionalModel.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/nominatim.js",
|
||||
"src/sources/featureGeometry.js",
|
||||
"src/sources/overpassFeaturesRecords.js",
|
||||
@@ -3114,6 +3146,7 @@
|
||||
"ui-share-restoration": {
|
||||
"exports": ["./ui/share-restoration"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/data/installationFeedback.js",
|
||||
"src/data/idSwitches.js",
|
||||
"src/data/layerState.js",
|
||||
@@ -3189,6 +3222,7 @@
|
||||
"civil-flight-records": {
|
||||
"exports": ["./layers/flights/records"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/flights/records.js",
|
||||
"src/layers/flights/recordPolicy.js",
|
||||
"src/data/aircraftMeta.js",
|
||||
@@ -3209,6 +3243,7 @@
|
||||
"military-flight-records": {
|
||||
"exports": ["./layers/military/records"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/military/records.js",
|
||||
"src/layers/military/recordPolicy.js",
|
||||
"src/data/aircraftMeta.js",
|
||||
@@ -3260,6 +3295,7 @@
|
||||
"directions-layer": {
|
||||
"exports": ["./layers/directions"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/directions/index.js",
|
||||
"src/data/pointerOwnerNames.js",
|
||||
"src/layers/directions/source.js",
|
||||
@@ -3335,6 +3371,7 @@
|
||||
"src/data/tomtomTiles.js",
|
||||
"src/layers/alpr/tileRecords.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/vectorTiles.js",
|
||||
"src/sources/phaseTiming.js"
|
||||
],
|
||||
@@ -3372,6 +3409,7 @@
|
||||
"src/data/tomtomTiles.js",
|
||||
"src/sources/capability.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/openFreeMap.js",
|
||||
"src/sources/militaryTileGeometry.js",
|
||||
"src/sources/vectorTiles.js",
|
||||
@@ -3409,6 +3447,7 @@
|
||||
"src/layers/traffic/source.js",
|
||||
"src/sources/overpassRoads.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/openFreeMap.js",
|
||||
"src/data/installationNames.js",
|
||||
"src/sources/militaryTileGeometry.js",
|
||||
@@ -3426,6 +3465,7 @@
|
||||
"src/layers/earthquakes/source.js",
|
||||
"src/layers/earthquakes/records.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/perimeters/source.js",
|
||||
"src/layers/perimeters/records.js",
|
||||
"src/layers/submarineCables/bundledSource.js",
|
||||
@@ -3444,13 +3484,14 @@
|
||||
"src/search/geospatial.js",
|
||||
"src/data/regionalModel.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/nominatim.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
"source-http-body": {
|
||||
"exports": ["./sources/http-body"],
|
||||
"modules": ["src/sources/httpBody.js"],
|
||||
"modules": ["src/sources/httpBody.js", "src/i18n/marker.js"],
|
||||
"external": []
|
||||
},
|
||||
"overpass-lexer": {
|
||||
@@ -3465,7 +3506,8 @@
|
||||
"src/nominatimGeocode.js",
|
||||
"src/search/geospatial.js",
|
||||
"src/data/regionalModel.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -3481,6 +3523,7 @@
|
||||
"src/data/transitFeeds.js",
|
||||
"src/data/transitProxy.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/transitHistory.js",
|
||||
"src/sources/transitService.js",
|
||||
"src/sources/transitHistoryStore.js",
|
||||
@@ -3620,6 +3663,7 @@
|
||||
"src/data/transitProxy.js",
|
||||
"src/data/gtfsRealtime.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/transitHistory.js",
|
||||
"src/sources/transitService.js",
|
||||
"src/sources/transitHistoryStore.js",
|
||||
@@ -3643,6 +3687,7 @@
|
||||
"modules": [
|
||||
"src/layers/perimeters/index.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/perimeters/source.js",
|
||||
"src/layers/perimeters/records.js",
|
||||
"src/layers/perimeters/cards.js",
|
||||
@@ -3659,6 +3704,7 @@
|
||||
"modules": [
|
||||
"src/layers/gnss/index.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/gnss/source.js",
|
||||
"src/layers/gnss/records.js",
|
||||
"src/i18n/index.js",
|
||||
@@ -3672,7 +3718,11 @@
|
||||
},
|
||||
"gnss-source": {
|
||||
"exports": ["./layers/gnss/source"],
|
||||
"modules": ["src/sources/httpBody.js", "src/layers/gnss/source.js"],
|
||||
"modules": [
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/gnss/source.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
"perimeters-source": {
|
||||
@@ -3680,6 +3730,7 @@
|
||||
"modules": [
|
||||
"src/layers/perimeters/records.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/perimeters/source.js"
|
||||
],
|
||||
"external": []
|
||||
@@ -3713,6 +3764,7 @@
|
||||
"src/layers/wind/inspection.js",
|
||||
"src/layers/wind/model.js",
|
||||
"src/layers/wind/fields.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/tools/queries/imagery.js",
|
||||
"src/tools/queries/cables.js",
|
||||
"src/tools/queries/surveillance.js",
|
||||
@@ -3766,6 +3818,7 @@
|
||||
"src/layers/cctv/sourcePolicy.js",
|
||||
"src/layers/cyclones/source.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/layers/earthquakes/source.js",
|
||||
"src/layers/earthquakes/records.js",
|
||||
"src/layers/firms/source.js",
|
||||
@@ -3812,6 +3865,8 @@
|
||||
"src/layers/weather/source.js",
|
||||
"src/layers/wind/source.js",
|
||||
"src/sources/windBody.js",
|
||||
"src/layers/wind/fields.js",
|
||||
"src/layers/wind/model.js",
|
||||
"src/layers/recentImagery/catalog.js",
|
||||
"src/layers/recentImagery/model.js",
|
||||
"src/layers/submarineCables/bundledSource.js",
|
||||
@@ -3841,6 +3896,7 @@
|
||||
"src/layers/airQuality/model.js",
|
||||
"src/layers/airQuality/source.js",
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/i18n/index.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/i18n/locales/es.json",
|
||||
@@ -3855,7 +3911,8 @@
|
||||
"modules": [
|
||||
"src/layers/airQuality/model.js",
|
||||
"src/layers/airQuality/source.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js"
|
||||
],
|
||||
"external": []
|
||||
}
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
* qa-scene-dialog-file-drop — the SHARE SCENE dialog's file control accepts a
|
||||
* file dropped from the desktop, proved in a real browser.
|
||||
*
|
||||
* The dialog replaces the browser-drawn "Choose files / No file chosen" line
|
||||
* with a translated one (src/ui/sceneSharing.js, fileChooser). The native
|
||||
* input must stay the whole control underneath it: a shrunken 1 px input
|
||||
* accepts drops only on its own pixel, so dragging a data-pack file onto the
|
||||
* visible row selected nothing. This harness drops a file onto the centre of
|
||||
* the visible row through CDP and expects `input.files` to hold it.
|
||||
* The dialog draws a translated row over the native file input
|
||||
* (src/ui/sceneSharing.js, fileChooser). The input must still cover the row,
|
||||
* or drops miss it. Drops a file on the row's centre via CDP and expects
|
||||
* `input.files` to hold it.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/qa-scene-dialog-file-drop.mjs
|
||||
|
||||
@@ -134,8 +134,7 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
|
||||
};
|
||||
// The gate itself is pure and unit-tested (admitKeySetupRequest in
|
||||
// src/keySetupCore.mjs) — this just feeds it the request.
|
||||
// A refusal as the panel reads it: `code` (and `params`) pick its localized
|
||||
// sentence in src/keySetup.js; `error` is the English for logs and scripts.
|
||||
// `code`/`params` pick the panel's sentence (src/keySetup.js); `error` is for logs.
|
||||
const refusal = ({ error, code, params }) => ({ error, code, params });
|
||||
const admit = (req) =>
|
||||
admitKeySetupRequest({
|
||||
|
||||
@@ -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'),
|
||||
|
||||
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