Merge branch 'main' into fix/multipart-cable-feature-ownership

This commit is contained in:
Sameh Khamis committed 2026-11-02 07:00:05 -08:00
commit c1b12a6aa4
1214 files changed
+106591 -22945

No files matched your search

+21 -3
View File
@@ -200,6 +200,24 @@ its pending results. Source disposal does not silently reinstate a default.
`ui/composition` supplies the default engines to the shell. Applications can
supply a HUD implementation or request policy through control services. The
shared chrome owns the welcome/loading transition; standalone composition adds
Provider Settings. `build/html` expands an allowlist of component markers from
`src/ui/templates`; unknown names cannot read arbitrary filesystem paths. The
standalone document expands to the same markup as before this extraction.
Provider Settings. A page places each Vue view in `src/ui/views` with a host,
`<div class="gev-view" data-gev-view="NAME"></div>`, and writes its own loading
screen (`#loading-screen` with a `.loader-status` line) as static markup so it
paints before any script runs. `createApplication()` renders the views before
anything reads the page; `omitViews` names views the application leaves out,
such as `provider-settings`, and their hosts are removed. Leaving out
`layer-panels` also leaves out the scene sharing dialogs it draws, and leaving
out `scene-chrome` the before/after imagery dividers (the imagery still splits,
with no line or handle to move it), the status chips, the toast, the ORBIT
indicator, the FPS readout, the Scene actions panel, the data-pack attribution
cards, the wind inspection marker, the Space Missions replay vehicle, the
full-globe celestial ring, the screen-space map annotations, the visible
map target list and the Global Context compass;
leaving out `command-dock` the voice control and its pointer reticle;
leaving out `context` the Bhote Koshi reconstruction panel; and leaving out
`intel-hud` the Intel HUD overlay (`#intel-hud`), whose readouts the HUD
implementation publishes to the `gev-hud` store. An unknown
view name is an error. A host that does not initialize Core's locale passes
`localePicker: false` to `createApplicationControls` instead of editing the
markup. Node tests that import application modules register the component
loader with `node --import=<core package>/build/register-vue`.
+31 -35
View File
@@ -79,17 +79,13 @@ moves it. Do not label all ingestion modules platform-independent by filename.
| Retained entry | Owner and current reason |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `src/ui.js` | UI facade; direct callers retain standalone StyleManager defaults. Normal assembly uses `src/ui/composition.js` |
| `src/data/manager.js` | Lifecycle/panel compatibility and existing tests; normal assembly constructs lifecycle and presentation separately |
| `src/data/<layer>.js` | Legacy default instances; new assembly calls layer factories with supplied sources |
| `src/data/localGeojson.js` | Infrastructure compatibility factories; catalog imports services from `src/app/localGeojsonServices.js` directly |
| `src/app/sourceSlot.js` | Re-export for direct callers/tests; portable slot ownership is `src/sources/sourceSlot.js` |
| `src/app/sources.js`, `src/services/application.js` | Existing default/override API; current application construction supplies its own instances |
| `server/providers/local.js` | Existing Node composition barrel and provider helper re-exports |
The two exact standalone-import exceptions are `src/ui.js` to
`src/standalone/catalog.js` and `server/providers/local.js` to
`server/standalone/key-setup.js`. They do not authorize new compatibility
The one exact standalone-import exception is `server/providers/local.js` to
`server/standalone/key-setup.js`. It does not authorize new compatibility
back-edges. Voice actions use plain `src/data/feedState.js`, not the manager
facade. Settings filesystem hardening belongs in
`server/standalone/key-setup-hardening.mjs`; its settings policy is unchanged.
@@ -237,7 +233,7 @@ DOM elements and callbacks; importing it creates no browser state. `destroy()`
removes owned listeners and cancels pending work without changing saved state
or moving focus. Call it before removing or replacing the controls.
`src/ui.js` retains panel layout, persistence, share restoration and application
`src/ui/applicationShell.js` retains panel layout, persistence, share restoration and application
reactions to state changes. Map Source selection and Location draft cleanup are
provided through callbacks. The component imports no globe, data, application
or server modules. Package checks and scoped formatting cover this entry.
@@ -280,7 +276,7 @@ compatible through re-exports.
### Adding a right-rail readout panel
Add the shell in `src/ui/templates/context.html` inside `#right-context-rail`:
Add the shell in `src/ui/views/ContextView.vue` inside `#right-context-rail`:
a `.panel-collapsible` with a stable `id` and matching `data-panel-id`, the
existing `.panel-header`, collapse button and a dedicated body. Register it
with the existing panel chrome/layout owners; do not allocate rail space in
@@ -301,13 +297,13 @@ compactStatus, blocks }` in place. Their full-width native button headers expose
adapter owns accordion state. Generic cards do not select data or enable layers.
`blocks` is an ordered array of loose, optional `{ id, type, ... }` descriptors,
rendered by `railCardBlocks.js`. There are no per-section disclosures:
rendered by `rail/RailCardBlocks.vue`. There are no per-section disclosures:
- `summary`: `text`, a single summary line.
- `list`: `list: { ariaLabel, items }`, reconciled by `syncRowList` in `rowList.js`.
- `list`: `list: { ariaLabel, items }`, keyed by `rail/RailRowList.vue`.
- `lines`: keyed `lines: [{ id, text, muted? }]` for details and status.
- `settings`: `settings: [{ id, label, chips }]`; each labelled row has a 56 px
label column and a wrapping `syncChipGroup` from `chipGroup.js`.
label column and wrapping keyed chips from `rail/RailChips.vue`.
- `legend`: `legend: { colors, labels, units?, zeroIndex?, categorical? }` for a
ramp/scale (including a physical freezing anchor) or separate keyed swatches.
- `actions`: keyed `actions: [{ id, label, params?, href?, onClick?, disabled?,
@@ -376,12 +372,10 @@ facade's asynchronous teardown can yield.
## Ordinary action buttons
`src/ui/actionButton.js` is a native, listener-free view helper used by Scene
presentation and sharing-toolbar owners. It receives translated text and explicit
presentation/accessibility properties. Feature controllers own action dispatch,
state, persistence and cleanup. Static Scenes/CCTV templates use the same CSS
surface and preserve IDs and layout hooks. The helper imports no application,
renderer, storage or services; it belongs to the consuming package closures.
The scene panels view and the CCTV panel draw their action buttons with the shared
`gev-action-button` classes in `src/ui/styles/action-button.css`. Feature
controllers own action dispatch, state, persistence and cleanup, and the views
preserve IDs and layout hooks.
## Display controls
@@ -405,9 +399,9 @@ remain in their callers.
`ui/maps` owns source-chip presentation, selection feedback and its subscription
lifetime. It receives the existing controller and explicit state/action callbacks;
it imports no renderer or application. Source construction and availability policy
remain with the map controller. Rebuilding controls removes their previous chip
listeners, and destruction suppresses late completions without owning or destroying
the supplied controller.
remain with the map controller. The dock renders the chips; rebuilding controls
drops their previous subscription, and destruction suppresses late completions
without owning or destroying the supplied controller.
## Layer panel
@@ -470,8 +464,8 @@ release listeners; pending action feedback is limited to its current owner.
## UI assembly and styles
`ui/shell` assembles controls from supplied existing layer, navigation, terrain,
rendering, HUD and share operations. `src/standalone/ui.js` provides the running
application's instances; `src/ui.js` remains the compatibility entry. The shell
rendering, HUD and share operations. `src/standalone/controls.js` provides the running
application's instances. The shell
imports no standalone bootstrap or concrete live layer implementation.
Panel layout, position/drag, notices, recording and deferred UI work have separate
owners with synchronous cleanup. Existing scene, share and HUD engines retain
@@ -602,6 +596,10 @@ state subscriptions retain their existing event contract.
tracking handoff. It reads Cockpit admission and uses supplied tracking operations.
- `ui/share-restoration` owns the initial restore transaction, layer coordinator,
status notices and gesture/timer cleanup.
- `share/view-codec` is the share-link codec (`decodeShareView`,
`encodeShareView`). It is pure (no DOM, no Cesium), so a host's server can
read a `/share/<code>` link, for example to describe it in link previews.
`share/link-path` reads and builds the `/share/<code>` address itself.
- `ui/visual-settings` owns style preferences, detection overrides, display inputs
and IR cleanup. Its engine services and panel operations are explicit.
- `ui/panel-chrome` owns disclosure, docking, collapse preferences and temporary
@@ -741,13 +739,13 @@ DOM and platform middleware.
### Native form fields
`src/ui/formField.js` constructs native numeric inputs without listeners or
application state. Static selects and number inputs use the same `gev-field`
surface, with explicit compact and inline densities. Feature owners retain
Native numeric inputs are rendered by the views that own them; the CCTV pose
chip renders its inline input in `src/ui/cctv/CctvCalValue.vue`. Static
selects and number inputs use the same `gev-field` surface, with explicit
compact and inline densities. Feature owners retain
option identity/order, bounds, parsing, draft values, input/change timing and
teardown. CCTV effective-pose conversion and tuner/device operations remain
feature responsibilities. A Vue replacement can reuse this contract and CSS;
it replaces DOM construction, rather than adding a second state owner.
feature responsibilities.
The shared stylesheet owns field surfaces and disabled opacity; feature and theme
palettes use `--gev-field-background`, `--gev-field-border-color`,
@@ -759,11 +757,9 @@ field rules separate, so Radio's palette remains an intentional exception.
### Read-only status badges
`src/ui/statusBadge.js` constructs/updates ordinary badges without listeners,
status inference or live-region policy. It consumes translated text and controlled
visibility; appearance variants share CSS rather than mapping machine states to
universal severity. CCTV owns frame/source and calibration rules and their data
attributes, while Weather owns coverage labels. The keyed rail-card owner keeps
node identity, header focus, disclosure and teardown. The shared adapter never
creates another state store or announces all card updates. A Vue view can replace
this adapter and retain these presentation inputs and styles.
Views draw read-only badges with the `gev-status-badge` classes, without
listeners, status inference or live-region policy. Appearance variants share
CSS rather than mapping machine states to universal severity. CCTV owns
frame/source and calibration rules and their data attributes, while Weather
owns coverage labels. The rail card keeps node identity, header focus,
disclosure and teardown, and does not announce every card update.
+608 -129
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,6 +1,6 @@
# Director authoring and file sharing
Open Scenes and select a scene and shot. Existing capture, update, playback and
Open Authoring and select a scene and shot. Existing capture, update, playback and
whole-project export controls remain available. Authored scene content, source
links, assets and contributor credit are preserved.
@@ -42,7 +42,7 @@ field definitions. This is a basic draft editor, not a node graph or path editor
SHARE SCENE exports only the selected scene. Download scene JSON preserves its
camera, timing, layers, anchors, manifests, interactions and attribution, but
contains no asset bytes. The recipient needs the referenced source registrations
and files. Existing EXPORT PRESETS continues to export the entire project.
and files. EXPORT (formerly EXPORT PRESETS) continues to export the entire project.
For a bundle, select the declared pack files or a folder, then Download asset
bundle. Folder paths match a manifest path either directly or after removing the
+30 -7
View File
@@ -25,17 +25,39 @@ 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.)
Vue views call the same `t()` and `tn()`. The page's vue-i18n instance (`src/ui/i18n.js`), installed in every view, holds the active locale, and `t()`, `tn()` and the formatters read it from there, so a view that translates while rendering renders again when the locale changes. Views don't use vue-i18n's own `t`: English is the key, `context` picks an entry and plural forms follow `Intl.PluralRules`, which only Core's functions do.
`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
Vue views translate in the template, so their text renders in the active locale:
```vue
<span>{{ t('Normal') }}</span>
<button :title="t('Pin visual presets')">…</button>
<option value="all">{{ t('All', { context: 'radio tag' }) }}</option>
```
Static page markup that no view renders (the loading screen in `index.html`, a host's own markup) opts in with attributes instead, and `npm run i18n` rejects them in a Vue template:
```html
<span data-i18n>Normal</span>
<button title="Pin visual presets" data-i18n-title>…</button>
<input placeholder="Search any location..." data-i18n-placeholder />
```
`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. Markup is translated once at boot, and switching language reloads the page. A module that holds work a reload would lose (an unsaved edit, an open session) registers it with `registerUnsavedWork(() => busy ? msg('unsaved …') : null)` from `src/i18n/index.js`, and the language picker asks before reloading.
`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. `data-i18n-context="radio tag"` gives the element's text a context, the catalog entry `radio tag|All`, as `t('All', { context: 'radio tag' })` does in code. Markup is translated at boot and again after every language switch.
## Switching language
The language picker switches in place, without a reload. A view that calls `t()` while it renders follows by itself. Code that translates text and hands it on (to a store, a published view object, a layer's state, a DOM node) must word it again when the language changes:
- **Present again.** `onLocaleChange(listener)` from `src/i18n/index.js` calls the listener after each change and returns the function that removes it; call that at teardown. The listener presents the view again from what it keeps, as `layerPanel.js` and `cockpitController.js` do.
- **Keep the English, translate when presenting.** Store `msg()` text or a function that words it, not its translation: the voice control takes `update(part, { text: () => t('MIC') })`, the scene director's status and the SDR receiver's messages keep such functions, and `imagerySplit.js` takes its titles that way.
- **Translated constants in a view** go in `computed`, not in a plain `const` set up once.
A toast already on screen keeps its words.
## Rules
@@ -66,16 +88,16 @@ An empty translation is reported, and so is one left as the English, plural form
`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-*`.
- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`. In a Vue template, static text or a static `title`, `aria-label` or `placeholder` (rather than `t()`), and any `data-i18n*` attribute.
- **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:
| Case | How |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML |
| Markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element |
| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: <reason>` on the line or the line above |
| Case | How |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML |
| Page markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element (not in a Vue template) |
| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: <reason>` on the line or the line above |
Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowercase identifiers are skipped automatically, and so is `src/tools/`, whose text is written for AI clients rather than the page.
@@ -84,6 +106,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/ui/i18n.js` | the page's vue-i18n instance: the active locale and the catalogs |
| `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 |
+2 -3
View File
@@ -53,9 +53,8 @@ Cesium installation shared with its viewer; it must not bundle separate copies.
`gods-eye-view/infrastructure/geojson` exports the lower-level
`createLocalGeoJsonLayer(options, services)` and existing infrastructure overlay
helpers. `gods-eye-view/infrastructure/lod` exports the existing LOD policy helpers.
The standalone app's `src/data/localGeojson.js` keeps its original single-argument
factory and supplies its existing functions. Other consumers should use the package
exports, which do not import standalone application globals.
Consumers use the package exports, which do not import standalone application
globals.
Dataset files and source/license notices remain under `src/data/local_data/`.
Their input counts are 4,351 datacenter features and 704 dam features. Cesium may
+2 -1
View File
@@ -97,7 +97,8 @@ operational sample contained 3,909 observations and two selected labels, but its
live aircraft and traffic rows were empty, so it remains a limited stress case.
Snow, Noir, dense detection, and text-heavy combined layers are the clearest
controlled comparison points for later optimization work.
controlled comparison points for later optimization work. Snow has since been
retired as a visual preset; its row records the measurement as taken.
## Keyed live sources
+1 -1
View File
@@ -1,6 +1,6 @@
# Scene document format
Scenes exports a UTF-8 JSON project. Version 5 adds [data packs](DIRECTOR-DATA-PACKS.md)
Authoring exports a UTF-8 JSON project. Version 5 adds [data packs](DIRECTOR-DATA-PACKS.md)
and is the write format. Versions 1, 2, 3, 4 and unversioned legacy files migrate on
import without converting their existing shots into authored moves. Unknown versions or unsupported fields are rejected with
a field path before the current project, selection or saved bytes change.
+370
View File
@@ -0,0 +1,370 @@
# Share-link format
A share link restores a view: where the camera is, what it follows, the visual
style, data layers and panel layout. There are two forms.
- **Long form** — `#v=2&lat=30.2672&lon=-97.7431&…`. Parsed by
`src/sharelink.js`. The address bar follows the live view in this form, and
links already in the wild keep working; the long form is never removed.
- **Short form** — `<app>/share/<code>`, where `<app>` is wherever the app is
served (an origin's root, or a base path) and `<code>` is encoded and
decoded by `src/share/viewCodec.js`, which is pure (no DOM, no Cesium) and
needs no server. Hosts import it as `@gods-eye-view/core/share/view-codec`
(a server can read a code to describe the view in a link preview). The share button makes this form. The same code opens on
any copy of the app. `src/share/shareLinkPath.js` reads and builds the
address, and `src/share/legacyShareState.js` converts between the long
form's parsed state and the code's state.
The same view in both forms (a real session's link):
| Form | Characters |
| ----- | ----------------- |
| Long | 302 (the hash) |
| Short | 39 (the `<code>`) |
For example, a camera-only place view is a 24-character code (31 with its
created-at time).
Following an aircraft is 42–50: 42 when the link is made from the app's own
follow camera with the fix taken as the link is made, 44 with a fix four
seconds older, and 50 after orbiting to another heading and pitch. A view in
any other style that keeps the style's own look adds only the style (two
bytes): the 42-character followed aircraft in Noir is 44.
## Opening a short link
At start-up the app reads `<code>` from an address whose path ends in
`share/<code>` (a host can pass the code instead, as `initialShareCode` to
`createApplicationControls`). A code that decodes runs the same restore as a
long link; once the app next writes the address, `/share/<code>` is replaced
by the app's own address and the long form, without a new history entry, so
a reload follows the live view. A code that is rejected restores nothing: the
app starts as usual and says the link can't be opened.
A thing view turns on its entity's layer and follows the entity by id, using
the same restore as a long link's followed flight or satellite (and its
expiry windows); a vessel is selected once the vessel feed has it. Opening is
best effort: until the entity is found, and if it is not found in time, the
camera is where the link was made, looking at the entity's last-known fix,
and the app says that the entity isn't reporting and when the fix was taken.
The share button makes a thing view while the camera follows an aircraft,
military aircraft or satellite. A selected vessel is not followed (the camera
stays where the user puts it), so a link made then is a place view; vessel
thing views open, but the app does not make them yet.
## Payload
The code is unpadded base64url (`A–Z a–z 0–9 - _`). The decoded bytes are:
1. one **version byte**, currently `1`;
2. a sequence of **fields**, each a varint tag `field_number << 3 | wire_type`
followed by its value;
3. one **check byte**: the CRC-8 of everything before it.
| Wire type | Value |
| --------- | ----------------------------------------------------------- |
| 0 varint | unsigned LEB128, at most 8 bytes, at most 2^53 − 1 |
| 1 fixed64 | 8 bytes (not used by v1; skipped) |
| 2 LEN | varint length, then that many bytes (text or a sub-message) |
| 5 fixed32 | 4 bytes (not used by v1; skipped) |
Signed values are zigzag varints (0, −1, 1, −2 → 0, 1, 2, 3). Text is printable
ASCII. Fields are written in ascending order; the decoder accepts any order.
A field whose value equals its default is **omitted**. The decoder fills every
omitted field from the same defaults table the encoder reads (for a
style-dependent field, the decoded style's defaults), so an empty
payload (code `AQc`: the version byte and its check byte) is the all-default
view.
### Check byte
The check byte is CRC-8 with polynomial x⁸ + x² + x + 1 (`0x07`), initial
value 0, no reflection and no final XOR (CRC-8/SMBUS; the check value of the
ASCII digits `123456789` is `0xF4`). A decoder that computes a different value
rejects the code (`checksum`) before reading any field, so a mistyped code
never opens a different view. A degree-8 CRC catches every error burst of up
to 8 bits; one base64url character is 6 bits, so **every single-character
typo is rejected** (a test tries every substitution at every position). Two
swapped neighbours, a dropped or an extra character are caught with a
probability of about 255 in 256 (many of them already fail as base64url).
## Field registry (top level)
| # | State key | Type | Default | Encoding |
| --- | ---------------------------- | ----- | ----------- | ----------------------------------------------------------------- |
| 1 | `view.kind` | enum | see below | 0 place, 1 thing |
| 2 | `lat` | coord | 0 | zigzag of degrees × 10⁴ |
| 3 | `lon` | coord | 0 | zigzag of degrees × 10⁴ |
| 4 | `alt` | sint | 800 | whole metres |
| 5 | `heading` | angle | 0 | whole degrees, see angles |
| 6 | `pitch` | sint | −35 | whole degrees |
| 7 | `roll` | angle | 0 | whole degrees |
| 8 | `sharedAtMs` | epoch | none | zigzag seconds since 2026-01-01T00:00:00Z |
| 9 | booleans | flags | 0 | bit set when the value differs from default |
| 10 | `layerState.enabledLayerIds` | LEN | none | layer bitset, see layers |
| 11 | `layerState.options` | LEN | none | packed option triples, see layers |
| 12 | `panelState` | LEN | none | packed panel entries, see panels |
| 13 | thing | LEN | none | sub-message, see thing views |
| 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) |
| 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm |
| 16 | `hudVariant` | enum | style | tactical, operator (retired), minimal, cyber |
| 17 | `bloomIntensity` | uint | style | |
| 18 | `bloomVersion` | uint | 2 | |
| 19 | `sharpenIntensity` | uint | style | |
| 20 | `detectionMode` | enum | style | OFF, SPARSE, BALANCED, DENSE |
| 21 | `detectionDensity` | uint | style | one of 0, 25, 50, 75, 100 |
| 22 | `detectionAllocation` | enum | style | ELASTIC, WEIGHTED |
| 23 | `detectionFadePct` | uint | style | 0–40 |
| 24 | `detectionOutsideOpacityPct` | uint | style | 0–100 |
| 25 | `scopeFeatherPct` | uint | 100 | 0–200 |
| 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus |
| 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs |
Field 1 absent means `thing` when field 13 is present and `place` otherwise,
so v1 never writes it; a later view kind writes its code.
Enum codes are the list position (0, 1, 2…). A name not in the list is written
as LEN text in the same field, so the app can add a style or map before this
table learns its code. A new style's settings are encoded against its own
defaults, while a decoder that does not know the style uses Normal's; adding a
style also extends the pinned style-defaults table.
`hudVariant` code 1 (`operator`) is retired: the app no longer offers that
layout and restores it as `tactical`. The code stays reserved.
Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3
`celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on).
### Style defaults
A default marked **style** is what the view's style applies when it is
selected in a fresh session (`SHARE_VIEW_STYLE_DEFAULTS`): the app's
first-load baseline (`GLOBAL_POST_DEFAULTS` in `src/ui/visualPresets.js`),
with the style's preset on top (`STYLE_PRESET_DEFAULTS`), and the style's
shader parameters at the preset's values, else the shader's own. The encoder
compares against the defaults of the view's style, and the decoder fills
omitted settings from the defaults of the style it decoded (an unknown style
uses Normal's). So a view that keeps its style's look costs its camera and
style alone. (A recipient-side preference the app remembers, such as the
detection allocation, is not a style default: a sender who changed it pays
for it.) In v1:
| Setting | Every style |
| ------------------------- | ------------- |
| bloom, intensity | off, 0 |
| sharpen, intensity | on, 49 |
| HUD visible, variant | on, tactical |
| detection mode, density | DENSE, 75 |
| allocation, fade, outside | ELASTIC, 7, 1 |
| celestial ring | off |
| Style | Shader parameters (field 27 defaults) |
| ------------ | --------------------------------------------------------------- |
| normal | none |
| retro | pixelation 1, distortion 0, instability 0.42 |
| surveillance | gain 0.18, bloom 0.22, scanlineStr 0.96, pixelation 1 |
| thermal | sensitivity 0.85, bloom 0.2, mode 0.33, pixelation 1, palette 0 |
| anime | saturation 1, edgeThick 0.5 |
| noir | contrastAmt 1.2, grainAmt 0.5, vignetteAmt 0.5 |
| snow | retired: decodes as normal; its parameters are dropped |
These are frozen like every other default: the codec reads them from the
app's tables today, and a test pins them, so if a table moves the old values
are frozen in the codec rather than changing what shared links mean.
### Precision
| Value | Stored as | Matches the long form |
| --------------------------- | ------------------------- | --------------------- |
| Latitude, longitude | 10⁻⁴ degree (about 11 m) | `toFixed(4)` |
| Altitude, range | whole metres | `Math.round` |
| Heading, pitch, roll | whole degrees | `Math.round` |
| Intensities and percentages | integers | `Math.round` |
| Style parameters | hundredths after clamping | `sp` (× 100) |
| Created-at and observed-at | whole seconds | `at` |
Angles: heading and roll are normalized into [0, 360) — the long form can
write `roll=360` for a camera at 359.6°, which the compact form stores as 0 —
and written as the nearer signed turn, so 350° costs one byte (−10).
When a state is encoded, out-of-range input is clamped (latitude, pitch) or
wrapped (longitude). A payload that stores a latitude or longitude beyond
±90/±180 cannot come from the encoder and is rejected.
### Layers (fields 10 and 11)
A layer's number is derived from its permanent layer-state token
(`src/data/layerStateTokenReservations.json`): a one-character token is its
index in `0–9a–z`, a two-character token is 36 + first × 36 + second. The
token ledger never reassigns a token, so layer numbers never change and new
layers need no change here.
Field 10 is a bitset: bit _n_ of byte ⌊n/8⌋ (least significant first) is set
when layer _n_ is on. Present-but-empty means "every layer off"; an absent
field leaves the recipient's layers alone (the long form's `l=` versus no `l`).
A thing view's entity layer is always on, so its bit is not written: in a
thing view, present-but-empty means "only the entity's layer", and an absent
field opens with only the entity's layer on (the app writes the field
whenever it has a layer set).
Field 11 repeats `(layer number, option key, value)` for each option that
differs from its frozen absent meaning (`SHARE_VIEW_LAYER_OPTION_ABSENT`). The
option key is `option token index × 2 + kind`: kind 0 means the value is a
zigzag varint (an option whose long-form spelling is a canonical integer),
kind 1 means a varint length and ASCII text (the long-form spelling). Values
are decoded by the layer-state codec's own option decoders.
A followed aircraft, military aircraft or satellite is a thing view in the
compact form, not a tracking option: the encoder moves it out of the layer
options, and the converter moves it back for the long form.
### Panels (field 12)
Each entry is one varint: `panel number × 16 + collapsed × 4 + pinned`, where
collapsed and pinned are 0 (no value), 1 (false) or 2 (true). Only panels that
differ from the frozen default layout are written, plus a collapsed-0 entry
for each panel the state has no opinion about (an older long-form link that
predates the panel); a panel with no entry takes its default. Values of 3 are
reserved: a decoder ignores that entry, as for collapsed 0.
| # | Panel | Long-form token | Default |
| --- | ---------------------- | --------------- | ------------------- |
| 0 | `control-panel` | `c` | collapsed, unpinned |
| 1 | `location-bar` | `l` | collapsed, unpinned |
| 2 | `data-panel` | `d` | collapsed |
| 3 | `cctv-panel` | `v` | collapsed |
| 4 | `recent-imagery-panel` | `i` | expanded |
| 5 | `street-level-panel` | `t` | collapsed |
| 6 | `radio-panel` | `r` | collapsed |
| 7 | `scene-panel` | `s` | collapsed |
| 8 | `global-context-panel` | `g` | collapsed |
| 9 | `pp-toggles` | `p` | collapsed |
| 10 | `param-slider-panel` | `m` | expanded |
### Style parameters (field 27)
Pairs of varints `(token index, value × 100)` for the parameters of the
view's style that differ from its defaults, using the long form's `sp` tokens
(`SHARE_VIEW_STYLE_PARAMS`). The decoded state always lists every parameter
of the style: the stored ones over the style's defaults. A long link without
`sp` converts to the style's defaults.
## View kinds
A **place** view (field 13 absent) opens at its camera, fields 2–7.
A **thing** view (field 13 present) follows an entity. Field 13 is a
sub-message:
| # | Field | Encoding | Default |
| --- | ----------- | -------------------------------------------------------- | ------- |
| 1 | aircraft | entity id: varint (24-bit ICAO address) or LEN text | |
| 2 | military | entity id: varint (24-bit ICAO address) or LEN text | |
| 3 | vessel | entity id: varint (MMSI) or LEN text | |
| 4 | satellite | entity id: varint (NORAD catalog number) or LEN text | |
| 5 | range | uint metres from the entity to the camera | 2000 |
| 6 | heading | angle, the camera's heading while following | 0 |
| 7 | pitch | sint whole degrees, the camera's pitch while following | −35 |
| 8 | fix lat | coord, the entity's last-known latitude | none |
| 9 | fix lon | coord, its last-known longitude | none |
| 10 | fix alt | sint metres | 0 |
| 11 | fix age | sint seconds the fix predates the created-at time | 0 |
| 12 | observed-at | epoch, when the fix was taken; only without a created-at | none |
The entity kind is the field that carries the id (its code in
`SHARE_VIEW_ENTITY_KINDS`), so it costs nothing; exactly one of fields 1–4 is
present (two is malformed). Entity ids are the ones the layers look up: an
aircraft's ICAO 24-bit address (six hex digits, stored as a number; other
forms such as TIS-B `~abc123` as text), a vessel's MMSI and a satellite's
NORAD catalog number (decimal numbers; an MMSI with a leading zero stays
text). Ids are lowercase and match `[0-9a-z~_-]{1,16}`.
Fields 5–7 are the follow offset: the camera looks along (heading, pitch) at
the entity from `range` metres away, heading clockwise from north, in the
entity's local east-north-up frame. The defaults match the app's own follow
camera, which looks north at about −35°, so only its range is written.
Fields 8–12 are the last-known fix, the entity's position when the link was
made. Fields 8 and 9 are both written whenever there is a fix; their presence
marks it. Its time is the created-at time (field 8 at the top level) minus
field 11, or field 12 when the view has no created-at time.
**The camera of a thing view with a fix is derived, not stored**: fields 2–7
are not written, and the camera is where following the fix from the offset
puts it (`followCameraFor` in `src/share/follow.js`): the camera sits
`−r·cos p·sin h`, `−r·cos p·cos h` and `−r·sin p` metres east, north and up
of the fix (heading h, pitch p, range r), on a sphere of radius 6 371 000 m,
and looks back at the fix (its heading and pitch are those of that direction
at the camera, roll 0; at follow distances they equal h and p). A
decoder ignores fields 2–7 when the fix is present. A thing view without a
fix (one converted from a long link, which records neither offset nor fix)
writes its camera in fields 2–7 like a place view.
A decoder that meets a view kind or an entity kind it does not know (an id in
a thing field it skips) opens the view as a place at its camera — the derived
one when there is a fix — and reports `fallback`.
## Decoding rules
`decodeShareView(code)` takes the bare code and never throws. It returns
`{ ok: true, state, unknownFields?, fallback? }` or `{ ok: false, reason }`.
Rejected (the whole link): input over 2048 characters, bad base64url
(padding, foreign characters, impossible length, stray bits), fewer than two
bytes, a check byte that does not match (`checksum`), an unknown version byte,
a truncated value, a varint over 8 bytes or above 2⁵³ − 1, an
overlong varint, a LEN longer than what remains (at any level, including
inside the thing sub-message), a reserved wire type, field number 0, a known
field with the wrong wire type or repeated, two entity ids in one thing, a
duplicate panel/option/style entry, non-ASCII text, and a latitude or
longitude (camera or fix) beyond ±90/±180.
Tolerated and reported in `unknownFields`: unknown fields at any level
(skipped by wire type), unknown enum codes and names (the default is used),
unknown flag bits, layers, panel values and style parameters, and layer
options the layer-state codec rejects or that are filed under a layer other
than their owner. Detection mode and allocation names outside their lists
fall back to the default rather than reaching the long form's lenient
migration. The map stack and HUD variant are open: a name outside their lists
that matches `[A-Za-z0-9_-]{1,32}` is kept as written (the app decides what
it means) and is not reported; only a name outside that grammar is. A
created-at time in the future is dropped, as the long form's `at` is.
## Changing the format
These rules keep every link that was ever shared working, in every version of
the app that can open it.
1. **Field numbers are permanent.** Never renumber, never reuse. A field that
is no longer written stays in `SHARE_VIEW_FIELDS` marked `retired: true`.
2. **A field's type and wire type are permanent.** To change one, add a new
field.
3. **Defaults are frozen meanings.** A default is what an omitted field meant
when links were written. If the app's default moves, leave the table alone:
the encoder will start writing the new value explicitly. Style defaults
are read from the app's style tables; when those move, freeze the old
values in the codec (see Style defaults). The same applies
to `SHARE_VIEW_LAYER_OPTION_ABSENT` and the panel defaults.
4. **Codes are append-only.** Enum lists, entity kinds, flag bits, panel
numbers and style parameter tokens only grow. A style the app no longer
offers keeps its code: `snow` (6) is retired, so a link that names it
opens as Normal and the parameters written for it are dropped.
5. **Add, don't change.** A new setting takes the next free field number
(28 onwards; fields 1–15 have one-byte tags and are all taken). A new
boolean takes the next flag bit instead, which costs nothing when it is at
its default.
6. **New view kinds and entity kinds** get the next code. A new entity kind's
id takes the next free thing field (13 onwards), so older apps skip it and
fall back to the camera. A thing that needs more than an id (for example a
CCTV camera, a radio station or a transit vehicle) adds further thing
fields the same way.
7. **A new layer option** needs an entry in `SHARE_VIEW_LAYER_OPTION_ABSENT`
(a test fails until it has one).
8. **Bump the version byte only** for a change older decoders must refuse.
Decoders reject any version they do not know. The trailing CRC-8 check
byte belongs to every version.
9. **Derivations are frozen too.** `followCameraFor` and the follow-offset
defaults are part of the format: changing them moves the camera of every
thing link already shared.
Add a test for every change: a round trip, and a payload written with the new
field that the previous decoder still reads.
+2 -1
View File
@@ -92,7 +92,8 @@ pitch), data layers, visual style, map imagery, and an aircraft, military
aircraft or satellite to follow. `createView` builds and bounds one,
`viewToParams` and `viewFromParams` write and read it in the share-link
format the app restores, and `viewUrl` gives the address that opens it. The
style names are the ones share links use. Ships cannot be followed from a
style names are the ones share links use; a retired style (`snow`) is read
as `normal`. Ships cannot be followed from a
link yet. An aircraft's `follow` may ask for its cockpit view
(`cockpit: true`); links cannot carry that and open the app following it.
+4 -4
View File
@@ -1,6 +1,6 @@
# Translating God's Eye View
Shipped languages: **English** (source and fallback), **Spanish** (`es`) and **Russian** (`ru`).
Shipped languages: **English** (source and fallback), **Arabic** (`ar`), **German** (`de`), **Spanish** (`es`), **French** (`fr`), **Italian** (`it`), **Japanese** (`ja`), **Brazilian Portuguese** (`pt`), **Russian** (`ru`), **Ukrainian** (`uk`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`).
## How it works
@@ -22,7 +22,7 @@ Every other language is one flat JSON file, `src/i18n/locales/<code>.json`, keye
A `tn()` entry is keyed by the English plural form and holds one string per `Intl.PluralRules` category of that language. A missing entry falls back to English.
The page picks its language from `?lang=`, then the choice stored by the language picker (the globe button in the bottom-right corner), then the browser languages, then English. `?lang=` is not stored, and copied share links leave it out. Switching reloads the page.
The page picks its language from `?lang=`, then the choice stored by the language picker (the globe button in the bottom-right corner), then the browser languages, then English. `?lang=` is not stored, and copied share links leave it out. Switching applies at once, without a reload.
## The check
@@ -44,7 +44,7 @@ Edit `src/i18n/locales/<code>.json` and run `npm run i18n`. To see your change,
1. Create `src/i18n/locales/<code>.json`, fill it from `npm run i18n -- --missing <code>` (start from `{}`), and translate every entry.
2. Import it in `src/i18n/index.js` and add it to `CATALOGS`.
3. Add the code to `LOCALES` and its own-language name to `LOCALE_NAMES` in `src/i18n/locale.js`. Right-to-left languages also go in `RTL_LOCALES`.
3. Add the code to `LOCALES` and its own-language name to `LOCALE_NAMES` in `src/i18n/locale.js`. Right-to-left languages also go in `RTL_LOCALES`; the chrome mirrors through logical CSS properties (`inset-inline-*`, `margin-inline-*`), and `src/ui/styles/rtl.css` holds what those cannot express (see the Arabic notes). If the language should keep Western digits, map it in `INTL_TAGS` in `locale.js`.
4. Add `src/i18n/locales/<code>.json` next to `ru.json` in `scripts/package-boundaries.json`.
5. Write `docs/translations/<code>.md`: glossary, plural forms, units and style (copy the structure of `ru.md`).
6. Run `npm run i18n` and `npm test`, then check the panels in the app at narrow widths.
@@ -63,4 +63,4 @@ These are contracts, not copy: layer ids and registry keys, status enums and oth
## Language notes
Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/<code>.md`: [Spanish](./translations/es.md), [Russian](./translations/ru.md). Read it before translating, and add a term there when you settle on one.
Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/<code>.md`: [Arabic](./translations/ar.md), [German](./translations/de.md), [Spanish](./translations/es.md), [French](./translations/fr.md), [Italian](./translations/it.md), [Japanese](./translations/ja.md), [Brazilian Portuguese](./translations/pt.md), [Russian](./translations/ru.md), [Ukrainian](./translations/uk.md), [Simplified Chinese](./translations/zh-CN.md), [Traditional Chinese](./translations/zh-Hant.md). Read it before translating, and add a term there when you settle on one.
+6
View File
@@ -11,12 +11,18 @@ scene playback and voice. It delegates; new behavior belongs with its owner.
| `cockpitCoordinator.js` | Cockpit entry/rollback, readouts and the single Display portal |
| `visualSettings.js` | Effects, detection overrides, visual restoration and render-loop lifetime |
| `panelChrome.js` | Disclosure, docking, layout and Cockpit panel restoration |
| `panelNavigation.js` | Desktop panel rails: each side's offered panels and the open one |
| `aircraftDisplay.js` | Shared commercial/military 3D display preferences and controls |
| `layerBindings.js` | Data-manager attachment, selected-tracker adoption and camera-entry listeners |
| `displayBindings.js` | Keyboard/display subscriptions and frame-rate monitor lifetime |
| `shareRestoration.js` | Initial share/layer restoration and cancellation |
| `shellFeedback.js` | Loading feedback, notices and toast lifetime |
Owners that present UI do so through Vue: they publish plain view objects to a
Pinia store in `src/ui/stores/`, views in `src/ui/views/` render them, and user
actions return through the handlers the owner connects to that store. See
[Building UI](../CONTRIBUTING.md#building-ui).
Owners receive named services, elements, operations and readers. They do not
receive the application shell itself. Readers resolve a replaceable collaborator
at use time; operations cross an ownership boundary without copying its state.
+368
View File
@@ -0,0 +1,368 @@
# Vue migration
The application UI moves from static templates plus imperative DOM wiring to
Vue 3 components with Pinia stores. The migration keeps the experience the
same: element ids, classes, the global stylesheet, translations, keyboard
shortcuts, voice actions, share links and the phone layout all behave as
before. The one intended change is the panel rail described below.
## Goals
- Views are Vue single-file components; state that several views read lives in
Pinia stores rather than in DOM classes.
- The owners listed in [UI-OWNERSHIP.md](UI-OWNERSHIP.md) keep their
responsibilities. A component replaces DOM construction; it does not add a
second owner of the same state.
- Public methods used by layers, scene playback, voice and host applications
(`setPanelCollapsed`, `setContextMode`, `setStyle`, share and location
subscriptions) keep their signatures.
- Ids and classes used by browser QA and host applications stay stable until a
host can use an explicit extension point instead.
## The panel rail
Each desktop sidebar gets a floating icon rail that lists every panel available
on that side. Selecting an icon opens that panel; selecting it again closes it.
One panel is open per side at a time, so the rail replaces the accordion
stacking. Panels that are not available in the current mode (for example
Weather before a weather layer is active) are shown disabled or omitted as the
panel's own availability rule dictates. The rail hides with the rest of the
chrome in Cockpit, scene playback, recording and the clean view.
Phones keep the existing bottom tab bar and sheets; the rail is desktop only.
Voice and keyboard openings go through the same panel store, so they follow the
one-panel rule too. Share links continue to record which panels are open.
## Component kit
`src/ui/kit/` holds the generic components: `UiRail`, `UiTooltip`, `UiIcon`,
`UiButton`, `UiPanel`, `UiSegmented`, `UiSlider`, `UiChip`, `UiSelect`,
`UiBadge` and `UiEmpty` so far, with more joining as the controls they serve
are converted. One component
serves every use of a control, with props for the differences (`UiPanel`'s
`sideways`, `glow`, `scroll`), rather than a copy per panel. The components
are styled by the existing stylesheet and custom properties; a kit class such as
`ui-button--icon` carries no Core style yet, and a converted control keeps
its own classes beside it. They contain no application state.
`UiPanel` takes a `title`, slots and `v-model:collapsed`, but its
`collapsed` drives only the button and `ui-panel--collapsed`: the root's
`collapsed` class, which hides the body, is drawn at once by `v-panel-state`
from the panel chrome store, and there is no `closable`, since Core's panels collapse rather than close.
`UiSegmented` adds `keyboard` (`none`, `arrows`, or `radio` with
Home and End), since each of Core's groups keeps its own keyboard policy, and
`itemClass`/`activeClass` for Core's segment classes. A key moves focus at
once and asks for a change only when it lands on another option. Every
segmented group in Core is a `UiSegmented`; the old `segmentedControl.js`
adapter is gone.
`UiButton` adds `pressedClass`, Core's class on a pressed toggle (most use
`active`); its pressed classes toggle through `classList`, so a class other
code adds (a voice highlight) survives a press. Every pressed toggle in the
views is a `UiButton` except Bhote Koshi's cinematic button and the
tilt-map button, whose owners still write their state into the DOM. Core's
pressable chips and pills (rail card chips, map sources, cities and places)
are `UiButton`s too; `UiChip` is a non-interactive label chip, used by
the voice card.
`UiSelect` renders the bare native select (Core's labels sit outside it or
come as an `aria-label`) with `v-model`, `options` and `disabled`;
options that carry their own attributes come through the slot, a caller's
`@change` still reaches the select, and `hold` works as in `UiSlider`.
Without `hold`, a select takes its model back after its own change renders,
so a pick its owner refuses goes back; an owner that answers later (the SDR
gain) takes `hold`. Every select in the views is a `UiSelect`.
`UiBadge` takes a `tone` and carries Core's `gev-status-badge`; its
density variants and colours stay caller classes, and it renders the CCTV
source and calibration badges and the rail card badges. `UiEmpty` takes
a `title` and `hint`, but Core's empty states are one line, so the title
is the text itself rather than a bold heading; the Scene shot list, mission
roster and imagery strip use it. The locale picker's and the mission tables'
empty rows stay a paragraph and table cells.
`UiSlider` keeps the native range input rather than drawing a
`role="slider"` track, so Core's keyboard, form behaviour and styling stay;
it takes `v-model`, `min`, `max`, `step` and `disabled`, emits a
number, and adds `hold` (never rewrite a focused handle, and take the model
back on blur) and `unstyled` (the browser's own range, as Bhote Koshi's
are, without Core's slider surface). A caller's own input, change and blur
handlers still reach the input, and a null model leaves it as it is. It is
a functional render-function component, so the portable `./ui/input` rows
use it without a component compiler. Every range in the views is a
`UiSlider` except the radio tuner's: its owner moves the handle back to the
selected station on a drag's first input without changing the slot, which
a functional component would not render again.
Controls share one skin, so every panel reads the same. Every scrolling
surface carries the kit's `.ui-scroll`, which draws the thin
themed scrollbar from `--scroll-thumb` and `--scroll-track`; the Cyber theme
sets those two tokens, and Cockpit's HUD windows keep their accent tint. A
surface without it would draw the platform's light scrollbar, because the
page declares no dark `color-scheme`. Selects and number fields are
`.gev-field`, which draws its own chevron in place of the platform's arrow
and keeps native spinners dark; ranges are `.gev-quantitative-slider`, with
a feature supplying only its accent.
## Phases
1. **Foundation.** Vue, Pinia and the Vite Vue plugin join the browser build
(`build/vite.js`, so host applications receive them too). One Vue runtime is
created by the shell (`src/ui/vueRuntime.js`) with one Pinia instance that
every view shares; disposing the shell unmounts them. Stores are added
when a view needs one, mirroring state an owner already publishes, so
there is never a second owner of the same state.
2. **Rail.** `PanelChrome` enforces one panel per side; the panel store
mirrors each side's panels and the rail renders from it. The rail panels
are `RailPanel`s: the kit's `UiPanel` drawing Core's panel markup, bound to the `gev-panel-chrome`
store, with their titles rendered by `t()` rather than `data-i18n`. Each panel's
collapse button, the dock tray toggles and the
dock pins render the open and pinned state PanelChrome presents to the
`gev-panel-chrome` store, and their clicks call it there. Each panel's
root draws its own state from there too (`v-panel-state`: `collapsed`,
`dock-pinned`, `dock-pinned-top`, `layout-auto-collapsed`,
`accordion-collapsed`): PanelChrome, the rails and the accordion present
it rather than writing classes. The directive applies it in a synchronous
watcher, not on Vue's next render, because the rails measure a panel in
the same task that opens or closes it. PanelChrome and its preference
restore, and the phone shell, read a panel's state back from the store
(`_shownPanel`; the markup's classes before its first presentation), so
once a panel is presented a class written outside the store no longer
counts. The rails, the accordion and the hover disclosure
(`panelDisclosure.js`), which act within the same task as a layout, read
the classes the directive has just drawn. Done.
3. **Panels.** Each template and its wiring become a component, keeping ids,
classes and CSS; text a template renders with `t()` drops its `data-i18n`
marker. The page places each view with a
`.gev-view` host (`display: contents`), and `mountApplicationViews()`
(`src/ui/views/index.js`) renders the view into it before the page is
translated and wired. `createApplication()` calls it too, removing the
hosts of any views its `omitViews` names. Views belong to the page and are
unmounted only when a later call omits them. The loading screen is static markup in the page so it
paints before any script runs. Markup
tests read `renderApplicationHtml()` from `src/testSupport`, which renders
the views on the server. Templates compile with Vue's `preserve`
whitespace, which keeps the whitespace between elements as a single
space, so a view renders as its template did; no view markup sits under a
`white-space: pre` rule. Application views (`src/ui/views/*View.vue`) are
kept out of Prettier; other components are formatted with strict
whitespace sensitivity, so their source spacing is what renders. The
markup has moved, unchanged:
every template except the loading screen is a view.
Next each view takes over its wiring: markup an owner builds with
`createElement` becomes component markup that renders from a Pinia store
the owner publishes to, and user actions come back to the owner through
the store, so the owner keeps every decision. Component text uses
`t()`, which follows the locale as the view renders; the views carry no
`data-i18n*` markers, which `npm run i18n` rejects in a template. Shared builders become components first and
keep their imperative entry points until their owners are components:
the rail cards, card blocks and timeline (`src/ui/rail/`) render through
Vue's synchronous `render()` behind `createRailCards()`,
`createRailCardBlocks()` and `createRailTimeline()`. Order: Scenes (done: `SceneOptions` and
`SceneShotList` over the `gev-scenes` store; the panel's buttons, status
line, progress bar and the runtime chip render the panel SceneControls
presents there, and the buttons and file chooser call its actions), Weather (done: the owner
renders `WeatherReadout` from the view it computes), Recent imagery
(done: `RecentImageryReadout` and its day cards, the same way),
Street level (done: `StreetLevelPanel` renders the `gev-street-level`
store that `StreetLevelControls` publishes to; the controls keep their
listeners, EXPAND, focus and resizing), CCTV (done: `CctvPanel` renders
the whole panel from the `gev-cctv` store, including the camera picker,
the inline pose editor, the frame preview, source badge and typed
summary; `CctvControls` keeps its listeners, the frame requests and the
live video surface), Layers (done: `LayerPanel` presents each row and
renders `LayerRows` synchronously; keyed chips and list items keep focus
through the refresh their own click triggers, and clicks re-read the live
descriptor before acting), Display (in progress: `DisplayPanel` renders
the HUD, Sonar, Scope, Celestial, detection, bloom, sharpen and the
active style's parameter sliders from the `gev-display` store, which
VisualSettings publishes to and answers the sliders through, and the 3D
aircraft group, the Draw controls and the imported file list, which
AircraftDisplay, the draw tool and the file import publish beside it;
the controls call DisplayBindings, the draw tool and the file import
through the store's `connect`/`act`, the newest owner of an action
answering it; `GeoImport` hands keyboard focus on before a focused row
goes, with `v-focus-safe-children`; the
style parameter rows render through Vue behind `createStyleParameters()`,
as a render function so the portable `./ui/input` entry needs no
component compiler; the other groups follow one owner at a time),
Context and Radio (in progress: `RadioPanel` renders the directory row,
station card, transport, volume and playback line from the `gev-radio`
store that RadioControls publishes to and whose controls it answers
through the store's `connect`/`act` (the shared `createOwners()`; a
refused station tag gets the kept tag back from `act`), and `SdrCard` the
local RTL-SDR card that LocalSdrControls publishes beside it and answers
the same way; fields a person edits take their value through
`v-field-value`, which never writes a focused field;
`RadioTuner` renders the directory tuner, where RadioControls keeps the
drag and its slot, takes the slider's pointer, key and value events
through the store and publishes the band, readouts, slider and needle,
and the component lays out the tick tape against the dial's measured
width and lays out the snapped needle before it drops the drag class;
`ContextView` renders the mode switch and the nearby sites
search from the `gev-context` store, which ContextControls reaches only
through its `publish` option, so `./ui/context` needs no Vue;
`AwarenessPanel` renders the Contacts panel that the awareness layer
publishes through `subscribePanel()`, the first layer to hand its rail
content to a view, and `MissionRoster` and `MissionDetail` the Space
Missions roster and selected mission that the launches layer publishes
the same way), the command dock (in progress: the style buttons, the
dock's style mini status and the ACTIVE STYLE corner render the style
VisualSettings publishes to the `gev-display` store; the buttons keep
their owner's listeners; the map source chips and status badge render
what `createMapSourceControls()` publishes beside it, so `./ui/maps`
needs no Vue; the location tray's city and landmark pills, mini status
and search error render what `LocationControls` publishes to the
`gev-location` store, and the search field keeps its listeners; beside
the voice control, the dock teleports each voice pointer reticle to the
body where its `PointerReticle` presents it in the `gev-pointer-reticle`
store), the phone shell (`PhoneTabBar` renders the tabs and open sheet the shell
publishes to the `gev-display` store, teleporting the tab bar and sheet
backdrop to the end of the page from the first publish until the shell
is destroyed; the shell takes their clicks from the document), the
imagery dividers (`ImagerySplitView`, in the scene chrome, teleports each
before/after divider that `createImagerySplit` publishes to the
`gev-imagery-split` store to the body; the split keeps the
drag, keyboard, CSS position and scene split), the status chips
(the top-center loading status and the road network and camera grid
chips render what ShellFeedback and CctvControls publish to the
`gev-status` store; `SplitFlapText` plays the departure-board flip that
`splitFlap.js` plans, writing only the label's one text node; the toast
renders what each owner's `createToast()` publishes there, on that
owner's own timer, and the title bar's FPS readout what
`createFrameRateMonitor()` publishes), the globe actions
(`GlobeActions` renders the top-center bar; its clear, share, tilt,
north-up and reset buttons, and the cockpit's reset, call the shell
through the `gev-globe-actions` store, which also carries a clear or a
reset under way and the camera's tilt and heading
that `bindCameraOrientationControls` publishes each drawn frame it
changes, which only `GlobeCompass` renders; an owner publishes only
while it answers the button, so a shell replaced by a newer one leaves
its state alone), the recording safe frame (the scene chrome renders
the frame `RecordingControls` publishes to the `gev-recording` store),
the Scene actions panel (`SceneInteractions`
teleports what the scene interactions owner publishes to the
`gev-scene-interactions` store into the viewer's container; the owner
keeps the pick handler and focuses the picked action), the data-pack
attribution cards (`DataPacks` teleports what the data-pack presentations
publish to the `gev-data-packs` store into the viewer's container and
stops a removed card's player), the wind inspection marker
(`WindInspectionMarker` teleports the marker into the wind layer's
container where `createWindInspectionMarker()`, now in
`src/layers/wind/inspectionMarker.js` so the portable `./tools` entry
needs no Vue, places it after each rendered frame; an unchanged frame
publishes nothing), the Space Missions replay vehicle
(`MissionReplayOverlay` teleports into the globe container what the
launches layer presents through `subscribeReplayOverlay()`, which the
application shell publishes to the `gev-mission-replay` store, so the
portable `./layers/launches` entry needs no Vue), the Bhote Koshi
reconstruction panel (`BhoteKoshiPanel`, last in the context rail,
renders what the event layer publishes to the `gev-bhote-koshi-panel`
store: each control's text, hidden, disabled, value and attributes as
the layer writes them, unchanged frames sending nothing; the rail takes
`bhote-event-active` while it shows), the full-globe celestial ring
(`CelestialRing` teleports into the globe container what `CelestialRing`
in `src/celestialRing.js` publishes to the `gev-celestial-ring` store and
hands its two canvases back for the owner to paint; the application shell
passes the store in, so the keyhole helpers other packages import need no
Vue), the screen-space map annotations (`ScreenAnnotations` teleports to
the body the board the screen annotation renderer publishes to the
`gev-screen-annotations` store after each rendered frame: each mark's SVG
parts as attribute records, the fading marks until they leave; the view
hands back the callout text and mark groups the renderer measures, and
the tracked-entity fade is measured once the frame is in the document;
the styles it injected now live in `src/ui/styles/annotations.css`), the
visible map target list (the scene chrome renders the buttons and status
the world overlay presents through `subscribeWorldOverlayActions()`, which
the application shell publishes to the `gev-world-overlay-actions` store
and answers with `activateWorldOverlayAction()`; the module needs no Vue
for the portable transit entry, and its card and detection canvases stay
painting surfaces it draws on, like the Cesium canvas), the Global
Context compass and contact bearings (`AwarenessDirection` teleports into
the globe container what the awareness layer presents through
`subscribeDirectionOverlay()`, which the application shell publishes to
the `gev-awareness-direction` store, so the portable `./layers/awareness`
entry needs no Vue), the Intel HUD (the `intel-hud` view renders
`#intel-hud`, its corners, edges and bottom bar from the `gev-hud`
store, which IntelHUD publishes to through its `publish` option: the
readouts, the typing summary, the REC blink, visibility, layout variant
and style colors; the vessel layer presents the selected vessel's
readout through `src/hudReadouts.js` instead of writing into the HUD),
the first-run mission launcher (the welcome view renders the card's
visibility, busy state, status line, copy and scroll fade from the
`gev-first-run` store that `initFirstRunExperience` publishes to, and its
choices, DISMISS and "Don't show this again" call the launcher's
controls; a refused save hands the tick back to the view to revert. The
launcher still reads the rendered card for its hit test, keyboard trap,
focus and overflow measurement), then
Cockpit (in progress: `CockpitBriefing` renders the briefing card's
pages, cycle toggle, regional news and local conditions, and
`CockpitContext` the Contact card's scope, navigation, counts, nearest
contact and weather toggle, `CockpitView` the instruments, destination
arrow and vision selector, and `CockpitRoute` the flight plan card, from
the `gev-cockpit` store that the Cockpit controller publishes to,
starting from `cockpitBriefPages.js`, `cockpitContextState.js` and
`cockpitInstrumentState.js`, which both read; the odometer readouts
(`CockpitRollingValue`) keep their throttle and roll in
`cockpitRollingValue.js`. `CockpitSignalList` renders the live signal
rows; `v-focus-safe-children` turns Vue's move of the focused row into
moves of the rows around it, and hands focus to the shown briefing tab
before the focused row is removed, so a keyed list reorders several times
a second without dropping keyboard focus. The instruments publish on
every HUD update (about 10 Hz); in a dev build their Vue patch costs
about 0.5 ms, close to the DOM writes it replaced. The controller also
publishes which surfaces are shown: the entry button and TR-3B chip in
the Context panel, the HUD with its exit and reset buttons and briefing
card, and the Contact card. Focus moves to the exit button, and back to
the entry, only after Vue renders them. The cards' collapse is part of
that presence too: each card's `data-collapsed` and its toggle's words
and icon render from it. The controls keep the controller's listeners,
and the cards' layout stays with the controller, which reads their
visibility when it lays them out. Its layout pass publishes the utility
strip's measured fit (the HUD's anchor and height limits, whether only the
expanded control fits and which control is hidden from assistive
technology), which `CockpitView` and `CockpitUtilityControls` render; the
briefing and Contact cards' positions stay imperative, because the same
pass measures them straight after writing them.
`CockpitUtilityControls` renders the Display
and Radio launchers, which popover is open and the compact Radio
controls from the `gev-radio` store that RadioControls publishes to and
answers through. The Cockpit display portal publishes the Display
popover's slots to the `gev-display` store while Cockpit is on, and
`DisplayPanel` teleports its HUD, Detection, Parameters and 3D groups into
them and back; the portal keeps each surface's scroll position and puts
back focus the move dropped).
Layers that write into the right rail directly gain a slot instead.
4. **Cleanup.** The build-time template expansion is retired: `index.html`
lists the view hosts and the loading screen itself. Every owner that
showed UI state through hand-written DOM writes now publishes to a store
that a view renders, apart from the imperative parts this document
records surface by surface; [Building UI](../CONTRIBUTING.md#building-ui)
now requires the pattern for new UI. The page-level markup tests read
`renderApplicationHtml()`, the real views rendered on the server, and new
tests mount components; three tests still read a `.vue` file's source
(`cyberTheme`, `panelStorageDocs`, `geoFileImport`). The views translate with `t()`
while rendering, and vue-i18n holds the locale they read; the attribute
translation pass is left for static page markup (the loading screen and a
host's own markup). An explicit API for hosts to register panels and rail items is
deferred until a host adds one: no host does today (the hosted
application's account interface is its own markup), and the first such
panel, the hosted timeline, will shape it.
## Verification
- Unit tests: stores and components run under `npm test`. Single-file
components are compiled by a test loader; component tests render with
`@vue/test-utils` and `happy-dom`. `npm test` registers the loader; to run
one file that reaches a component (including anything importing the
shell), use `node --import=./build/registerVue.js --test <file>`.
- Never pass happy-dom nodes to `assert.equal` or `deepEqual`: a failure
prints both operands, and a node reaches the whole page and window, which
runs the process out of memory. Compare nodes with `assertSameNode()`
(`src/testSupport/domAssert.mjs`).
- Panels lifted out of a view keep its verbatim markup in
`src/ui/views/panels/`, which Prettier leaves alone like the views.
- Visual parity: before/after screenshots of every panel at desktop and phone
widths, in the default and Cyber themes, with the rail as the only expected
difference.
- Browser QA: the selector-based `scripts/qa-*.mjs` suites keep passing.
- `npm run format:check`, `npm run check:boundaries`, `npm run i18n` and
`npm run build`.
+15 -9
View File
@@ -3,10 +3,11 @@
God's Eye View uses explicit auth modes for `/api/flights`:
- `OPENSKY_AUTH_MODE=oauth` (default, recommended)
- `OPENSKY_AUTH_MODE=auto` (OAuth first, then Basic fallback)
- `OPENSKY_AUTH_MODE=basic`
- `OPENSKY_AUTH_MODE=auto` (OAuth when configured, otherwise anonymous)
- `OPENSKY_AUTH_MODE=anon`
`OPENSKY_AUTH_MODE=basic` is retired (see below).
Reference: OpenSky REST API docs recommend OAuth2 Client Credentials flow.
## Quick Start (OAuth Client JSON)
@@ -41,13 +42,18 @@ Launchers resolve OAuth creds in this order:
2. `OPENSKY_CREDENTIALS_FILE`
3. Keychain (`opensky-network` / `client_id`, `client_secret`)
## Basic Auth (Legacy)
The server itself reads 1 and 2, so `npm run dev`, `npm run preview` and
Pinokio also work with only `OPENSKY_CREDENTIALS_FILE` set in `.env`, on any
platform. It reads the file on each token refresh.
Basic auth is still supported for now, but OpenSky is moving away from it.
## Basic Auth (Retired)
```bash
OPENSKY_AUTH_MODE=basic ./scripts/dev-fresh.sh
```
OpenSky stopped accepting username/password (HTTP Basic) sign-in on
2026-03-18; its API now takes only OAuth2 client credentials. With
`OPENSKY_AUTH_MODE=basic` the server no longer sends `OPENSKY_USERNAME` /
`OPENSKY_PASSWORD`: it requests anonymously (reason `basic_retired`) and
logs once what to set instead. Create an API client on your OpenSky
account page and use its `credentials.json` as above.
## Troubleshooting
@@ -61,6 +67,6 @@ Useful reasons:
- `oauth_invalid_or_missing`: OAuth client not found/usable
- `oauth_invalid_credentials`: OAuth client rejected by OpenSky
- `basic_invalid_credentials`: username/password rejected
- `missing_basic_creds`: basic mode missing user/pass
- `missing_oauth_creds`: auto mode found no OAuth client
- `basic_retired`: basic mode requested; answered anonymously
- `rate_limited`: OpenSky throttling
+24 -15
View File
@@ -12,7 +12,7 @@ A rail panel uses an outer owner with its existing stable `id`, matching
`data-panel-id`, and `panel-collapsible` class. Its first non-glow child is the
surface. Keep the header and any outward popup outside the scrolling body:
```html
```vue
<section
id="example-panel"
data-panel-id="example-panel"
@@ -21,14 +21,11 @@ surface. Keep the header and any outward popup outside the scrolling body:
<div data-panel-surface>
<header data-panel-header>
<h2 class="panel-title panel-surface-title">Example</h2>
<button
class="panel-collapse-btn panel-surface-control"
data-collapse-target="example-panel"
aria-expanded="true"
aria-label="Collapse Example"
>
−
</button>
<PanelCollapseButton
class="panel-surface-control"
target="example-panel"
:name="t('Example')"
/>
</header>
<div data-panel-body data-rail-scroller>
<label>Filter <input class="panel-surface-control" type="text" /></label>
@@ -92,15 +89,27 @@ Do not copy the rail example's `.panel-collapse-btn` class into the dock:
existing dock rules intentionally hide that class. The generic surface skin
does not replace the dock's disclosure policy.
The existing `PanelChrome` binds `.panel-collapse-btn[data-collapse-target]`
at initialization and routes state through `setPanelCollapsed`. For additions
A panel with Core's standard header (glow, title, badges, divider, actions
and collapse button) is a `RailPanel` (`src/ui/rail/`): the kit's `UiPanel`
bound to the `gev-panel-chrome` store, with its state classes drawn by
`v-panel-state` and its button's click asking `PanelChrome` to toggle it.
A panel with its own header uses `PanelCollapseButton`, which renders the
collapse button (`.panel-collapse-btn`) from the open state `PanelChrome`
presents to that store; until then, both show their `startCollapsed` state,
which should match the markup's `collapsed` class.
Either way, the click reaches `PanelChrome`'s `setPanelCollapsed`. `PanelChrome` collects the buttons'
targets at initialization and binds each panel's Escape. For additions
present at startup, keep those hooks and the existing chrome lifecycle. A
dynamically mounted panel must bind and dispose its own disclosure using
`bindPanelDisclosure` / `collapsePanelOnEscape` from `panelDisclosure.js`, with
the application owning state changes. Do not rerun the whole application chrome
dynamically mounted panel must own its disclosure: its own state, button and
Escape, bound and disposed with `bindPanelDisclosure` /
`collapsePanelOnEscape` from `panelDisclosure.js` (which bind Escape only). Do not rerun the whole application chrome
initializer just to add one panel. `layoutLeftPanelRail` and
`layoutRightPanelRail` accept arbitrary direct `data-panel-id` children; their
host owns layout scheduling, observations and collapse callbacks. Dynamic
host owns layout scheduling, observations and collapse callbacks. A rail
never writes a panel's classes: it calls `onCollapse(panel, {collapsed,
auto})`, and the host draws that state (the application presents it to the
panel chrome store, which each root's `v-panel-state` draws at once, so the
rail can measure the panel in the same task). Dynamic
content that changes size may require an owner-scheduled layout pass or an
owned `ResizeObserver`; adding CSS attributes does not register new observers.
+117
View File
@@ -0,0 +1,117 @@
# Arabic (`ar`)
Catalog: `src/i18n/locales/ar.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation sources: abiyeenzo (#915).
## Style
- Modern Standard Arabic, neutral across regions. Address the user with plain imperatives: «انقر»، «فعّل»، «أعد المحاولة».
- Arabic has no letter case: an UPPERCASE English HUD label becomes the same short Arabic label. Keep labels as short as the English: «مباشر»، «متقادم»، «متوقف».
- Status words in progress use «جارٍ …» («جارٍ التحميل»، «جارٍ الضبط»، «… جارٍ تشغيل اللقطة»), so a status chip never reads as the command next to it («ضبط» is the TUNE button); finished actions use the passive («تم الحفظ»، «أُلغي التحميل»). Data that refreshes itself «يُحدَّث» (يتحدّث means "speaks"). A sentence about a `{placeholder}` whose gender and number vary (tool, layer and cohort names, call signs, vessel names) stays impersonal: «{layers}: تم التفعيل للتو»، ««{name}» قيد الاستخدام»، «{id}: لا بيانات واردة الآن»، «لم يعد {name} ضمن البيانات المحمّلة».
- A label followed by its value takes a colon, so it does not read as a sentence: «الصوت: نشط»، «السياق: {value}»، «الاحتواء: {percent}%». A named mode or control quoted in a sentence takes «»: «يظهر في وضع «كثيف» فقط».
- Arabic punctuation in Arabic text: ، ؛ ؟ and «» for quotes. Keep `·` as the HUD separator, `—` with spaces, and `…`.
- Arrows that show a sequence point the way Arabic reads: «الإشارات ← الأخبار ← المحلي», «{origin} ← {destination}».
- Product names, provider names and codes stay in Latin letters: Google, Cesium ion, CelesTrak, NOAA, FIRMS, AIS, ADS-B, GNSS, CCTV, HUD, CRT, NVG, FLIR, WHOT/BHOT, Ironbow, Street View, Mapillary. Aviation and camera abbreviations stay Latin too: HDG, FOV, BRG, CRS, GS, TRK, V/S, SQUAWK. Local place names from the Bhote Koshi scene (Rasuwagadhi, Bidur, Timure…) stay Latin, and a site known by two names takes one Arabic noun in front: «جسر Mailung / Upper Trishuli»; well-known cities and countries are written in Arabic (أوستن، طوكيو، نيبال).
- Months are the international names used across the Arab world: يناير، فبراير … أغسطس.
## Digits and direction
- **Western digits (0–9)**, as in the catalog. The HUD reads like an instrument panel, and mixing ٠–٩ with Latin codes and coordinates misreads. `intlLocale('ar')` in `src/i18n/locale.js` passes `ar-u-nu-latn` to every Intl formatter, so browsers that default to Arabic-Indic digits for an Arabic region still show 0–9.
- **Right to left.** `ar` is in `RTL_LOCALES`, so the page gets `<html dir="rtl">`. The title, style indicator, both panel stacks and their icon rails swap sides; the dock and the corner row reverse. The intel HUD (classification block, MGRS, coordinates, GSD) stays a left-to-right instrument overlay; its translated lines read right to left inside it. FM seek and the radio dial stay left to right, as on any receiver. The rails' screen-reader names follow the mirrored layout: «Left panels» is «اللوحات اليمنى». Layout rules: `src/ui/styles/rtl.css`.
- Latin runs inside Arabic sentences (product names, codes, `{placeholders}` filled with English data) are isolated by the bidi algorithm; write them as plain text, without manual direction marks.
- Position counters: a translated counter says «من» («القناة {index} من {total}», «الإطار {index} من {total}»); a spaced slash between two isolated numbers swaps them in right-to-left text. Counters the code builds from bare numbers (`01 / 25`, `1 / 3`) are shown left to right by `rtl.css`.
- Code stays left to right: the JSON fields of the scene editor are LTR in every language.
- Comparison signs `<` and `>` are mirrored glyphs in right-to-left text («< 2%» shows as «> 2%»). Write them as words: «أقل من 2%»، «أكثر من 10%»، «أقل من ساعة». A legend's open upper bound («300+», «-40») is isolated by `formatScaleLabel()`, so its sign stays where it is written; `formatScaleRange()` isolates the unit after the range too («°C», not «C°»).
## Units
Distances and speeds take Arabic abbreviations after the number, with a space: كم، م، كم/س، كم/ث، م/ث، ميل/س، عقدة (knots)، قدم (feet)، مم. Time: ث (seconds)، د (minutes)، س (hours)، يوم; compact age labels («3d», «5D AGO») use ي: «{count} ي»، «قبل {count} ي», since a plain string cannot make يوم agree with the count. Area: آكر (acres, the value is not converted; فدان is a different unit). Money: the word دولار after the amount, not a bare `$`: «{amount} مليون دولار». Scientific and radio units stay Latin: MHz، dB، dBFS، dBZ، hPa، MW، MiB. `°` and `%` stay. Hemisphere letters in coordinates: ش (N)، ج (S)، ق (E)، غ (W), as in `21.33ش`.
## Plurals
`Intl.PluralRules('ar')` uses all six CLDR categories, and a `tn()` entry needs every one:
| Form | Counts | Example |
| ------- | --------------------------- | ------------- |
| `zero` | 0 | 0 مصدر |
| `one` | 1 | مصدر واحد (1) |
| `two` | 2 | مصدران (2) |
| `few` | 3–10, 103–110… | 3 مصادر |
| `many` | 11–99, 111–199… | 11 مصدرًا |
| `other` | 100–102, 200–202, fractions | 100 مصدر |
Every form must keep `{count}`. For one and two, Arabic names the quantity with the noun itself (مصدر واحد، مصدران), so the number follows in parentheses. `few` takes the plural noun; `many` the accusative singular (مصدرًا، مفتاحًا); `zero` and `other` the genitive singular. When a word next to the number cannot agree in every form, or a short chip has no room, put the number after a colon instead: «القريبة: {count}»، «الملفات المحددة: {count}». Where neither fits, «من أصل {total} من السجلات» agrees with any number. A standalone unit label («الدقائق») is never glued after a number: the voice-timeout options («3 دقائق»، «15 دقيقة») come from `Intl.NumberFormat` unit formatting, not from the catalog.
## Glossary
| English | العربية |
| ------------------------------------------ | ---------------------------------------------------------- |
| layer / data layer | طبقة / طبقة بيانات |
| feed / source | مصدر بيانات (short: مصدر) / مصدر |
| LIVE (status) / live data | مباشر / البيانات المباشرة (not حي) |
| Live ships, live detections (descriptions) | بيانات مباشرة عن السفن، رصد مباشر (not the adverb مباشرةً) |
| track, tracking | تتبّع (always with the shadda) |
| SIMULATED | محاكى (status) / محاكاة |
| stale / degraded / unavailable | متقادم / متدهور / غير متاح |
| ON / OFF (state) | مفعّل / متوقف |
| retry | إعادة المحاولة |
| key (API key) / POWER UP | مفتاح (مفتاح API) / تشغيل |
| Provider Settings | إعدادات المزوّدين |
| globe / map / map source / basemap | الكرة الأرضية / الخريطة / مصدر الخريطة / الخريطة الأساسية |
| globe map (map source drawn on the globe) | خريطة على الكرة الأرضية (not خريطة كروية) |
| tiles | بلاطات |
| aircraft / flight / military flights | طائرة / رحلة / الرحلات العسكرية |
| Live Flights / Live Vessels (layers) | تتبّع الرحلات / السفن المباشر (not الرحلات المباشرة) |
| call sign | رمز النداء |
| vessel, ship | سفينة |
| satellite | قمر صناعي |
| launch (rocket) / space mission | إطلاق / مهمة فضائية |
| contact (tracked object) | هدف |
| installation, site / mapped | منشأة، موقع / مرسوم |
| camera / CCTV camera | كاميرا / كاميرا CCTV |
| earthquake / active fire / fire perimeter | زلزال / حريق نشط / محيط حريق |
| containment (wildfire) | احتواء (not السيطرة) |
| cyclone advisory | نشرة إعصار |
| scene / shot / scene recipe | مشهد / لقطة / قالب المشهد |
| briefing / panel / dock | إحاطة / لوحة / الشريط السفلي |
| preset / visual preset | إعداد مسبق / نمط بصري |
| Scope (circular viewport mask) / keyhole | المنظار / الفتحة |
| Clean UI / EXIT CLEAN VIEW | واجهة نظيفة / الخروج من الواجهة النظيفة (not العرض) |
| cockpit | القمرة (قمرة القيادة) |
| heading / altitude / ground speed | الاتجاه (HDG) / الارتفاع / السرعة الأرضية |
| Street Level / Street View | مستوى الشارع / Street View |
| Recent Imagery | الصور الحديثة |
| Global Context / Context | السياق العالمي / السياق |
| radio station / tuner / gain | محطة / موالف / الكسب |
| broadcaster (radio stream) | محطة البث (not المحطة المُذيعة) |
| VOLUME (slider) / VOICE | مستوى الصوت / الصوت |
| decoder (SDR) | مفكّك |
| transit / bike share | النقل العام / الدراجات المشتركة |
| Previous / Next | السابق / التالي |
| compass points (N, NNE, …) | شمال، شمال شمال شرق، … |
| forecast / valid (time) / issued | توقع / عند، وقت التوقع (not صالح حتى) / صدر |
| observation / observed | رصد / مرصود |
| heard (aircraft received by the SDR) | الملتقطة |
| upstream (camera feed) | المصدر الأصلي (in every CCTV message, not bare المصدر) |
| field (weather) / model | حقل / نموذج |
| parameters / ellipsoid | المعلمات / المجسّم الإهليلجي |
| scene cloud | غيوم المشهد |
| box (Recent Imagery area) / pin | منطقة / تثبيت |
| width (box size) / view (current camera) | اتساع / العرض الحالي (عرض alone reads as display) |
| N/A | غير متاح (no abbreviation) |
| METRO (view band) / REGIONAL | حضري (not حاضرة) / إقليمي |
| THIS (voice pointer) | هذا |
| outline / mark (map annotation) | مخطط / علامة، تعليم |
| share (a view or link) | مشاركة |
| AI agent | وكيل ذكاء اصطناعي |
| spacecraft | مركبة فضائية (مركبة alone is any vehicle) |
| stage (rocket) / downrange | مرحلة / على امتداد المسار |
| landing position: CONFIRMED / PAD (RTLS) | مؤكَّد / منصة الإطلاق |
| recovery (rocket stage) | استعادة (not استرجاع) |
| Arrive at your destination (route step) | الوصول إلى وجهتك (a step, not وصلت) |
| forecast lead hours | مدة التوقع بالساعات (not ساعات المهلة) |
| cel-shaded | بتظليل كرتوني |
| (debris-)dammed lake | بحيرة خلف سدّ (ركامي) |
| frame (bring into view) | إظهار في الإطار |
| story beat | مرحلة القصة |
+108
View File
@@ -0,0 +1,108 @@
# German (`de`)
Catalog: `src/i18n/locales/de.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: abiyeenzo (#908).
## Style
- Standard German that reads naturally in Germany, Austria and Switzerland; ß where it belongs (Straße, schließen), no Swiss ss.
- No direct address: neither «Sie» nor «du». Actions and hints use the neutral infinitive: «Erneut versuchen», «Kamera anklicken zum Aktivieren», «Zuerst eine Aufnahme wählen», «Mikrofonzugriff erlauben und erneut versuchen». "Your" becomes the article or «eigene»: «Eigener RTL-SDR-Empfänger», «Die IP-Adresse ist für diesen Sender sichtbar».
- Nouns are capitalized, as German requires; otherwise sentence case, not Title Case: "Live Flights" → «Live-Flüge», "Provider Settings" → «Anbieter-Einstellungen». UPPERCASE English stays UPPERCASE: «LIVE-DATEN AUS», «KEINE ENTWARNUNG». In UPPERCASE, ß is written SS: «SCHLIESSEN», «STRASSENEBENE».
- Compounds are written as one word or with a hyphen next to a name, code or English term: «Datenebene», «Wetterkacheln», «TomTom-Schlüssel», «AIS-Schiffe», «Street-View-Fallback». Do not split them with spaces.
- Short labels, chips and HUD readouts stay short. German runs about a third longer than English, so prefer a short word or a standard abbreviation with a period over a wrapped line: REICHW., GESCHW., NIEDERSCHL., SONST., NACHR., VERST., VGL., KAL., KOMM. (a bare «KOMM» reads as the imperative)
- The avionics codes HDG, BRG, CRS, DEST, SPD, ALT, GS, TRK, V/S, FOV and SQUAWK stay in English, as in the cockpit. Words used in a sentence are translated: heading → «Kurs», bearing → «Peilung», range → «Reichweite».
- A dash between clauses is a spaced en dash « – »; a colon is fine where it explains: «Feed ausgefallen – neue Versuche in größeren Abständen», «SIMULIERT: Verkehrsdienst nicht erreichbar». Quotes are „…“.
- Status and waiting messages use the neutral infinitive too: «Warten auf Aktualisierung», not «Warte auf …». Reasons inserted after a dash stay lower case: «Szene nicht gespeichert – das Projekt ist größer als 5 MiB».
- A layer named in a sentence takes its panel name in quotes: «Die Ebene „Live-Flüge“ ist nicht aktiviert», «die Ebene „Live-Flüge“ lädt Daten für den jeweiligen Bildausschnitt».
- Rolling time windows say their length, so they cannot read as the previous calendar period: LAST YEAR → «LETZTE 12 MONATE», LAST MONTH → «LETZTE 30 TAGE».
- Keyboard keys are named as on German keyboards where they differ: Leertaste, Rücktaste, Umschalt; Esc, Enter, Tab and letter keys stay.
- Decimal comma in fixed text: «M4,4», «28,2710° N». Thousands separator is a period: «1.000 km». Formatted numbers come from `Intl` and are already German.
- Units: km, m, km/h, m/s, kn (knots, also in the cockpit), ft (feet), hPa, MW, h, min, s, T (days, in short readouts); in UPPERCASE text KM, KM/H, KN, FT, MIN, H, T («VOR {count} H»). A space between number and unit, in compact readouts too: «{value} m», «{count} min», «50 %», «{speed} KN», «{count} / 30 T». `°` sticks to the number.
- "ago" goes before the time: «vor {count} min», «vor {value}». When a placeholder already holds a localized "ago" text (`{age}` in `STALE · cached {age}`, `{ago}`), do not add «vor» again.
- Compass points use O for Ost: N, NNO, NO, ONO, O, OSO, SO, SSO, S, SSW, SW, WSW, W, WNW, NW, NNW; hemisphere letters N, S, O, W.
- Routing: direction words are lowercase adverbs (links, leicht rechts, scharf links, geradeaus), so a sentence never starts with one: «Auf {road} {direction} abbiegen», «Hier {direction} abbiegen», «Weiter {direction}». «Hier», not «Dann»: the step card already says «dann {leg}» and «Nächster Schritt: …». At a fork the verb is «weiter», which also works with «geradeaus»: «An der Gabelung {direction} weiter». Roundabout exits are ordinals with a period: «die {exit} Ausfahrt» → «die 2. Ausfahrt». A lane is named by where it leads, not by its position: «Die geradeaus führende Spur benutzen», «die leicht nach links führende», «die scharf nach rechts führende» (not «halblinke» or «äußerst linke»).
## Plurals
A `tn()` entry needs two forms. `Intl.PluralRules('de')` picks:
| Form | Counts | Example |
| ------- | --------------------------- | --------- |
| `one` | 1 | 1 Kamera |
| `other` | everything else: 0, 2, 1,5… | 3 Kameras |
Keep the verb in agreement: «{count} Station meldet» / «{count} Stationen melden». Abbreviated units and codes do not change: «{count} NACHR.», «{count} EMPFANGEN».
## Glossary
| English | Deutsch |
| ---------------------------------------------- | ---------------------------------------------------------------------- |
| layer / data layer | Ebene / Datenebene |
| feed / source / data source / input (cockpit) | Feed / Quelle / Datenquelle / Quelle |
| LIVE (status) / live data | LIVE / Live-Daten |
| SIMULATED | SIMULIERT |
| stale / degraded / unavailable | veraltet / eingeschränkt / nicht verfügbar |
| retry / retrying / retrying slowly | neuer Versuch / erneut versuchen / neue Versuche in größeren Abständen |
| fallback | Fallback |
| key (API key) / POWER UP / POWERED UP | Schlüssel / STARTKLAR / HOCHGEFAHREN |
| Provider Settings | Anbieter-Einstellungen |
| ON / OFF (state chips) | EIN / AUS |
| enable / disable | aktivieren / deaktivieren |
| globe / map / map source / map stack / basemap | Globus / Karte / Kartenquelle / Kartenstapel / Basiskarte |
| aircraft / flight / military flights | Luftfahrzeug (HUD and short labels: Flugzeug) / Flug / Militärflüge |
| vessel, ship | Schiff |
| satellite / shell (Starlink) / DENSE | Satellit / Schale / DICHT |
| launch (rocket) / launch site / space mission | Start / Startplatz / Raummission |
| ascent / replay / payload / stage / downrange | Aufstieg / Replay, Wiedergabe / Nutzlast / Stufe / Downrange |
| contact / tracking | Kontakt / Verfolgung |
| installation / site | Anlage / Standort |
| mapped / observed | kartiert / beobachtet (Bhote chip OBSERVED IMAGERY: REALE BILDDATEN) |
| camera / CCTV camera / camera badge | Kamera / CCTV-Kamera / Kamera-Markierung |
| earthquake / active fire / fire perimeter | Erdbeben / aktiver Brand / Brandgrenze |
| cyclone / advisory (NHC/CPHC) / cone | Wirbelsturm / Lagebericht (not «Warnung») / Kegel |
| issued (advisory, forecast) | ausgegeben |
| radio station | Sender |
| broadcast FM | UKW |
| road / flow (traffic) / flow coverage | Straße / Verkehrsfluss / Live-Abdeckung |
| transit / bike share | ÖPNV / Bikesharing |
| scene / shot / scene recipe | Szene / Aufnahme / Szenenrezept |
| capture shot / camera move | Aufnahme speichern / Kameraflug |
| anchor / data pack / bundle | Anker / Datenpaket / Bundle |
| story beat / evidence beat | Story-Schritt / Beweisschritt |
| briefing / panel / dock | Briefing / Bereich / Dock |
| cockpit | Cockpit |
| preset / visual preset | Preset / visuelles Preset |
| Scope (circular viewport mask) / keyhole | Scope / Schlüsselloch |
| feather / fade | Weichzeichnung / Ausblenden |
| overlay / detection overlay | Overlay / Erkennungs-Overlay |
| Clean UI | Reine Ansicht |
| Night vision / Thermal / Snow (cockpit styles) | Nachtsicht / Wärmebild / Schnee |
| heading / altitude / ground speed | Kurs / Höhe / Geschwindigkeit über Grund (short: GESCHW. ÜBER GRUND) |
| HDG / BRG / CRS / DEST / SPD / FOV | unchanged |
| RANGE / PITCH / HGT (CCTV calibration chips) | REICHW. / NEIGUNG / HÖHE |
| PREV / NEXT | ZURÜCK / WEITER |
| RESUME (radio transport button) | FORTSETZEN (not «WEITER», which is NEXT in the same row) |
| nearest (spatial) | nächstgelegen (always; «nächste» is NEXT) |
| expand / collapse (sections) | ausklappen / einklappen |
| L / R (relative bearing) | L / R |
| SIG / NEWS / LOCAL (briefing tabs) | SIG / NEWS / LOKAL |
| NOT AN ALL-CLEAR | KEINE ENTWARNUNG |
| Street Level / Street View (Google) | Straßenebene / Street View |
| Recent Imagery / box / pin (a day) | Aktuelle Bilddaten / Bereich / fixieren |
| frame (bring into view) | ins Bild nehmen |
| mark (map annotation) / outline | Markierung / Umriss |
| voice (control) / voice session | Sprachsteuerung / Sprachsitzung |
| YOU / THIS / HERE (voice card labels) | EINGABE / DIES / HIER |
| click / click to … | anklicken / … klicken |
| tap (touch) | Antippen (not «Tipp», which is a hint) |
| press / tap a key | Taste drücken / kurz drücken (never «tippen» for keys) |
| tilted view | Schrägansicht |
| card (host card) / voice card | Eintrag (in hints), Infokarte / Sprachkarte |
| label (on the map) / clear (switch off layers) | Beschriftung / ausschalten (not «löschen») |
| station tag (radio filter) | Kategorie |
| DIRECTORY BAND / {category} BAND (radio dial) | SENDERSKALA / SENDERSKALA · {category} (not «-BAND»: «ROCK-BAND») |
| MIC (dock button) | MIKRO |
| frame (weather, video) / image | Frame / Bild |
| collapse / landslide (Bhote Koshi) | Erdrutsch (not «Abbruch», which reads as "cancel") |
| website (radio station) | Website (SENDER-WEBSITE; «Standort» is a physical site) |
+105
View File
@@ -0,0 +1,105 @@
# French (`fr`)
Catalog: `src/i18n/locales/fr.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: abiyeenzo (#907); the French part of meirm's #312 was used for comparison.
## Style
- Standard French that reads naturally in France, Belgium, Switzerland and Canada; avoid regionalisms.
- Address the user with "vous" imperatives: « Cliquez », « Activez », « Réessayez ». Tooltips that name an action use the infinitive: « Réduire le panneau », « Réinitialiser la caméra ». UPPERCASE HUD instructions also use the infinitive: « CLIQUER SUR UNE CAMÉRA POUR L'ACTIVER ».
- Sentence case, not Title Case: "Live Flights" → « Vols en direct ». UPPERCASE English stays UPPERCASE, accents included: « ÉTAT », « PRÉC. ».
- Typography: a narrow no-break space (U+202F) goes before `:` `;` `?` `!` and `%`, and inside « » quotes: `Supprimer le plan « {shot} » ?`. It keeps the sign on the same line as the word before it. Do not type a plain space there. The same narrow no-break space separates thousands in a number written into the text: « 1 000 km ». Ellipsis is `…`, not `...`. Dashes keep their spaces: « flux indisponible — réessayez ».
- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: PRÉC., SUIV., RÉINIT., ENREG., INDISP., DIST.
- The HUD codes HDG, BRG, CRS, FOV, DEST, SPD and ALT stay in English everywhere, as in cockpit avionics. Words are translated: RANGE → « PORTÉE », heading in a sentence → « cap », ground-track course → « route » (« ROUTE 270° »).
- ON and OFF stay as they are on HUD toggles and chips (« CCTV ON », « PROJECTION OFF »); in a sentence use « activé » / « désactivé ».
- Keyboard keys stay in English: Space, Esc, Enter, Tab, Shift, Backspace, H, 1–9 (« Maintenez Space pour parler », « ESC pour fermer »).
- Units: km, m, km/h, m/s, nd (knots), ft (feet), h, min, s, with a space before the unit: « 250 km », « il y a 5 min »; in UPPERCASE text KM, KM/S, ND, FT, MIN. MW, dBZ, hPa and `°` stay. Decimal comma in fixed numbers: « 28,2710° N », « M4,4 ». Money: « 12 k$ », « 3 M$ », « 2 Md$ ».
- "ago" goes before the time: « il y a {count} min ». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add « il y a » again.
- Round-about exit ordinals: 1re, 2e, 3e… (« prenez la 1re sortie »).
## Plurals
A `tn()` entry needs three forms. `Intl.PluralRules('fr')` picks:
| Form | Counts | Example |
| ------- | -------------------------------------- | -------------------- |
| `one` | 0 and 1, fractions below 2 (1,5) | 1 caméra, 0 caméra |
| `many` | 1 000 000, 2 000 000… (exact millions) | 1 000 000 de caméras |
| `other` | everything else: 2–999 999, 2,5… | 3 caméras |
The `many` form is the `other` text with « de » (« d' » before a vowel) after the number: « {count} de couches effacées », « {count} D'ENTRÉES INCONNUES ». When the words after the number cannot agree (an UPPERCASE readout, a list), put the number after a colon in every form: « CAPTÉS : {count} ». The same goes for a layer name whose gender and number vary: « Couche activée : {label} », not « {label} activé », or a tool name: « Outil actif : {name} », not « {name} est actif ».
## Glossary
| English | Français |
| ------------------------------------------------------------ | ----------------------------------------------------------------------- |
| layer / data layer / DATA LAYERS (panel title) | couche / couche de données / COUCHES |
| feed / source / data source | flux / source / source de données |
| LIVE (status) / live data | DIRECT / données en direct |
| SIMULATED | SIMULÉ |
| stale / degraded / unavailable | obsolète / dégradé / indisponible |
| retry / retrying (status: RETRYING X) | nouvel essai / réessayez (NOUVEL ESSAI : X) |
| rate-limited | limite de requêtes atteinte |
| key (API key) / POWER UP / POWERED UP | clé (clé API) / ALLUMER / ALLUMÉ |
| Provider Settings | paramètres des fournisseurs |
| enable / disable | activer / désactiver |
| globe / map / map source / basemap / map stack | globe / carte / source de carte / fond de carte / fond de carte |
| tile (map) | tuile |
| aircraft / flight / military flights | aéronef (avion in short copy) / vol / vols militaires |
| vessel, ship / tanker (AIS type) | navire / navire-citerne |
| satellite / shell (Starlink) / DENSE | satellite / couche de la constellation / DENSE |
| launch / launch site / space mission | lancement / site de lancement / mission spatiale |
| ascent / replay / payload / stage / downrange | ascension / rejeu / charge utile / étage / distance au sol (DIST. SOL) |
| contact / tracking / track (HUD) | contact / suivi / piste |
| brackets (HUD corner brackets around contacts) | cadres de ciblage |
| installation / site | installation / site |
| mapped / observed | cartographié / observé |
| camera / CCTV camera / ALPR camera | caméra / caméra CCTV / caméra LAPI |
| earthquake | séisme |
| active fire (FIRMS detection) / wildfire | feu actif / incendie |
| contained / containment (wildfire) | circonscrit / part circonscrite (not « maîtrisé », which is controlled) |
| advisory (NHC/CPHC cyclone) | bulletin |
| forecast / lead hours / valid / issued | prévision / échéances / valide / émise |
| AQHI (Canada) | CQAS (cote air santé) |
| radio station / tune | station / régler |
| bikeshare / transit | vélos partagés / transports en commun |
| route (directions) / turn (maneuver) | itinéraire / manœuvre |
| scene / shot / scene recipe | scène / plan / recette de scène |
| bundle / data pack / anchor | archive / paquet de données / ancre |
| import / export (noun: file, scene, project) | importation / exportation (not « import » / « export ») |
| capture camera as anchor / set move start (scene editor) | ancre à la position de la caméra / vue actuelle comme départ |
| story beat | étape du récit |
| briefing / panel / dock | briefing / panneau / dock |
| cockpit | cockpit |
| preset / visual preset | préréglage / préréglage visuel |
| Scope (circular viewport mask) / keyhole | lunette / ouverture |
| detection overlay | surcouche de détection |
| Clean UI | vue épurée |
| Street Level (layer) / Street View (Google) | Niveau rue / Street View |
| Recent Imagery / box | imagerie récente / zone |
| Daily overview / overview (Recent Imagery VIIRS product) | vue quotidienne (one name for the product, not « vue d'ensemble ») |
| pin (Draw tool) / pin (Recent Imagery) | épingle / épingler |
| Cyber (HUD layout, effect) | Cyber |
| heading / course (ground track) / altitude / ground speed | cap / route / altitude / vitesse sol |
| HDG / BRG / CRS / FOV / DEST / SPD / ALT | unchanged |
| RANGE / PITCH / HGT (CCTV calibration chips) | PORTÉE / INCL. / HAUT. |
| PREV / PREVIOUS / NEXT (chips) | PRÉC. / PRÉC. / SUIV. |
| Previous / Next (button text) | Précédent / Suivant |
| L / R (relative bearing) | G / D |
| SIG / NEWS / LOCAL (briefing tabs) | SIG / ACTUS / LOCAL |
| compass points (N, NNE, …, W, NW) | N, NNE, …, O, NO |
| NOT AN ALL-CLEAR | PAS UN FEU VERT |
| AI agent | agent IA |
| share (a view or link) | partager |
| click / click to … | cliquez / cliquer pour … |
| map heading (compass, north-up control) | orientation (de la carte) |
| subject (cockpit / shared contact) | cible |
| cloud cover / CLOUD (briefing readout) | nébulosité / NÉBULOSITÉ |
| LEVEL (cockpit horizon guide) | HORIZON |
| CALIBRATED / CURATED / RAW PRIOR (CCTV pose) | CALIBRÉE / VÉRIFIÉE / ESTIM. INITIALE |
| DATASHEET / SPEC FOV (CCTV lens range) | FICHE TECHNIQUE / FOV CONSTRUCTEUR |
| PORT TENDER (AIS type) | SERVITUDE PORT. (navire de servitude portuaire) |
| ChatGPT sign-in (action) / opening sign-in / sign-in expired | connexion / page de connexion / la session a expiré |
| authored shot (scene) | plan scénarisé |
| report / record (USGS event card) / readout, reading | rapport / relevé (a measured value only) |
+108
View File
@@ -0,0 +1,108 @@
# Italian (`it`)
Catalog: `src/i18n/locales/it.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: abiyeenzo (#912).
## Style
- Standard Italian as in Italian software UIs. Address the user with informal "tu" imperatives: «Aggiungi», «Attiva», «Scegli», «Incolla», «Riprova». Tooltips that name an action use the same imperative: «Espandi il pannello», «Ripristina la camera».
- Sentence case, not Title Case: "Live Flights" → «Voli live», "Space Missions" → «Missioni spaziali». UPPERCASE English stays UPPERCASE: «LIVE», «NON DISPONIBILE».
- Quotes inside Italian text are straight double quotes: «Eliminare lo scatto "{shot}"?». Use the accented capitals (È, À) in UPPERCASE text: «VELOCITÀ», «CITTÀ».
- Errors read as a statement, not a past participle on its own: "Search failed" → «Ricerca non riuscita»; "Could not …" → «Impossibile …». A dash that joins a cause and a hint becomes a colon: «Chiave API rifiutata: controlla AISSTREAM_API_KEY». The same goes for a label and its hint in a tooltip: «Direzione (bussola °): clic per digitare».
- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: REGOLAZ., RILEV., INCL., ALT., VEL., STIM., PROIEZ., ESP.; PREC / SUCC for PREV / NEXT and PREVIOUS / NEXT.
- The HUD codes HDG, BRG, CRS, FOV and DEST stay in English, as in cockpit avionics. RANGE is «PORTATA». Words used in a sentence are translated: heading → «prua» (aircraft, ships) or «direzione» (camera, buses, trams, trains), course → «rotta».
- Keyboard keys stay in English: Space, Enter, Esc, Tab, Shift, Backspace, H, 1–9 («Tieni premuto Space per parlare», «Premi Enter per riprovare»).
- Decimal comma in numbers written in the text, magnitudes included: «28,2710° N», «M5,2». Numbers formatted by the code follow `Intl` and need nothing.
- Amounts: thousands, millions and billions are written out or abbreviated in Italian, not with K/M/B: «{amount} mila $», «{amount} mln $», «{amount} mld $».
- Units: km, m, km/h, m/s, kn (knots), ft (feet), h, min, s; in UPPERCASE text KM, KM/S, KT, FT, MIN, H. "m" is always metres, so minutes are «min» («5 min fa»). MW stays MW. `°` and `%` stay.
- "ago" goes after the time: «{count} min fa». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «fa» again.
- A feature name used as a sentence subject gets a head noun so the verb agrees: «La modalità Missioni spaziali isola…», «Il pannello Impostazioni provider è disattivato…», «Il livello {layer} è attivo…», «Lo strumento {name} è attivo…». A short card title with a placeholder name puts the state after a colon: «{label}: attivato». The same goes for a placeholder list of names: «Attivazione recente: {layers}». A count in a plain `t()` entry (no plural forms) that can be 1 goes after a label, so nothing has to agree with it: «Record accettati: {accepted} su {raw}».
- English loanwords that Italian UIs use as they are stay in English: feed, live, cockpit, briefing, preset, overlay, layout, snapshot, tile, provider, timeout, payload, bloom, scope.
## Plurals
A `tn()` entry needs three forms. `Intl.PluralRules('it')` picks:
| Form | Counts | Example |
| ------- | -------------------------------------- | ----------------------- |
| `one` | 1 | 1 telecamera |
| `many` | 1 000 000, 2 000 000… (exact millions) | 1 000 000 di telecamere |
| `other` | everything else: 0, 2–999 999, 1,5… | 3 telecamere |
The `many` form is the `other` text with «di» after the number when a noun follows: «{count} di telecamere caricate», «{count} DI CHIAVI IN ATTESA». When no noun follows the number («{count} vicine», «{count} RICEVUTI»), `many` repeats `other`. Nouns that do not change in the plural keep one form for all three (`{count} file`, `{count} data center`), with «di» in `many`.
## Glossary
| English | Italiano |
| --------------------------------------------- | ------------------------------------------------------------ |
| layer / data layer | livello / livello dati |
| feed / source / data source | feed / fonte / fonte dati |
| LIVE (status) / live data / live (feed state) | LIVE / dati live / live |
| SIMULATED | SIMULATO |
| stale / degraded / unavailable | obsoleto / degradato / non disponibile |
| retry / retrying | riprova / nuovo tentativo |
| rate limited | limite di richieste (raggiunto) |
| key (API key) / POWER UP / POWERED UP | chiave / ACCENDI / ACCESO |
| Provider Settings | Impostazioni provider |
| ON / OFF (state chips) | ON / OFF |
| enable / disable | attivare / disattivare |
| globe / map / map source / map stack | globo / mappa / fonte mappa / stack mappa |
| basemap / box (Recent Imagery area) | mappa di base / riquadro |
| aircraft, plane / flight / military flights | aereo / volo / voli militari |
| vessel, ship | nave |
| satellite / shell (Starlink) / DENSE | satellite / shell / DENSO |
| SPARSE / BALANCED / DENSE (detection modes) | RADO / BILANCIATO / DENSO (agree with «overlay») |
| broadband (Starlink shell) | a banda larga |
| VISUAL (CelesTrak group) | VISIBILI |
| launch (rocket) / launch site / space mission | lancio / sito di lancio / missione spaziale |
| ascent / replay / payload / stage | salita / replay / payload / stadio |
| contact / tracking | contatto / tracciamento |
| installation / site | installazione / sito |
| mapped / observed | mappato / osservato |
| camera (view) / camera, CCTV camera | camera / telecamera, telecamera CCTV |
| earthquake / active fire / fire perimeter | terremoto / incendio attivo / perimetro dell'incendio |
| lightning strike / cloud (cover, %) | fulmine / nuvole («non singoli fulmini», «20% di nuvole») |
| incident (wildfire, Type N) / scene incident | evento (Evento di tipo N, Incendio senza nome) / evento |
| trigger record (USGS card) / OPEN SOURCE link | scheda / APRI LA FONTE |
| advisory (NHC/CPHC cyclone) / cone | bollettino / cono di incertezza |
| forecast lead hours (cyclone points) | scadenze di previsione in ore |
| AIR / SEA (radio category) / ground station | ARIA / MARE / stazione di terra |
| radio station / broadcaster | stazione radio / emittente |
| CH (radio channel readout) | CH (unchanged) |
| road / flow (traffic) | strada / flusso |
| transit / bike share | trasporto pubblico / bike sharing |
| last report (vehicle position) | ultima segnalazione |
| scene / shot / scene recipe | scena / scatto / ricetta della scena |
| anchor / data pack / bundle (scene) | ancora / pacchetto dati / pacchetto |
| story beat | tappa del racconto |
| briefing / panel / dock | briefing / pannello / dock |
| cockpit | cockpit |
| preset / visual preset | preset / preset visivo |
| Tactical / Minimal / Cyber | Tattico / Minimale / Cyber |
| Scope (circular viewport mask) / keyhole | scope / foro |
| feather / fade | sfumatura / dissolvenza |
| detection overlay | overlay di rilevamento |
| pin (Draw tool) / mark | segnaposto / segno |
| pin (Recent Imagery) | fissare |
| Draw / Directions (tools) | Disegno / Indicazioni |
| Street Level (layer) / Street View (Google) | Vista stradale / Street View |
| Recent Imagery | Immagini recenti |
| Global Context / Context (panel) | Contesto globale / Contesto |
| heading, course / altitude / ground speed | prua, rotta / altitudine / velocità al suolo (VEL. AL SUOLO) |
| altitude in short HUD captions / Alt unknown | QUOTA (QUOTA · FT) / Quota sconosciuta |
| fallback (FALLBACK, Street View fallback) | riserva / di riserva (SUPERFICIE DI RISERVA) |
| forecast track / advisory center (cyclones) | traccia prevista / centro del ciclone da bollettino |
| witness source / authored (scene) | fonte testimoniale / predefinito, definito dall'autore |
| explore manually | esplora manualmente |
| HDG / BRG / CRS / FOV / DEST | HDG / BRG / CRS / FOV / DEST (unchanged) |
| RANGE / PITCH / HGT (CCTV calibration) | PORTATA / INCL. / ALT. |
| PREV / NEXT | PREC / SUCC |
| Previous / Next (button text) | Precedente / Successivo |
| Focus {target} (action, aria label) | Inquadra {target} (the FOCUS button stays FOCUS) |
| L / R (relative bearing) | S / D |
| SIG / NEWS / LOCAL (briefing tabs) | SIG / NOTIZIE / LOCALE |
| compass points (N, NNE, …, W, NW) | N, NNE, …, O, NO (west is «O», ovest) |
| NOT AN ALL-CLEAR | NON È UN VIA LIBERA |
| weather field / model / valid / issued | campo / modello / valida / emessa |
| click / click to … | fai clic / clic per … |
+112
View File
@@ -0,0 +1,112 @@
# Japanese (`ja`)
Catalog: `src/i18n/locales/ja.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: abiyeenzo (#913).
## Style
- Sentences use です/ます: 「キーを保存しました」, 「もう一度お試しください」. Labels, buttons and HUD chips are short nouns or verb stems without a final 。: 「再試行」, 「保存」, 「ライブ」, 「オフ」.
- Japanese has no case: an UPPERCASE English label becomes the same short Japanese label. Codes, units and names stay in their own case (HDG, ADS-B, km).
- Full-width punctuation in Japanese text: 、。:?!()「」. Quote names and titles with 「」: 「{shot}」を削除しますか? Keep `·` as the HUD separator, `…` for an ellipsis and `—`, `→`, `↗` as in English. A dash inside a sentence becomes 。 or :.
- Button names cited in running text go in []: [次へ]を使ってください.
- No space between Japanese and Latin letters, digits or a `{placeholder}`: 「AIS船舶」, 「Spaceを長押しで話す」, 「{count}件」, 「{name}を表示」.
- Two exceptions keep the space: a Japanese label before its value in a readout (「雲量 {pct}%」, 「ステーション {id}」, 「高度 {altitude}」), and the state words in toggles (「CCTV オン」, 「ラジオ オフ」).
- Waiting states are 「X待ち」, not 「Xを待機中」: 「観測データ待ち」, 「進路/予報円は第{number}報待ち」, 「RTL-SDRデバイスの接続待ち…」. 待機中 is for something on standby with no object: 「フィード待機中」, 「音声待機中」.
- Key names keep their own case in running text and hints: Tab, Enter, Space, Esc (「Tabでプレビュー · Enter / Spaceで選択」). Text that CSS upper-cases shows them in capitals, as in English.
- Loanwords are katakana with the long-vowel mark, as in Microsoft and Apple style: ユーザー, ブラウザー, ビューアー, プロバイダー, レイヤー, フィルター, モニター, パラメーター. Pick one katakana form per term and keep it (see the glossary).
- Product, provider and dataset names stay in Latin script: Google Maps, Cesium ion, OpenSky, CelesTrak, NASA FIRMS, Sentinel-2, KartaView. Google's own product names use Google's Japanese, joined like any Latin word: Googleストリートビュー. Nepal place names in the Bhote Koshi scenes stay in Latin script, as in zh-CN; country and well-known city names are translated (ネパール, 東京, オースティン).
## Numbers, units and dates
- Units: km, m, km/h, m/s, kt, ft, mph, hPa, MW, dB, °, % stay as written, with the English spacing (「250 km」, 「{frp} MW」, 「95 kt」). Code-built cockpit distances follow this too (`cockpitUnitCase` skips the HUD upper-casing for ja, as for zh). The cockpit's own speed caption is the HUD code KT, in 対地速度 · KT and under the readout.
- Number ranges keep the English en dash, not ~: 「2–6日ごと」, 「50–99%」, 「南緯25°–北緯80°」.
- Time units are words written straight after the number: 秒, 分, 時間, 日 (「5分前」, 「{count}時間」, 「1時間5分」). `formatQuantity` and `formatRelativeTime` drop the space for Japanese unit words, so code-built values match the catalog.
- Counters follow the thing counted: 機 (aircraft), 隻 (ships), 基 (satellites), 台 (cameras, vehicles), 局 (stations), 件 (records, results, events), か所 (sites), 個 (generic). Prefer a counter to a bare number.
- Dates come from `Intl` and read year first: 2026年8月26日. Coordinates in prose are 北緯 / 東経; HUD hemisphere letters stay N, S, E, W.
## Plurals
`Intl.PluralRules('ja')` has one category, `other`: a `tn()` entry has only that form. Put the counter next to the number: 「航空機{n}機」, 「船舶{n}隻」, 「メッセージ {count}件」.
## Glossary
| English | 日本語 |
| ----------------------------------------- | --------------------------------------- |
| layer / data layer | レイヤー / データレイヤー |
| feed | フィード |
| source / data source / attribution | ソース / データソース / 出典 |
| LIVE (status) / live data | ライブ / ライブデータ |
| SIMULATED | シミュレーション |
| stale / degraded / unavailable | 更新遅延 / 性能低下 / 利用不可 |
| cached (status) | キャッシュの(キャッシュの予報) |
| PARTIAL (feed state) | 部分的 |
| upstream (remote data source) | 配信元 |
| retry | 再試行 |
| key (API key) / POWER UP | キー(APIキー)/ パワーアップ |
| Provider Settings | プロバイダー設定 |
| globe / map / map source / basemap | 地球儀 / 地図 / マップソース / ベースマップ |
| mapped (OSM-mapped sites) | マップ済み |
| aircraft / flight / military flights | 航空機 / フライト / 軍用機 |
| FLIGHT {count} (a rocket stage's nth flight) | {count}回目の飛行 |
| vessel, ship | 船舶 |
| satellite | 衛星 |
| launch (rocket) / space mission | 打ち上げ / 宇宙ミッション |
| stage (rocket) / downrange | 段 / ダウンレンジ |
| contact (tracked object) | コンタクト |
| installation, site | 施設 |
| camera / CCTV camera | カメラ / CCTVカメラ |
| earthquake / active fire / fire perimeter | 地震 / 火災検知 / 延焼範囲 |
| contained / containment (fire) | 封じ込め / 封じ込め率 |
| scene / shot | シーン / ショット |
| briefing / panel / dock | ブリーフィング / パネル / ドック |
| preset / visual preset | プリセット / ビジュアルプリセット |
| Tactical / Minimal / Cyber | タクティカル / ミニマル / サイバー |
| Scope (circular viewport mask) / keyhole | スコープ / キーホール |
| Clean UI | クリーンUI |
| cockpit | コックピット |
| heading / altitude / ground speed | 機首方位 / 高度 / 対地速度 |
| track (TRK, direction over the ground) | 対地針路 |
| HDG / BRG / CRS / FOV / SPD / POS | そのまま(HUDコード) |
| squawk | スコーク |
| radio failure / emergency (7600 / 7700) | 通信機故障 / 緊急事態 |
| map heading / North up | 方位 / 北を上に |
| Street Level / Street View (Google) | ストリートレベル / ストリートビュー |
| Recent Imagery | 最近の衛星画像 |
| Global Context / Context (panel) | グローバルコンテキスト / コンテキスト |
| radio (station) / tune / seek | ラジオ(局)/ 選局・同調 / シーク |
| Public Safety / SAFETY (radio category) | 警察・消防 |
| transit / bikeshare | 公共交通 / シェアサイクル |
| occupancy: empty / standing room only / crushed | 乗客なし / 立ち乗りのみ / 非常に混雑 |
| traffic flow (TomTom) | 交通流 |
| Previous / Next | 前へ / 次へ |
| SIG / NEWS / LOCAL (briefing tabs) | シグナル / ニュース / ローカル |
| compass points (N, NNE, NE, ENE, …) | 北, 北北東, 北東, 東北東, … |
| weather field / FIELD (selector) | 要素 |
| weather model / forecast lead hours | モデル / 予報時間 |
| valid (forecast time) / issued | 対象時刻 / 発表 |
| radar reflectivity / lightning density | レーダー反射強度 / 雷密度 |
| cyclone, storm (NHC/CPHC) / advisory | サイクロン / 情報(第N報); alone: サイクロン情報 |
| potential tropical cyclone (NHC PC) | サイクロン化のおそれがある擾乱 |
| forecast uncertainty cone | 予報円 |
| tropical storm / tropical depression | 熱帯暴風雨 / 熱帯低気圧 |
| scene cloud / mosaic / daily overview | シーン雲量 / モザイク / 日次概観 |
| box (Recent Imagery area) | 範囲 |
| WINDOW (search radius around a subject) | 範囲 |
| coverage / coverage % / full, partial | カバー範囲 / カバー率(%のみ)/ 全体, 一部 |
| pin (Recent Imagery) | 固定 |
| clear day (Recent Imagery) | 雲の少ない日 |
| outline / mark (map annotation) | 輪郭 / マーク |
| share (a view or link) | 共有 |
| import / export (project, presets) | インポート / エクスポート |
| EXPORT (image, PNG) | 書き出し |
| voice (the voice assistant) | 音声アシスタント / 音声機能 |
| activate (a control, screen-reader text) | 押す(「押すと…」) |
| AI agent | AIエージェント |
| frame (bring into view) | 視野に収める |
| story beat / evidence beat | ストーリー区切り / 証拠の区切り |
| camera pose / camera move | カメラの姿勢 / 移動 |
| roundabout / ramp / exit | ロータリー / ランプ / 出口 |
| reduced motion | 動きを減らす |
| NOT AN ALL-CLEAR | 安全の保証ではありません |
| … Watch (scene titles: Earth, Orbital) | 監視(地球監視, 軌道監視) |
+107
View File
@@ -0,0 +1,107 @@
# Brazilian Portuguese (`pt`)
Catalog: `src/i18n/locales/pt.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: abiyeenzo (#909).
## Style
- Brazilian Portuguese (pt-BR): câmera, tela, compartilhar, arquivo, baixar, ônibus, metrô. Every `pt` tag, `pt-PT` included, gets this catalog, so avoid slang and regionalisms.
- Address the user with «você» imperatives: «Clique no mapa», «Ative o CCTV», «Tente novamente». Tooltips that name an action use the infinitive: «Expandir painel», «Redefinir a câmera».
- Sentence case, not Title Case: "Live Flights" → «Voos ao vivo». UPPERCASE English stays UPPERCASE: «AO VIVO», «SEM DADOS».
- Quotes inside Portuguese text are straight double quotes: «Excluir o plano "{shot}"?». A sentence that names a panel quotes it: «o painel "Configurações de provedores"» as the subject, so the verb agrees, and «adicione em "Configurações de provedores"» after a preposition.
- A dash between clauses is a spaced em dash « — », even where the English runs it in.
- A date or time after a participle takes a preposition: «emitido em {time}», «válida para {valid}», «Posição em {time}», «onde estava em {time}». Use «em», not «às», wherever {time} can include a date.
- A count in a string with no plural form (no `tn()`) goes after a colon, so it reads right for 1: «sincronizando feeds de cidades: {count}...», «CÂMERAS EM ESPERA: {count}».
- A placeholder that holds a layer or tool name of unknown gender and number gets a noun to agree with: «{label}: camada ativada», «A ferramenta "{owner}" está usando o ponteiro — feche-a primeiro».
- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: ANT., PRÓX., DESLIG., EXCL., INCL., ALT., VEL., câm.
- The HUD codes HDG, BRG, CRS, FOV, DEST, SPD, RANGE, ALT, GS, TRK and V/S stay in English, as in cockpit avionics («ALT {altitude} FT · GS {speed} KT · TRK {track}», «V/S {rate} FT/MIN»), and so does the HUD recording mark REC. Words used in a sentence are translated: heading → «rumo», range → «alcance».
- Keyboard keys stay in English: Space, Esc, Enter, Tab, Shift, Backspace, H, 1–9 («Segure Space para falar»).
- Common English tech words used in Brazil stay: feed, stream, snapshot, briefing, dock, layout, bloom, status, app, link, token.
- Units: km, m, km/h, m/s, kt (knots), ft (feet), h, min, s, always spaced from the number («Terremotos (24 h)», «Missões espaciais (30 d)»; in the layer names the space is a no-break space, U+00A0, so the unit never wraps away from the number); in UPPERCASE text KM, KM/S, KT, FT, MIN, H. "m" is always metres, so minutes are «min» («há 5 min»). MW stays MW. `°` and `%` stay. Decimal comma in written numbers («M4,4»).
- "ago" goes before the time: «há {count} min», «mais recente há {value}». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «há» again.
- Money: «US$ {amount} mil / mi / bi» for thousands, millions and billions.
- Compass points use Leste/Oeste: N, NNE, NE, ENE, L, ESE, SE, SSE, S, SSO, SO, OSO, O, ONO, NO, NNO; hemisphere letters N, S, L, O.
## Plurals
A `tn()` entry needs three forms. `Intl.PluralRules('pt')` follows Brazilian usage:
| Form | Counts | Example |
| ------- | -------------------------------------- | ------------------ |
| `one` | 0, 1 and anything below 2: 0, 1, 1.5 | 0 câmera, 1 câmera |
| `many` | 1 000 000, 2 000 000… (exact millions) | 1000000 de câmeras |
| `other` | everything else: 2–999 999, 2.5… | 3 câmeras |
The `many` form is the `other` text with «de» after the number: «{count} de câmeras carregadas», «{count} DE CHAVES PENDENTES». A `one` form is also read for 0, so keep it natural with zero («0 arquivo selecionado» is standard Brazilian usage).
## Glossary
| English | Português |
| --------------------------------------------- | ------------------------------------------------------------ |
| layer / data layer | camada / camada de dados |
| feed / source / data source | feed / fonte / fonte de dados |
| LIVE (status) / live data | AO VIVO / dados ao vivo |
| SIMULATED | SIMULADO |
| stale / degraded / unavailable | desatualizado / degradado / indisponível |
| retry / retrying | nova tentativa / tentando de novo |
| retrying (a fetch) / retrying slowly | buscando de novo / tentando de novo em intervalos maiores |
| rate-limited | «X: limite de requisições atingido» |
| dismiss (window, card, error) | fechar |
| key (API key) / POWER UP / POWERED UP | chave / ATIVAR / ATIVADO |
| Provider Settings | Configurações de provedores |
| ON / OFF (state chips) | ATIVO / INATIVO |
| enable / disable | ativar / desativar |
| globe / map / map source / map stack | globo / mapa / fonte do mapa / pilha de mapas |
| basemap / tile | mapa base / bloco |
| aircraft, plane / flight / military flights | aeronave, avião / voo / voos militares |
| vessel, ship / TANKER (AIS class) | embarcação / NAVIO-TANQUE |
| satellite / shell (Starlink) / DENSE | satélite / camada orbital / DENSO |
| SPARSE / BALANCED / DENSE (detection modes) | ESPARSO / EQUILIBRADO / DENSO (o modo) |
| launch (rocket) / launch site / space mission | lançamento / local de lançamento / missão espacial |
| KM DOWNRANGE / EST. DOWNRANGE | KM DO LOCAL DE LANÇAMENTO / EST. PELA DIST. DO LANÇAMENTO |
| ascent / replay / payload / stage | subida / reprise / carga útil / estágio |
| contact / tracking / track | contato / rastreamento / rastro |
| installation / site | instalação / local |
| PLACES CANDIDATE (installation class) | CANDIDATO DO GOOGLE PLACES |
| mapped / observed | mapeado / observado |
| camera / CCTV camera | câmera / câmera de CCTV |
| CALIBRATED / CURATED (CCTV badge) | CALIBRADA / COM CURADORIA |
| AUTO HOP ON / OFF (CCTV) | TROCA AUTO. LIGADA / DESLIG. (as the other CCTV toggles) |
| earthquake / active fire | terremoto / incêndio ativo |
| advisory (NHC/CPHC cyclone) | boletim |
| E/C Pacific (NHC/CPHC coverage) | Pacífico Leste/Central (no «L/C») |
| weather (layer, card) / weather satellite | clima / meteorologia, meteorológico |
| FOG / cloud unknown | NEVOEIRO / nebulosidade desconhecida |
| AQHI bands Low … Very High | Risco baixo … Risco muito alto |
| radio station / broadcaster | estação de rádio / emissora |
| Oldies / World (radio tags) | Flashback / Músicas do mundo |
| radio genre tags agree with «música» | Alternativa, Clássica, Eletrônica, Latina |
| road / flow (traffic) | via / fluxo |
| Street Level (layer) | Nível da rua |
| scene / shot / scene recipe | cena / plano / receita de cena |
| briefing / panel / dock | briefing / painel / dock |
| cockpit | cabine |
| preset / visual preset / EXPORT PRESETS | predefinição / predefinição visual / EXPORTAR PREDEF. |
| VISUAL PRESETS (dock caption) | PREDEF. VISUAIS |
| Scope (circular viewport mask) / keyhole | mira / visor |
| feather / fade | suavização / esmaecimento |
| detection overlay | sobreposição de detecção |
| pin (Draw tool) / box (Recent Imagery) | marcador / área |
| Cyber (HUD layout, effect) | Ciber |
| heading / course / altitude / ground speed | rumo / curso / altitude / velocidade solo (VELOC. SOLO) |
| map heading / bearing (compass, north up) | orientação (do mapa) |
| HDG / BRG / CRS / FOV / DEST / SPD / RANGE | HDG / BRG / CRS / FOV / DEST / SPD / RANGE (unchanged) |
| V/S (vertical speed code) | V/S (unchanged) |
| PREV / NEXT | ANT. / PRÓX. |
| Previous / Next (button text) | Anterior / Próximo (Próxima) |
| L / R (relative bearing) | E / D |
| SIG / NEWS / LOCAL (briefing tabs) | SIG / NOTÍCIAS / LOCAL |
| REGIONAL NEWS (fits the 16-character kicker) | NOTÍCIAS DA ÁREA |
| PITCH / HGT (CCTV calibration chips) | INCL. / ALT. |
| NOT AN ALL-CLEAR | NÃO GARANTE SEGURANÇA |
| Last report (transit vehicle position) | Último registro |
| intersection (CCTV) | cruzamento |
| {layers} just turned on (any list of layers) | «{layers}: ativação recente» (no agreement needed) |
| {label} on / off (one layer) | «{label}: camada ativada / desativada» |
| turn-by-turn: turn left / keep left / lane | vire à esquerda / mantenha-se à esquerda / faixa da esquerda |
+106
View File
@@ -0,0 +1,106 @@
# Ukrainian (`uk`)
Catalog: `src/i18n/locales/uk.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: meirm (#312).
## Style
- Address the user with polite imperatives: «Натисніть», «Увімкніть», «Спробуйте ще раз».
- Sentence case, not Title Case: "Live Flights" → «Рейси онлайн».
- Ukrainian norms, not Russian calques: «у радіусі», «за посиланням», «надто довгий», «потім», «вигляд» (a camera view: «вигляд згори»), «стеження» (tracking); prepositions alternate у/в and і/й by euphony.
- The apostrophe is always ʼ (U+02BC): обʼєкт, звʼязок, памʼять. Not ' or ’.
- Quotes inside Ukrainian text are «ялинки»; a button or layer name inside a sentence goes in them: Увімкніть «Вуличні знімки».
- In short labels, a standard abbreviation with a period beats a wrapped line: ПОПЕР., ДАЛЬН., КАЛІБР.
- Units: км, м, км/год, м/с, вуз (knots), фт (feet), год, хв, с, МГц, дБ, гПа; in UPPERCASE text КМ, М, КМ/С, ВУЗ, ФТ. MW is МВт in any case. `°` and `%` stay. Decimal comma in coordinates written as text: 28,2710° пн. ш.
- Coordinates: пн. ш. / пд. ш. / сх. д. / зх. д.; the HUD hemisphere letters are Пн, Пд, Сх, Зх.
## Plurals
A `tn()` entry needs four forms. `Intl.PluralRules('uk')` picks:
| Form | Counts | Example |
| ------- | ------------------------ | ---------- |
| `one` | 1, 21, 31, 101… | 1 камера |
| `few` | 2–4, 22–24, 32–34… | 3 камери |
| `many` | 0, 5–20, 25–30, 111–114… | 5 камер |
| `other` | fractions: 1.5, 2.25… | 1,5 камери |
`other` takes the genitive singular: 1,5 кадру, 1,5 літака, 1,5 ключа. When a word next to a number cannot agree in every form, move the number after a colon instead: «поруч: {count}», «відповідей: {responses}». The same trick keeps place names and tool names out of a preposition's case: «звідки: {origin}», «Активно: {name}».
## Glossary
| English | Українська |
| ----------------------------------------------- | ---------------------------------------------------------------- |
| layer / data layer | шар / шар даних |
| feed | потік даних (short: потік) |
| source / data source | джерело / джерело даних |
| LIVE (status) / live data | ОНЛАЙН / у реальному часі (status chip: ДАНІ ОНЛАЙН) |
| SIMULATED / SYNTHETIC | ІМІТАЦІЯ |
| stale / degraded / unavailable | застарів / з перебоями (chip: ПЕРЕБОЇ) / недоступний |
| retry | повтор |
| key (API key) / POWER UP | ключ / ЗАПУСК |
| Provider Settings / provider | Налаштування постачальників / постачальник |
| globe / map / map source / basemap | глобус / карта / джерело карти / підкладка |
| aircraft / flight / military flights | літак (short HUD: борт) / рейс / військові рейси |
| vessel, ship / vessels | судно / судна (gen. pl. суден) |
| satellite | супутник |
| launch (rocket) / space mission | запуск / космічна місія |
| ascent (rocket) / booster FLIGHT {n} | виведення / ПОЛІТ {n} (not рейс) |
| contact / tracking | контакт / стеження |
| installation, site / mapped | обʼєкт / нанесений (обʼєкт, контакт) |
| camera / CCTV camera | камера / камера CCTV |
| earthquake / active fire / fire perimeter | землетрус / активна пожежа / контур пожежі |
| advisory (NHC/CPHC cyclone) | зведення |
| scene / shot / scene recipe | сцена / кадр / рецепт сцени |
| briefing / panel / dock | брифінг / панель / док |
| preset / visual preset | пресет / візуальний пресет |
| Scope, keyhole (circular viewport mask) | приціл |
| overlay (detection overlay) / world overlay | накладка / накладка на глобусі |
| heading / altitude / ground speed | курс / висота / шляхова швидкість |
| map bearing, map heading | азимут карти |
| HDG / BRG / CRS, TRK / FOV / DEST / SPD / RANGE | КУРС / ПЕЛ. / ШК (шляховий кут) / ОГЛЯД / ПРИЗН. / ШВИД / ДАЛЬН. |
| Street View (Google) | Перегляд вулиць (as in Ukrainian Google Maps) |
| Street Level (layer) | Вуличні знімки |
| imagery / recent imagery | знімки / свіжі знімки |
| radar reflectivity / lightning density | радіолокаційна відбивність / щільність блискавок |
| infrared | інфрачервоний (short: ІЧ) |
| receiver / gain / decoder | приймач / підсилення / декодер |
| broadcaster | мовник |
| CONUS, WHOT/BHOT, Ironbow, CRT, NVG, FLIR | без перекладу |
| PREV / NEXT (HUD) / Previous / Next | ПОПЕР. / ДАЛІ / Попередній / Наступний |
| anchor / data pack / bundle (scene) | якір / пакет даних / архів |
| box (Recent Imagery area) | область |
| camera move | переліт |
| SIG / NEWS / LOCAL (briefing tabs) | СИГН. / НОВИНИ / МІСЦ. |
| compass points (N, NNE, NE, …) | Пн, ПнПнСх, ПнСх, … |
| L / R (relative bearing) | Л / П |
| click / tap | клацнути / натиснути |
| session (voice, OAuth) | сеанс |
| sequence (street-level photos) | послідовність |
| search (place search) | пошук місця |
| pitch (CCTV camera pose) | нахил |
| traffic (road traffic layer) | дорожній рух |
| phosphor (CRT) | люмінофор |
| view (map extent) / view centre / in view | видима область (карти) / центр карти / у кадрі |
| select (verb) / selected | вибрати / вибраний (not обрати) |
| ping (cockpit proximity contacts) | контакт |
| merge (turn-by-turn) / on ramp | влитися в потік / вʼїзд (off ramp: зʼїзд) |
| watch (Earth/Orbital Watch) | спостереження |
| timeout | тайм-аут |
| icon | значок (not іконка) |
| run (scene playback) / RUN LOG | відтворення / ЖУРНАЛ ВІДТВОРЕННЯ (запуск is a rocket launch) |
| Preview (voice panel replies) | відповіді |
| Marine (radio tag) / pleasure craft | море (not флот) / прогулянкове судно |
| emergency (ADS-B) / Emergency (radio tag) | аварійний стан / надзвичайні ситуації |
| confidence (FIRMS fire detection) | достовірність (short: достов.) |
| thermal (FLIR-style imaging) | тепловий (not термальний) |
| map stack | набір карт |
| stage (rocket) re-entry / recovery | вхід ступеня в атмосферу / повернення |
| ON / OFF (toggle chips) | УВІМК. / ВИМК. |
| Clean UI / EXIT CLEAN VIEW | чистий інтерфейс / ПОВЕРНУТИ ІНТЕРФЕЙС |
| full globe view | вигляд всього глобуса |
| inputs (cockpit context chip) | потоки (not вхідні дані) |
| downrange (rocket stage) | дальність |
| resolution (spatial) | роздільна здатність (not роздільність) |
| shared follow expired | посилання на стеження застаріло |
+77
View File
@@ -0,0 +1,77 @@
# Simplified Chinese (`zh-CN`)
Catalog: `src/i18n/locales/zh-CN.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation sources: abiyeenzo (#914) and EDIT-Aeterna (#979).
## Style
- Mainland usage and Simplified characters. Address the user neutrally, without 您/你 where a plain imperative works: 「点击地图」, 「重试」.
- Chinese has no case: an UPPERCASE English HUD label becomes the same short Chinese label. Keep labels as short as the English: 「实时」, 「过期」, 「关」.
- Full-width punctuation in Chinese text: ,。:;?!()“”. A dash is 「——」 without spaces; keep `·` as the HUD separator and `…` for ellipsis.
- One space between Chinese and Latin letters or digits: 「AIS 船舶」, 「24 小时」, 「{count} 个」. No space next to full-width punctuation.
- Units: km, m, km/h, m/s, kn, ft, MW, °, % stay as written; time words are 小时, 分钟, 秒 after a number (「5 分钟前」).
- Product names, provider names and codes stay as they are: Google, Cesium, CelesTrak, NOAA, FIRMS, AIS, ADS-B, GNSS, CCTV, HUD, CRT, NVG, FLIR, WHOT/BHOT, Ironbow, CONUS.
## Plurals
`Intl.PluralRules('zh-CN')` has one category, `other`: a `tn()` entry has only that form. Use a measure word next to the number where Chinese needs one: 「{count} 个密钥」, 「{count} 架飞机」, 「{count} 艘船」.
## Glossary
| English | 中文 |
| ------------------------------------- | --------------------------- |
| layer / data layer | 图层 / 数据图层 |
| feed | 数据源 |
| source / data source | 来源 / 数据源 |
| LIVE (status) / live data | 实时 / 实时数据 |
| SIMULATED | 模拟 |
| stale / degraded / unavailable | 过期 / 降级 / 不可用 |
| retry | 重试 |
| key (API key) / POWER UP | 密钥(API 密钥)/ 增强 |
| Provider Settings | 服务商设置 |
| globe / map / map source / basemap | 地球 / 地图 / 地图源 / 底图 |
| aircraft / flight / military flights | 飞机 / 航班 / 军用航班 |
| vessel, ship | 船舶 |
| satellite | 卫星 |
| launch (rocket) / space mission | 发射 / 太空任务 |
| contact (tracked object) | 目标 |
| installation, site | 设施 |
| camera / CCTV camera | 摄像头 / CCTV 摄像头 |
| earthquake / active fire | 地震 / 活跃火点 |
| scene / shot | 场景 / 镜头 |
| briefing / panel / dock | 简报 / 面板 / 停靠栏 |
| preset / visual preset | 预设 / 视觉预设 |
| Display / HUD layout | 显示 / HUD 布局 |
| Tactical / Minimal / Cyber | 战术 / 精简 / 赛博 |
| Scope (circular viewport mask) / keyhole | 视域 |
| Clean UI | 纯净界面 |
| cockpit | 座舱 |
| heading / altitude / ground speed | 航向 / 高度 / 地速 |
| map heading / North up | 方位 / 正北朝上 |
| Street Level / Street View (Google) | 街景 / 街景(Google) |
| Recent Imagery | 近期影像 |
| Global Context / Context (panel) | 全局态势 / 态势 |
| radio (station) | 电台 |
| transit / bikeshare | 公共交通 / 共享单车 |
| Previous / Next | 上一个 / 下一个 |
| compass points (N, NNE, …) | 北, 北东北, … |
| weather field / FIELD (selector) | 气象场 / 要素 |
| weather model / forecast lead hours | 模式 / 预报时效 |
| valid (forecast time) | 有效时间 |
| scene cloud / mosaic | 整景云量 / 镶嵌图 |
| pin (Recent Imagery) | 固定 |
| THIS (voice pointer) | 所指 |
| outline / trace an outline | 轮廓 / 绘制轮廓 |
| mark (map annotation) | 标注 |
| camera badge | 摄像头标记 |
| provider / embedded provider controls | 服务商 / 平台控件 |
| share (a view or link) | 分享 |
| AI agent | AI 智能体 |
| stage (rocket) / downrange | 子级 / 航程 |
| capture shot / camera move | 保存镜头 / 运镜 |
| story beat / evidence beat | 事件节点 / 证据节点 |
| camera pose | 位姿 |
| forecast uncertainty cone | 预报不确定范围 |
| seed (catalog data) | 预置 |
| frame (bring into view) | 将…纳入视野 |
+17
View File
@@ -0,0 +1,17 @@
# Traditional Chinese (`zh-Hant`)
Catalog: `src/i18n/locales/zh-Hant.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
`zh-Hant` is the Simplified Chinese catalog (`zh-CN`, credited to abiyeenzo, #914, and EDIT-Aeterna, #979) written in Traditional characters with OpenCC character conversion. Wording and terminology are the same as in `zh-CN`: change a term in [zh-CN.md](./zh-CN.md) and the `zh-CN` catalog first, then convert. Only the script differs.
After `s2tw` conversion, fix the characters OpenCC picks wrongly: 年曆史 → 年歷史, 下遊 → 下游, 並道 → 併道, 發佈 → 發布, 公佈 → 公布.
## Style
- Follow [zh-CN.md](./zh-CN.md) for wording, punctuation, spacing, units and the glossary, written in Traditional characters.
- Relative-time placeholders may already include 前. In `LIVE · updated {age}`, the caller supplies the complete relative age; do not append another 前.
- The picker displays 中文(繁體) / Chinese (Traditional); explicit Hans tags select Simplified Chinese.
## Plurals
`Intl.PluralRules('zh-Hant')` uses only `other`. Keep the measure words of the `zh-CN` entry: 「{count} 架飛機」、「{count} 艘船」.