Merge branch 'main' into feat/cctv-model-specs-refresh

This commit is contained in:
German Khokhlov authored and GitHub committed 2026-11-04 17:53:10 +04:00
commit 2d917c5909
1249 files changed
+105057 -21970

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`.
+22 -26
View File
@@ -276,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
@@ -297,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?,
@@ -372,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
@@ -401,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
@@ -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.
+513 -114
View File
File diff suppressed because it is too large. Load diff
+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. `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 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 |
+3 -3
View File
@@ -71,9 +71,9 @@ Status: Open (owner-accepted 2026-07-08, documented)
a few stragglers take one more poll.
- **Born-grounded first poll:** a contact first seen on the ground with no altitude
data renders at the geoid for ≤1 poll until its floor cell warms.
- Full context, improvement ideas, and the verification oracle
(`scripts/qa-floor-verify.mjs`):
`docs/superpowers/reports/2026-07-08-height-datum-handover.md`.
- Verify floor-placement behavior with `scripts/qa-floor-verify.mjs` against a
running local app; set `QA_BASE_URL` when it is not on the default port
(`http://localhost:4173`).
---
+4 -4
View File
@@ -1,6 +1,6 @@
# Translating God's Eye View
Shipped languages: **English** (source and fallback), **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`).
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`: [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.
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.
+1 -1
View File
@@ -20,7 +20,7 @@ The session adapter, action runner and backend interfaces remain unchanged.
| `pointerContext.js` | Pointer position, dwell pick and the current turn's pointer snapshot (session-scoped) |
| `referents.js` | Numbered items from the latest result for `referent:n` (session-scoped) |
| `resultDisplay.js` | What the voice card shows for a result and which items it numbers (read by the card and `referents.js`) |
| `gevActions.js` runner | Analyst follow-up memory (session: `resetConversation()` on stop, `dispose()` on removal) |
| `gevActions.js` runner | Area handles (page lifetime); analyst follow-up memory (session: `resetConversation()` on stop, `dispose()` on removal) |
The backend is replaceable per deployment: `realtimeController.js` accepts it as a
`backend` option and falls back to `createRealtimeBackend()`. Its two method
+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`.
+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 | مرحلة القصة |