From 41378656c6aa80968b9f10abb88f95b78f5ae615 Mon Sep 17 00:00:00 2001 From: Meir Michanie Date: Thu, 24 Sep 2026 20:03:47 +0200 Subject: [PATCH] docs(i18n): translator's guide for the en-only foundation, current-state and changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/TRANSLATORS.md is rewritten for the foundation state: English shipped, and the locale-addition recipe front and center — the one-PR-per-locale procedure (catalogs, registration, CATALOG_LOCALES, pair behavior, parity gates). CURRENT-STATE.md documents the subsystem (file map, resolution order, selector, gates, deferrals) and the CHANGELOG records the foundation. --- CHANGELOG.md | 28 ++++-- docs/CURRENT-STATE.md | 119 +++++++++++++++++++++++++ docs/TRANSLATORS.md | 198 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 340 insertions(+), 5 deletions(-) create mode 100644 docs/TRANSLATORS.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 58103d04d..1637811cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,29 @@ keeps frame time with ALPR on within a few percent of ALPR off, seats nearby badges on the rendered surface and says when no loaded camera is on screen. +## Internationalization foundation + +- Add the locale core (`src/i18n/`): configurable locale pair + (`GEV_DEFAULT_LOCALE` / `GEV_SECONDARY_LOCALE`, validated against the + shipped catalogs), guarded preference storage, a one-shot `?lang=` + override, and ``/`` metadata. English is the only + shipped locale — the built-in pair degenerates to en-only and the dock + language switch renders a single EN button — and English behavior is + byte-identical to before. +- Extract all application-owned presentation text into four English + catalogs (`shell`/`cockpit`/`layers`/`setup`) consumed through `t()` call + sites, `data-i18n*` template attributes, and the runtime locale selector; + complete the duplicate `data-i18n` split on the first-run EXPLORE + MANUALLY tile (the subcopy was wiped in every locale). +- Enforce the parity gates every additional locale must pass (key / + placeholder / plural-shape parity, markup coverage, repair-pass anchors). + Additional locales ship as stacked follow-up PRs, one per locale, via the + locale-addition recipe in `docs/TRANSLATORS.md` (Spanish is first). + +- Let CLI tools, development launchers and the setup doctor use an explicit project directory while retaining their existing default paths. + +- Split application scene, controls, catalog, tools and HTML into reusable components; configure application request services and sources without changing global fetch. Preserve standalone markup and voice behavior. Explicit annotation navigation may resolve a distant named target. + - CCTV cameras whose bearing is a guess now say so. Packs mark bearings derived from a hash of the camera id as `headingConfidence: 'low'`, but nothing read the flag, so roughly 70% of a default catalog rendered like surveyed facings. The HUD @@ -497,11 +520,6 @@ Analyst records for loaded satellites, datacenters and dams, with explicit bound - Separate navigation, share restoration, visual settings and panel state into focused UI owners with explicit dependencies and terminal cleanup. - Separate layer lifecycle transactions from panel construction and render/detection reactions; retain existing transition and refresh behavior. - -- Let CLI tools, development launchers and the setup doctor use an explicit project directory while retaining their existing default paths. - -- Split application scene, controls, catalog, tools and HTML into reusable components; configure application request services and sources without changing global fetch. Preserve standalone markup and voice behavior. Explicit annotation navigation may resolve a distant named target. - ## Satellite pass prediction - Bisect pass rise/set to ~0.2 s and fit peak elevation with a parabola. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 22ce1eb3b..b9f55990c 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2806,6 +2806,125 @@ test:track` 43 tracking invariants · headless QA harnesses under > attribution 18/18). New modules: `src/data/{motionModel,aircraftMeta,aircraftClass,aircraftIcons,issPass,routePlausible,dataCredits}.js`. > The live runtime now declares 29 voice tools; the 17→20 count above is retained only as milestone history. +### Internationalization / catalog locales (September 2026) + +The application-owned UI is fully extracted into message catalogs; **en** +is the only shipped locale in this foundation state — additional locales +land as stacked follow-up PRs, one per locale (Spanish first), each +following the locale-addition recipe in +[`docs/TRANSLATORS.md`](./TRANSLATORS.md). English is the default, the +source catalog, and the fallback. The subsystem lives in `src/i18n/`: + +- **File map.** `src/i18n/locale.js` owns locale resolution, guarded storage + (`gev:locale:v1`), the one-shot `?lang=` override, and ``/`` metadata. `src/i18n/index.js` owns the catalog registry, `t()` + (interpolation + `Intl.PluralRules` plural selection), `formatNumber` / + `formatDate`, `applyDocumentTranslations()`, and + `persistLocaleAndReload()`. The shipped catalog is four flat message maps + — `src/i18n/locales/en/{shell,cockpit,layers,setup}.js`, 912 keys (shell + 34, cockpit 369, layers 388, setup 121). Plural entries are the en + `{ one, other }` shape; a future locale may add only cardinal categories + `Intl.PluralRules` reports valid for it, and an entry lacking the selected + category degrades to `other` at runtime. Locale-code normalization + (`es-MX`/`es_419` → `es`, plus fr/ru/uk and variants) ships ahead of the + locales on purpose: knowing a code is hygiene, shipping is catalog-driven + through `CATALOG_LOCALES`. +- **Selector.** The command dock carries a compact locale switch: a static + `.dock-locale-switch` group container in `index.html` whose buttons + (`.dock-locale-btn`) are rendered at runtime by `src/ui/applicationShell.js` + `_initLocaleSelector()` — one per locale in the configured pair; while + only English ships that is a single EN button. A click persists the + choice and reloads the page with the hash preserved, so no live re-apply + of already-rendered dynamic panels is needed. +- **Locale pair config.** `GEV_DEFAULT_LOCALE` and `GEV_SECONDARY_LOCALE` in + `.env`, injected as `import.meta.env` defines by `vite.config.js`; both + values are validated against the shipped catalogs (`CATALOG_LOCALES`), + and the offered set is `dedup([default, secondary, 'en'])`. While English + is the only shipped catalog the built-in pair is en-only, so every + configuration degenerates to a single offered locale; an invalid, + unshipped, or degenerate configured pair falls back to that built-in + en-only shape (dev-only warn). The first secondary-locale PR restores the + built-in en+es pair. +- **Resolution order.** `?lang=` (search string only; never + persisted, never written into share links; stripped by + `persistLocaleAndReload`; accepted only for offered locales) → stored + `gev:locale:v1` (re-checked against the pair) → + `navigator.languages` (regional variants normalize to the primary tag: + `es-MX`/`es_419` → `es`) → the CONFIGURED default locale. Unsupported + values defer to the next + step rather than forcing English. +- **Gates** (all under `node --test src/i18n/`, 40 tests): + `catalog.test.mjs` enforces per-locale key, placeholder-name, and + plural-shape parity with the strict exact-parity flip ON + (`REQUIRE_FULL_PARITY`; `GEV_I18N_REQUIRE_FULL_LOCALE_PARITY=0` opts out + for staged work; the loops are vacuous while only en ships — each locale + PR meets them from day one); + `markupCoverage.test.mjs` requires every `data-i18n*` attribute in + `index.html` to resolve in every shipped catalog and rejects unknown attribute + spellings; `i18n.test.mjs` pins precedence, guards, fallback, + interpolation, the plural-selection contract (synthetic missing-category + degradation; real-catalog plural pins arrive with each locale PR), and + DOM application; `repairPass.test.mjs` anchors en byte-identity for + extracted literals and rejects a locale-scoped CCTV wrap rule shipping + ahead of its locale (per-locale value anchors arrive with each locale + PR). +- **Accepted deferrals (do not "fix" silently):** + - The military-awareness subject header literal `FLIGHT / VESSEL WINDOW` + (`src/data/militaryAwareness.js`) stays English; the literal is + test-pinned in `src/data/militaryAwareness.test.mjs`. + - The visible cockpit-brief tab tokens `SIG` / `NEWS` / `LOCAL` + (`index.html`) stay untranslated to match the runtime keys; their + `aria-label`s ARE localized (`cockpit.brief.tab*AriaLabel`). + - `src/voice/gevActions.js` tool-result confirmation strings stay English + by contract (voice tool schemas and spoken confirmations are + keep-English; see the Keep-English boundary in `docs/TRANSLATORS.md`). + - Number formatting is still the pre-i18n `toLocaleString` policy at: + `src/ui.js:1442`, `src/data/satellites.js:829`, + `src/data/flights.js:436` (`'en-US'` pinned), and + `src/data/rocketLaunches.js:2281/2299/2364/2371/2374` + (default-locale). A locale-aware number-format policy is deferred; the + `formatNumber` helper exists in `src/i18n/index.js` when that lands. + +### Overpass proxy mirror rotation (September 2026) + +- `/api/overpass` fans out across four public mirrors. `overpassPayloadIsData()` governs cache reads, writes, and stale fallback: only a 2xx that is neither rate-limited nor a body-level runtime error qualifies. Previously stored refusals are ignored on both fresh and stale reads, so upgrading does not require manually clearing the disk cache. +- HTTP refusals such as 406 now rotate alongside the existing network, rate-limit, and runtime-error cases. A refusal from one mirror no longer prevents reaching healthy alternatives or persists under the seven-day road/month-long boundary cache TTLs. Concurrent identical queries share one mirror sequence; if it fails, both the initiating and joined callers can use the same last-good data. +- A refusal every mirror agrees on is still reported with the first mirror's status and body, so a genuinely malformed query says what upstream said — but only after every mirror has had the chance to answer it. `fetchOverpassPayload` takes injectable endpoints and fetch so the rotation is tested without a live mirror (`src/overpassProxy.test.mjs`). + +### Share-link v2 layer state (August 2026) + +- Generated share links use a deterministic v2 hash. Existing camera, visual, + HUD, detection, post-processing, celestial, scope, and map-stack fields remain, + with compact fields for enabled layers, allowlisted layer options, panel state, + and the active preset's allowlisted shader controls. An absent layer field uses + deterministic defaults; an explicit empty field means no enabled layers. +- The registry seals only after all 16 production layers register, and every + layer has an explicit serialization disposition. Unknown enabled-layer tokens + reject the layer payload; unknown option tokens are ignored. Restoration + settles independently per layer so one failed or unavailable source cannot + block its siblings. +- Stable visible options are limited to aircraft 3D mode, selected civilian and + military flight IDs, Satellite catalog and selection, CCTV coverage/projection/ + auto-hop, and Radio filter/volume. Playback and tuning, live-data health, + calibration, caches, lifecycle state, temporary Context ownership, and derived + effects are deliberately excluded. Radio restore never selects or plays a + station. +- Normal loads restore the last successful explicit UI, voice, or tool choice + from versioned local storage. Any valid camera share wins for the current load + without overwriting recipient preferences. Restore ownership is split by + visibility, option/selection, camera, visual, map, and individual panel lane: + a newer explicit action supersedes only the field it owns. In particular, + navigation cannot turn unrelated layers off, and an option change cannot + +#### Keep-English adjudications (post-merge catch-ups) + +- `src/ui/visualPresets.js` `'ELASTIC'` — detection-allocation STRATEGY IDENTIFIER (persisted + under `gev:detection-allocation`, compared via `dataset.allocation ===`), not display copy. +- `src/ui/layerPanel.js` guidance meta — the `{source} · {statusMessage}` composition uses a + locale-neutral interpunct; the statusMessage itself IS localized via `installationFeedback`. +- `src/ui/mapSourceControls.js` `'...'` switching label and the orbit `'↻'` glyph — + punctuation/glyphs, identical across locales. + ## Canonical Docs Order Use docs in this order when details conflict: diff --git a/docs/TRANSLATORS.md b/docs/TRANSLATORS.md new file mode 100644 index 000000000..bb9fdb70b --- /dev/null +++ b/docs/TRANSLATORS.md @@ -0,0 +1,198 @@ +# Translator's guide (i18n foundation — English shipped) + +This is the working guide for translating God's Eye View. The i18n +foundation ships **English only**: the locale core, the English +catalogs, and the parity gates are in, and every additional locale lands +as its own stacked PR, one locale at a time, following the +[locale-addition recipe](#the-locale-addition-recipe) below. That recipe +is the deliberate design artifact of this phase — a locale is not +"translated into the app", it is *shipped* through the same gate every +time. + +## Which locale ships, and which pair is offered + +One catalog ships: **en** (source of truth and unconditional fallback). +Shipping is catalog-driven (`CATALOG_LOCALES` in `src/i18n/locale.js`): +while only English ships, the built-in locale pair is en-only and the +dock language switch renders a single EN button. Which pair the app +*offers* is configuration, not code: `GEV_DEFAULT_LOCALE` and +`GEV_SECONDARY_LOCALE` in `.env`, injected into the browser via vite +defines. Both values are validated against the shipped catalogs — an +invalid, unshipped, or degenerate pair (e.g. the same locale twice) +degenerates to the built-in en-only shape with a dev-server-only +`console.warn`. English is always resolvable — it is the fallback +catalog — even when not part of the configured pair. + +## How the catalog system works + +All application-owned UI text lives in four flat message catalogs (en +today; one directory per locale once shipped): + +```text +src/i18n/ + locale.js pair config + resolution + storage + / + index.js catalog registry, t(), Intl formatters, DOM apply + locales/en/{shell,cockpit,layers,setup}.js +``` + +| Namespace | Surface | +| --- | --- | +| `shell.*` | Document chrome: title, dock actions, panels, global status chip, sync chips, toasts, locale selector | +| `cockpit.*` | Cockpit HUD readouts, Context panel, CCTV scene summary, Display/panel chrome, visual presets, Radio utility | +| `layers.*` | Data-layer presentation: layer names, feed-state labels, row meta, vessel/flight/satellite/mission/CCTV/Radio/awareness cards and readouts | +| `setup.*` | First-run launcher, key setup (POWER UP), scenes/director status, map-stack chips | + +Each catalog file exports `NAMESPACE` and a default map of +namespace-relative keys; `mergeNamespace()` prefixes them (`cockpit.…`) +and `buildCatalog()` rejects duplicates. English is the default and the +fallback locale: a key missing in another locale renders its English +value (dev-server-only `console.warn`). The en catalogs hold 912 keys +(shell 34, cockpit 369, layers 388, setup 121). + +## Key naming + +- Markup surfaces: `..` — e.g. + `shell.panels.dataLayers`, `cockpit.readout.altitude`. +- Runtime flows/states: `..` — e.g. + `shell.status.loadingLiveData`, `layers.missions.telemetry.km`. +- Names are semantic and stable. Never rename, reorder, or renumber an + existing key: `data-i18n` attributes and runtime `t()` call sites in other + files reference them. + +## The locale-addition recipe + +Every locale PR follows the same steps, in this order. Nothing here is +optional — the gates below fail the build if a step is skipped. + +1. **Catalogs.** Copy `src/i18n/locales/en/` to + `src/i18n/locales//` and translate all four files + (`` is the primary language tag: `es`, not `es-419`). Keep the + en key order, keep key names byte-identical, and translate VALUES + only. A locale may never carry a key en lacks. +2. **Registration.** In `src/i18n/index.js`: add the four import lines + and one `_NAMESPACES` array, then one entry in the `CATALOGS` + map. Registration is append-only; nothing else in the file changes. +3. **Shipping.** Append `''` to `CATALOG_LOCALES` in + `src/i18n/locale.js`. This single constant is what makes the locale + normalizable-to-shipped: pair config, `?lang=`, storage, and the + selector all read from it. Regional variants (`es-MX`, `es_419`) + already normalize — locale-code hygiene is shipped ahead of the + locales on purpose. +4. **Selector labels.** Append + `'locale..ariaLabel': 'Switch to '` to the `shell` + catalog of EVERY shipped locale (en included — the en catalog names + the new button; the new catalog names the English one). + `markupCoverage.test.mjs` requires a label for every shipped code in + every catalog, so this cannot be forgotten. +5. **Plurals.** Plural entries are variant objects carrying a `{count}` + placeholder, selected per active locale through `Intl.PluralRules`. + English entries use `{ one, other }`. Your locale must keep every en + variant and may add ONLY categories `Intl.PluralRules` reports valid + for it (e.g. a Slavic locale adds `few`/`many`) — + `catalog.test.mjs` enforces this shape against the live ICU data. + Pin your locale's real-catalog plural agreement in + `src/i18n/i18n.test.mjs` (one/few/many/other counts where relevant). +6. **Layout.** Prefer concise glossary-approved copy over CSS surgery; + fixed-width `nowrap` panels must be checked at narrow viewports. A + locale-scoped wrap rule (`html[lang=''] .cctv-controls { + flex-wrap: wrap; }` in `src/ui/styles/cctv.css`) is added ONLY when + the locale demonstrably clips, with a comment recording the measured + widths — the English layout stays pixel-identical, and + `repairPass.test.mjs` rejects a wrap rule shipping ahead of its + locale. +7. **Gates.** `node --test src/i18n/` must be green with the strict + parity gate ON: exact key-set equality with en, identical + placeholder names, plural-shape superset. While a new catalog is a + staged untranslated seed, CI may run with + `GEV_I18N_REQUIRE_FULL_LOCALE_PARITY=0` (subset mode) — record every + such flip here, and record the flip back. +8. **Docs.** Add the locale's section + glossary to this file, note it + in `docs/CURRENT-STATE.md` (Internationalization) and `CHANGELOG.md`, + and update the shipped-locales wording in `.env.example`. + +## Append-only rules + +- New keys are appended to the END of the namespace file in EVERY + shipped locale directory in the same change. Never edit another + namespace's files, and never touch the registration arrays in + `src/i18n/index.js` without re-reading this guide first. +- A fifth namespace is a last resort (prefer fitting one of the four); its + registration recipe is the same append-only pattern as a locale (step 2 + of the recipe above), applied to every shipped locale at once. +- Parity is enforced by tests, not goodwill: no locale may carry a key en + lacks, and every shared key must keep identical placeholder names + and a plural-variant set that includes every en variant plus only + categories `Intl.PluralRules` reports for that locale + (`src/i18n/catalog.test.mjs`). + +## Placeholder, plural, and typography rules + +- Interpolation uses named `{camelCase}` placeholders: + `'Flying to {place}…'`. The **same placeholder names must exist in + every locale** — the parity gate fails on a renamed `{name}` because + it would break interpolation at runtime. +- Locale-aware numbers, dates, and relative times go through the + `formatNumber` / `formatDate` / `formatRelativeTime` helpers in + `src/i18n/index.js` — do not hand-format in a catalog call site. +- Style: the English catalog is ALL-CAPS for tactical labels, readouts, + and status text; translations follow the same convention in their + script's norms. The `·` separator used between metadata segments is + part of the surface style and is reproduced in translations. +- Instrument codes stay identical across languages; units (KTS, FT, km, + km²) are never translated. +- Romance languages run ~25% longer than English and Slavic languages + compound that on multi-word tactical labels — plan string budgets and + check fixed-width panels before review (see step 6 above). + +## Keep-English boundary (summary) + +Never catalog these — they are contracts, not copy: + +- Internal layer IDs and registry keys (`military-installations`, + `ais-live-vessels`, stack ids). +- Status enums and machine values (`FEED_STATE_LABELS` keys, lifecycle + states, detection/context modes) — translate only the final label at + the presentation boundary. +- URL/hash contracts: share-link parameters, `?welcome=`, `?lang=`, + storage schemas. +- API fields, voice tool names/schemas, `/api/setup/*` payloads. +- Provider/dataset/product names, callsigns, vessel/satellite/place + names from data, news headlines, license/source attributions. +- Console diagnostics, QA pin scripts, and existing exact-English test + expectations. +- User-authored content (annotations, labels drawn from live data). + +Adjudicated exceptions live in "Internationalization" in +[`docs/CURRENT-STATE.md`](./CURRENT-STATE.md) (accepted deferrals — do not +"fix" them silently). + +## Glossary + +Approved recurring terms per locale. Each locale PR appends its + glossary here (English term · translation · catalog evidence) so later + translators reuse approved copy verbatim. None yet — the first entry + arrives with the first translated locale. + +## Running the i18n test gates + +```sh +node --test src/i18n/ # 40 tests: core + pair config, catalog parity, markup coverage, repair-pass anchors +npm test # full suite (see below for the known environmental caveat) +``` + +- `src/i18n/i18n.test.mjs` — resolution precedence, guarded storage, + fallback, interpolation, plural selection (contract + synthetic + missing-category degradation; real-catalog plural pins arrive with + each locale PR), DOM application. +- `src/i18n/catalog.test.mjs` — key + placeholder + plural-shape parity for + every shipped locale against en, strict `REQUIRE_FULL_PARITY` gate + (vacuous while only en ships; each locale PR meets it from day one). +- `src/i18n/markupCoverage.test.mjs` — every `data-i18n*` attribute in + `index.html` must resolve in every shipped catalog; unknown attribute + spellings fail loudly. +- `src/i18n/repairPass.test.mjs` — byte-identity anchors for English + literals; per-locale value anchors arrive with each locale PR. + +Full-suite caveat: `src/devFreshDotenv.test.mjs`'s external-keys provenance +test fails when a provider key (e.g. `OPENAI_API_KEY`) is exported in the +running shell — an environmental false positive unrelated to i18n.