Merge upstream main and migrate cable changelog to fragment

This commit is contained in:
Aliciawque committed 2026-10-22 13:13:59 +08:00
commit 4c0a8e3748
1020 files changed
+126461 -21563

No files matched your search

+1 -1
View File
@@ -158,7 +158,7 @@ routing and regional caches. Request parameters cannot override these endpoints.
Voice controls accept an action runner and a controller factory through
`createVoiceCommands` (`./voice/commands`). The default Realtime controller owns
microphone tracks, playback, push-to-talk, tool cancellation and radio handoff.
Its backend supplies `requestToken({ tier, signal })` and
Its backend supplies `requestToken({ tier, authMode, signal })` and
`negotiate({ offerSdp, credential, signal })`. The Realtime-compatible adapter
(`./voice/realtime-backend`) accepts separate token and connection transports
and endpoints. Only the short-lived client secret reaches the SDP endpoint.
+46 -9
View File
@@ -65,11 +65,10 @@ check, not a JavaScript sandbox. Common voice controls cannot depend on a
Realtime protocol implementation. Negative fixtures cover indirect helpers,
self-package imports, symlinks and unreachable files.
Source factories have dedicated exports for ALPR, bikeshare, CCTV, earthquakes,
FIRMS, installations, launches, radio, satellites and traffic. They preserve the
same factory implementations without loading layer rendering. ALPR/earthquake
record normalization and CCTV source endpoint policy have plain owners separate
from geometry/cards. Source exports do not start acquisition at import time.
Source-only factories use dedicated `layers/<family>/source` exports where
available, preserving the same factory implementation without loading rendering.
Record normalization and endpoint policies have plain owners separate from
geometry/cards. Source exports do not start acquisition at import time.
Other layer `ingestion.js` files may still coordinate Cesium resources; the
portable contract applies to the explicitly reviewed graphs above. Their layer
@@ -357,8 +356,8 @@ drag or final change. Feature adapters own data mapping and service subscription
generic modules own DOM/listeners/timers and release them in `destroy()`.
Keep persistence with panel chrome: collapse uses
`godsEyeView.v6.panelCollapsed.<panelId>`, position uses
`godsEyeView.v8.panelPos.<panelId>`, and share state uses the registered panel id.
`godsEyeView.v6.panelCollapsed.<panelId>`, positions are
not stored (`godsEyeView.v8.panelPos.<panelId>` stays reserved), and share state uses the registered panel id.
Do not rename existing
ids or reset persistence versions when adding a body. The WEATHER adapter is
`src/ui/weatherPanel.js`; its observed-history selection remains transient.
@@ -375,6 +374,15 @@ renderer, persistence or services. The facade retains panel visibility, share
restore claims and render scheduling. Both controls are destroyed before the
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.
## Display controls
`ui/display` owns Display button, selector and slider subscriptions. It receives
@@ -630,9 +638,9 @@ explicit operations. Both exports exclude Cesium, DOM and application assembly.
The snapshot renderer applies record changes; rendering owns a weak map of
geometry and billboard resources used by cards, picking and trails.
`gods-eye-view/sources/reference` constructs fresh earthquake and bundled cable
`gods-eye-view/sources/reference` constructs fresh earthquake, GNSS integrity and bundled cable
source instances independently of standalone setup. Individual sources remain
available through `layers/earthquakes/source` and `layers/submarine-cables/source`.
available through their `layers/<family>/source` exports, including `layers/gnss/source`.
The latter retains the bundled dataset’s attribution and licensing requirements.
## Geocoding and feature-query providers
@@ -730,3 +738,32 @@ sources supply `{records, droppedCount, status, saturated}`; viewport filtering,
exact-bound retry and rendering stay in the layer. Legacy cache saturation is
decoded in the source. Source and geometry modules remain independent of Cesium,
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
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.
The shared stylesheet owns field surfaces and disabled opacity; feature and theme
palettes use `--gev-field-background`, `--gev-field-border-color`,
`--gev-field-color` and `--gev-field-disabled-opacity`. Radio retains its own
compact density and arrow space. CCTV owns calibration editor width in
`cctv.css`; the shared inline density does not impose a feature-specific width.
Cyber supplies a shared-field palette explicitly and keeps its legacy native
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.
+1037 -108
View File
File diff suppressed because it is too large. Load diff
+92
View File
@@ -0,0 +1,92 @@
# UI text and translation
Every piece of text a user can see goes through `t()`, `tn()` or `msg()` in code, or a `data-i18n*` attribute in markup. The English text itself is the key. English needs no catalog; other languages live in `src/i18n/locales/<code>.json`. Translators work from [TRANSLATORS.md](./TRANSLATORS.md).
## Which one to use
| | Translates | Use for |
| ------------------------------- | --------------------------------- | --------------------------------------------- |
| `t(text, values)` | now | any text shown right where it is written |
| `tn(count, one, other, values)` | now, picking the form for `count` | text that changes with a number |
| `msg(text)` | never: returns `text` unchanged | text stored in data and passed to `t()` later |
```js
import { t, tn, msg } from '../i18n/index.js';
status.textContent = t('Save keys');
status.textContent = t('Saved to {store}', { store: storeLabel() });
chip.textContent = tn(missing, '{count} KEY WAITING', '{count} KEYS WAITING');
// en: 1 KEY / 3 KEYS ru: 1 КЛЮЧ / 3 КЛЮЧА / 5 КЛЮЧЕЙ
const FEED_STATE_LABELS = { loading: msg('LOADING'), stale: msg('STALE') };
label.textContent = t(FEED_STATE_LABELS[state]);
```
A module the server, tools or other portable code also imports must not load the catalogs: it imports `msg` from `src/i18n/marker.js`, which only marks the text, and the UI translates it with `t()` where it shows it. (Older modules keep a local `const msg = (text) => text`, which works the same.)
`msg()` exists for two reasons. `npm run i18n` finds messages only as string literals inside `t(…)`, `tn(…)` and `msg(…)`, so `t(FEED_STATE_LABELS[state])` alone tells it nothing. And a `t()` call at module load runs before the page has picked its language, so it would freeze the English.
## Markup
```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.
## Rules
- **Pass literals.** `t('Close')`, not `t(label)` (unless `label` holds a `msg()` value) and never a template literal with `${…}`.
- **Use placeholders, not concatenation.** `t('Flying to {place}…', { place })`, not `t('Flying to') + ' ' + place`. Word order differs between languages.
- **Use `tn()` for counts**, not `count === 1 ? … : …`. Russian has three forms.
- **Translate the label, not the value.** Layer ids, enums, share-link parameters, API fields, voice tool schemas and feed data (callsigns, vessel and place names) stay as they are; translate only the text shown for them.
- **No `t()` at module top level.** Keep the English in `msg()` and call `t()` when rendering.
- **Numbers and dates** go through `formatNumber`, `formatDate`, `formatRelativeTime` and `formatUtcDayMonth` from `src/i18n/index.js`, which follow the active language.
- **Identical English means identical translation,** unless you give it a context: `t('CLEAR', { context: 'action' })` (a button) is its own catalog entry, `action|CLEAR`, while `t('CLEAR')` (the sky) stays separate. English output is the same.
- **Text that should stay English** (a brand, a code, a unit symbol) is simply not wrapped.
## After you add or change text
```sh
npm run i18n # what each language lacks or no longer uses, and unmarked UI text
npm run i18n -- --missing ru # the missing ru entries as JSON
npm run i18n -- --prune # drop entries whose English no longer exists
```
Changing the English changes the key: the old translation goes unused and the new text shows as missing. Add the translation to every `src/i18n/locales/<code>.json` (or ask a translator), prune, and run `npm test`; it fails until every language is complete.
To see a language, open the app with `?lang=ru`, or use the language picker (the globe button in the bottom-right corner).
An empty translation is reported, and so is one left as the English, plural forms included. When English is right on purpose (a code such as CRT, a product name), list it under the language in `src/i18n/same-as-english.json`.
## Text the check reports as unmarked
`npm run i18n` and `npm test` fail on UI text that is not marked:
- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`.
- **Code:** string literals that reach the page without `t()`, `tn()` or `msg()`: assigned to `textContent`, `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 |
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.
## Where things live
| File | What it holds |
| ------------------------------- | ------------------------------------------------------------------------------ |
| `src/i18n/index.js` | `t`, `tn`, `msg`, the Intl formatters, markup translation, switching language |
| `src/i18n/marker.js` | `msg` alone, for modules the server or tools import (no catalogs) |
| `src/i18n/locale.js` | shipped languages, their names, choosing the language at boot |
| `src/i18n/locales/<code>.json` | one catalog per language, keyed by English text |
| `src/i18n/same-as-english.json` | per language, the entries that stay as in English on purpose |
| `scripts/i18n.mjs` | `npm run i18n`: extracts messages, checks the catalogs, finds unmarked UI text |
| `src/i18n/catalog.test.mjs` | runs that check under `npm test` |
+2 -3
View File
@@ -57,9 +57,8 @@ Related keys (current versions):
- Panel collapsed state: `godsEyeView.v6.panelCollapsed.<panel-id>` — `'0'` open,
`'1'` closed, absent means the panel's own default. A view opened from a share
link ignores the stored value entirely.
- Panel positions: `godsEyeView.v8.panelPos.<panel-id>` — written only for a
floating portable panel (CCTV, Street Level). Deleting it, or double-clicking
the panel header, docks the panel again.
- Panel positions: `godsEyeView.v8.panelPos.<panel-id>` — the current layout
writes none.
- CCTV calibration: `godsEyeView.cctv.calibration.v2`
---
+11 -1
View File
@@ -49,7 +49,8 @@ failure elsewhere. These questions guide review, not a mandatory questionnaire.
regressions? Prefer fixes whose tests fail on the base. Test-only PRs should
close a real coverage gap with deterministic fixtures and useful assertions.
Inspect removed/skipped tests, weakened assertions, mocks, snapshots, and runner
changes. Update `CURRENT-STATE.md` and `CHANGELOG.md` for runtime changes, and
changes. Update `CURRENT-STATE.md` and add a `changelog.d/<pr-number>-<slug>.md`
fragment (not an edit to `CHANGELOG.md`) for runtime changes, and
`DATA_SOURCES.md` for source changes.
## 3. Choose execution proportionate to risk
@@ -124,6 +125,15 @@ Condition the merge on the reviewed head where supported; if revisions change,
return to validation. Verify the merged tree matches the validated candidate and
record the merge commit/PR URL; stop on mismatch.
Pending release notes live in `changelog.d/`, one file per PR, so integration never
resolves the top of `CHANGELOG.md`. At release, `npm run changelog:check` previews
the batch and its order; `npm run changelog` splices it into `CHANGELOG.md` and
empties the directory. The assembler refuses a malformed filename or two fragments
claiming one PR number before writing, and copies text through unchanged so the
contributor's wording and credit ship. The write is staged in
`changelog.d/.assembling/`, so if it fails part-way, rerunning `npm run changelog`
finishes or rolls back the batch. It never duplicates or drops an entry.
Return this concise record privately unless posting is authorized. Keep sensitive
security evidence in the private report:
+6
View File
@@ -128,6 +128,12 @@ Use the same controls before attributing a difference to the application:
6. Record live object counts before attributing a difference to the client.
7. Treat a live-source outage as missing coverage, not as evidence of low client
rendering cost.
8. Record whether the app is running in Pinokio's embedded view or in an
external browser. One Pinokio 8.2.0 user reported severe stutter in the
embedded view that went away when the same local URL was opened in a normal
browser ([#365](https://github.com/bilawalsidhu/gods-eye-view/issues/365)).
Do not compare embedded-view and external-browser measurements directly;
compare the reported GPU renderer and matched scene settings first.
## What is not established yet
+3 -3
View File
@@ -263,17 +263,17 @@ and the development server's internal routes. See SECURITY.md.
| `get_earthquakes` | `earthquakes` | USGS M2.5+ events in the last 24 hours, strongest first |
| `get_active_fires` | `fires` | NASA FIRMS detections in an area, highest radiative power first |
| `get_recent_launches` | `launches` | Launch Library 2 launches in the last 30 days, newest first |
| `aircraft_in_area` | `aircraft` | Aircraft in an area, nearest first; `military: true` reads the `military` feed |
| `aircraft_in_area` | `aircraft` | Aircraft in an area, nearest first; `military: true` reads the `military` feed; `group_by` and `stats` count and summarize every match |
| `find_aircraft` | `aircraft` | Aircraft anywhere by callsign, ICAO address or registration |
| `get_aircraft_track` | `aircraft` | Recent positions of one aircraft, thinned to 200 points |
| `get_aircraft_info` | `aircraft` | Aircraft type and registration, and flight route, from adsbdb |
| `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type |
| `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type; `group_by` and `stats` count and summarize every match |
| `find_vessel` | `vessels` | Ships anywhere by MMSI, IMO number or name |
| `get_vessel_track` | `vessels` | Recent positions of one ship, thinned to 200 points |
| `next_satellite_pass` | `satellites` | Next pass over a place or point (default the ISS), with naked-eye visibility |
| `satellites_overhead` | `satellites` | Satellites in a CelesTrak group above a place or point now, highest first |
| `find_cctv_cameras` | `cctv` | Public cameras in an area, nearest first |
| `get_cctv_snapshot` | `cctv` | The current image from one camera, returned as image content |
| `get_cctv_snapshot` | `cctv` | An image from one camera, returned as image content; Street View fallbacks are labelled historical |
| `find_alpr_cameras` | `alpr` | OpenStreetMap-mapped license plate readers in a US/Canadian area up to 3° |
| `find_radio_stations` | `radio` | Radio Browser stations by area and/or search terms, with stream URLs |
| `search_places` | `placeSearch` | Points of interest matching a query within an area (Google Places) |
+66
View File
@@ -0,0 +1,66 @@
# Translating God's Eye View
Shipped languages: **English** (source and fallback), **Spanish** (`es`) and **Russian** (`ru`).
## How it works
The English text is the key: code calls `t('Save keys')`, and markup marks elements with `data-i18n`. How developers mark text is in [I18N.md](./I18N.md).
Every other language is one flat JSON file, `src/i18n/locales/<code>.json`, keyed by the English text:
```json
{
"Save keys": "Сохранить ключи",
"{count} cameras": {
"one": "{count} камера",
"few": "{count} камеры",
"many": "{count} камер",
"other": "{count} камеры"
}
}
```
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 check
```sh
npm run i18n # what is missing, unused or broken, per language
npm run i18n -- --missing ru # missing ru entries as JSON, ready to translate
npm run i18n -- --prune # drop entries the code no longer uses
```
`npm test` runs the same check. It fails on a missing, empty or unused entry, on placeholders that differ from the English, on a missing plural form, and on text or plural forms still in English. The `--missing` template fails until every entry is translated. If an entry is right in English (a code such as CRT, a product name), add it under your language code in `src/i18n/same-as-english.json`.
Changing English text changes its key: the old translation becomes unused and the new text shows as missing. Translate it and prune.
## Fixing a translation
Edit `src/i18n/locales/<code>.json` and run `npm run i18n`. To see your change, open the app with `?lang=<code>`.
## Adding a language
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`.
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.
## Rules
- Keep every `{placeholder}` name exactly as in the English: do not rename, drop or add one. Its position inside the sentence follows the grammar of your language, e.g. `"{accepted} of {raw} records accepted"` → `"принято записей: {accepted} из {raw}"`.
- UPPERCASE English stays UPPERCASE. Short labels, chips and HUD readouts should stay short; prefer an abbreviation over a wrapped line.
- Keep the separators `·`, `—`, `→`, `↗`, `…`.
- Leave product, provider and data names in their original form: Google Maps, Cesium ion, OpenAI, ChatGPT, NASA FIRMS, USGS, OpenSky, ADS-B, AIS, CCTV, HUD, TomTom, OpenStreetMap, Mapillary, and keyboard keys (Space, Esc, H, 1–9).
- Units follow the language's convention; see the language notes.
## Never translated
These are contracts, not copy: layer ids and registry keys, status enums and other machine values (translate only the label shown for them), share-link and hash parameters, `?lang=`, storage schemas, API fields, voice tool names and schemas, provider and dataset names, data from feeds (callsigns, vessel, satellite, station and place names, headlines), attribution, console diagnostics, and user-authored content.
## 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.
+10
View File
@@ -8,6 +8,7 @@ The session adapter, action runner and backend interfaces remain unchanged.
| Owner | State and responsibility |
| --- | --- |
| `realtimeConnection.js` | Start generation, request cancellation, peer/data channel, acquired microphone stream and playback element |
| `realtimeBackend.js` | Token and SDP requests, their separate transports, the abort/timeout scope around both, and client-secret expiry |
| `realtimeTurns.js` | Active and superseded responses, pending confirmations, deduplicated calls and action cancellation |
| `realtimeRadio.js` | Speaker ducking, prepared playback, stronger-action reservations and playback observers |
| `realtimeInput.js` | Physical Space gesture, microphone mute, input/output meters and their frame/listener lifetime |
@@ -21,6 +22,15 @@ The session adapter, action runner and backend interfaces remain unchanged.
| `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) |
The backend is replaceable per deployment: `realtimeController.js` accepts it as a
`backend` option and falls back to `createRealtimeBackend()`. Its two method
signatures live with [application construction](APPLICATION.md); what belongs here
is the ownership. The connection is the backend's only consumer in the application.
The connection reads credential metadata for diagnostics and cost accounting,
then passes the complete credential unchanged to `negotiate`. Client-secret expiry is refused inside the backend, not by the connection.
`realtimeOwners.test.mjs` drives whole sessions through a substitute that
implements only those two methods.
Pure input policy, preferences and protocol-response policy have separate modules.
Owners receive named readers, operations or focused collaborators. Cross-owner
cancellation uses operations; response handling does not own Radio's reservations
+75
View File
@@ -0,0 +1,75 @@
# Spanish (`es`)
Catalog: `src/i18n/locales/es.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
## Style
- Neutral Spanish that reads naturally in Spain and Latin America; avoid regionalisms.
- Address the user with informal "tú" imperatives: «Añade», «Activa», «Elige», «Pega», «Inténtalo de nuevo». Tooltips that name an action may use the infinitive: «Expandir el panel», «Restablecer la cámara».
- Sentence case, not Title Case: "Live Flights" → «Vuelos en vivo». UPPERCASE English stays UPPERCASE: «EN VIVO», «SIN DATOS».
- Quotes inside Spanish text are straight double quotes: «¿Eliminar el plano "{shot}"?». Questions and exclamations open with ¿ and ¡.
- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: ANT., SIG., VEL., DIST., SINCR., cám., cob.
- The HUD codes HDG, BRG, CRS, FOV, DEST, SPD, RANGE and ALT stay in English everywhere, as in cockpit avionics. Words used in a sentence are translated: heading → «rumbo», range → «alcance».
- Keyboard keys stay in English: Space, Esc, Enter, Tab, Shift, H, 1–9 («Mantén pulsado Space para hablar»).
- Units: km, m, km/h, m/s, kn (knots), ft (feet), h, min, s; in UPPERCASE text KM, KM/S, KN, FT, MIN, H. "m" is always metres, so minutes are «min» («hace 5 min»). MW stays MW. `°` and `%` stay.
- "ago" goes before the time: «hace {count} min». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «hace» again.
- Spelling: «costo» (not «coste»).
## Plurals
A `tn()` entry needs three forms. `Intl.PluralRules('es')` picks:
| Form | Counts | Example |
| ------- | -------------------------------------- | -------------------- |
| `one` | 1 | 1 cámara |
| `many` | 1 000 000, 2 000 000… (exact millions) | 1 000 000 de cámaras |
| `other` | everything else: 0, 2–999 999, 1.5… | 3 cámaras |
The `many` form is the `other` text with «de» after the number: «{count} de cámaras cargadas», «{count} DE CLAVES EN ESPERA».
## Glossary
| English | Español |
| --------------------------------------------- | ----------------------------------------------------------------- |
| layer / data layer | capa / capa de datos |
| feed / source / data source | fuente / fuente / fuente de datos |
| LIVE (status) / live data | EN VIVO / datos en vivo |
| SIMULATED | SIMULADO |
| stale / degraded / unavailable | desactualizado / degradado / no disponible |
| retry / retrying | reintento / reintentando |
| key (API key) / POWER UP / POWERED UP | clave / ENCENDER / ENCENDIDO |
| Provider Settings | Ajustes de proveedores |
| ON / OFF (state chips) | ACTIVO / INACTIVO (short enough for the layer pill) |
| enable / disable | activar / desactivar |
| globe / map / map source / map stack | globo / mapa / fuente del mapa / conjunto de mapas |
| aircraft, plane / flight / military flights | aeronave / vuelo / vuelos militares |
| vessel, ship | buque |
| satellite / shell (Starlink) / DENSE | satélite / capa / DENSO |
| launch (rocket) / launch site / space mission | lanzamiento / lugar de lanzamiento / misión espacial |
| ascent / replay / payload / stage | ascenso / repetición / carga útil / etapa |
| contact / tracking | contacto / rastreo |
| installation / site | instalación / sitio |
| mapped / observed | mapeado / observado |
| camera / CCTV camera | cámara / cámara CCTV |
| earthquake / active fire | terremoto / incendio activo |
| radio station | emisora |
| road / flow (traffic) | vía / flujo |
| scene / shot / scene recipe | escena / plano / receta de escena |
| briefing / panel / dock | información / panel / dock |
| cockpit | cabina |
| preset / visual preset | preajuste / preajuste visual |
| Scope (circular viewport mask) / keyhole | mirilla / ojo de cerradura |
| feather / fade | difuminado / desvanecimiento |
| detection overlay | superposición de detección |
| pin (Draw tool) | marcador |
| Bloom / Glow (visual effect) | resplandor |
| Cyber (HUD layout, effect) | Ciber |
| heading, course / altitude / ground speed | rumbo / altitud / velocidad respecto al suelo (short: VEL. SUELO) |
| HDG / BRG / CRS / FOV / DEST / SPD / RANGE | HDG / BRG / CRS / FOV / DEST / SPD / RANGE (unchanged) |
| PREV / NEXT | ANT. / SIG. |
| Previous / Next (button text) | Anterior / Siguiente |
| L / R (relative bearing) | I / D |
| SIG / NEWS / LOCAL (briefing tabs) | SIG / NOTICIAS / LOCAL |
| PITCH / HGT (CCTV calibration chips) | INCL. / ALT. |
| NOT AN ALL-CLEAR | NO IMPLICA TODO DESPEJADO |
| click / click to … | haz clic / clic para … |
+65
View File
@@ -0,0 +1,65 @@
# Russian (`ru`)
Catalog: `src/i18n/locales/ru.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
## Style
- Address the user with polite imperatives: «Нажмите», «Включите», «Попробуйте снова».
- Sentence case, not Title Case: "Live Flights" → «Рейсы в реальном времени».
- Quotes inside Russian text are «ёлочки». Write ё where it belongs: её, ещё, всё.
- 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.
## Plurals
A `tn()` entry needs four forms. `Intl.PluralRules('ru')` 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 камеры |
When a word next to a number cannot agree in every form, move the number after a colon instead: «доступно {bikes}», «ответов: {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 | ОНЛАЙН / в реальном времени |
| SIMULATED | СИМУЛЯЦИЯ |
| stale / degraded / unavailable | устарел / с перебоями / недоступен |
| retry | повтор |
| key (API key) / POWER UP | ключ / ЗАПУСК |
| Provider Settings | Настройки провайдеров |
| globe / map / map source | глобус / карта / источник карты |
| aircraft / flight / military flights | самолёт (short HUD: борт) / рейс / военные рейсы |
| vessel, ship / vessels | судно / суда |
| satellite | спутник |
| launch (rocket) / space mission | пуск / космическая миссия |
| contact / tracking | контакт / отслеживание |
| installation, site | объект |
| camera / CCTV camera | камера / камера CCTV |
| earthquake / active fire | землетрясение / активный пожар |
| scene / shot / scene recipe | сцена / кадр / рецепт сцены |
| briefing / panel / dock | брифинг / панель / док |
| preset / visual preset | пресет / визуальный пресет |
| Scope (circular viewport mask) | прицел |
| heading / altitude / ground speed | курс / высота / путевая скорость |
| HDG / BRG / CRS / FOV / DEST / SPD / RANGE | КУРС / ПЕЛ. / ПУТЬ / ОБЗОР / НАЗН. / СКОР / ДАЛЬН. |
| advisory (NHC/CPHC cyclone) | сводка (не «бюллетень»); Advisory center — центр циклона по сводке |
| Street View (Google) | Просмотр улиц (как в русских Google Картах) |
| CONUS (NOAA region code) | CONUS, без перевода |
| WHOT/BHOT, Ironbow (thermal modes, palette) | без перевода |
| PREV / NEXT | ПРЕД. / СЛЕД. |
| Previous / Next (button text) | Назад / Далее |
| anchor / data pack / bundle (scene) | якорь / пакет данных / архив |
| basemap / box (Recent Imagery area) | подложка / область |
| camera move | перелёт |
| SIG / NEWS / LOCAL (briefing tabs) | СИГН. / НОВОСТИ / МЕСТН. |
| compass points (N, NNE, …) | С, ССВ, … |
| L / R (relative bearing) | Л / П |