feat(i18n): ship Spanish

Register es next to ru (selector shows EN | ES | RU) and complete the
catalog against the current English: the 13 messages added since #602,
the "many" plural form Intl.PluralRules('es') selects for exact millions,
and fixes where the English had changed (e.g. "hace hace" in LIVE ·
updated, minutes written as metres). Terminology made consistent (buque,
aeronave, incendio activo); key names stay in English. Meir's wording is
kept everywhere else. Adds docs/translations/es.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-12 14:33:43 +04:00
1 parent 47cfdb823e
commit 70936b58b9
9 files changed
+180 -51

No files matched your search

+4 -1
View File
@@ -1,9 +1,12 @@
# Changelog
- Spanish interface (`es`), translated by @meirm (#602) and reviewed against the
current English.
- Interface localization, starting with Russian. UI text is marked with
`t()`, `tn()` (plurals) and `msg()` in code and `data-i18n*` in markup, keyed
by the English text itself; each language is one flat JSON catalog
(`src/i18n/locales/ru.json`). The language comes from `?lang=`, the EN/RU
(`src/i18n/locales/ru.json`). The language comes from `?lang=`, the language
switch in the Visual Presets dock, then the browser language; English stays
the default and the fallback. `npm run i18n`, also run by `npm test`, fails on
missing, unused or broken translations. Built on the i18n foundation by
+2 -2
View File
@@ -3011,7 +3011,7 @@ test:track` 43 tracking invariants · headless QA harnesses under
### Interface localization (October 2026)
Shipped locales: **en** (source and fallback) and **ru**. The code lives in
Shipped locales: **en** (source and fallback), **es** and **ru**. The code lives in
`src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow:
[`docs/TRANSLATORS.md`](./TRANSLATORS.md).
@@ -3022,7 +3022,7 @@ Shipped locales: **en** (source and fallback) and **ru**. The code lives in
value). English has no catalog; every other locale is
`src/i18n/locales/<code>.json`, and a missing entry falls back to English.
- **Locale choice.** `?lang=` (one-shot) → stored choice (`gev:locale:v1`) →
`navigator.languages` → English. The EN/RU switch in the Visual Presets dock
`navigator.languages` → English. The language switch in the Visual Presets dock
stores the choice and reloads, so nothing re-renders live.
- **Gate.** `npm run i18n` (and `src/i18n/catalog.test.mjs` under `npm test`)
extracts every message and fails on missing, unused, placeholder-mismatched or
+1 -1
View File
@@ -56,7 +56,7 @@ npm run i18n -- --prune # drop entries whose English no longer exists
Changing the English changes the key, so the old translation goes unused and the new text shows as missing. Add the translation to `src/i18n/locales/ru.json` (or ask a translator), prune, and run `npm test`, which runs the same check and fails until every language is complete.
To see a language, open the app with `?lang=ru`, or use the EN/RU switch in the Visual Presets dock.
To see a language, open the app with `?lang=ru`, or use the language switch in the Visual Presets dock.
## Where things live
+3 -3
View File
@@ -1,6 +1,6 @@
# Translating God's Eye View
Shipped languages: **English** (source and fallback) and **Russian** (`ru`).
Shipped languages: **English** (source and fallback), **Spanish** (`es`) and **Russian** (`ru`).
## 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 EN/RU switch in the Visual Presets dock, then the browser languages, then English. Switching reloads the page.
The page picks its language from `?lang=`, then the choice stored by the language switch in the Visual Presets dock, then the browser languages, then English. Switching reloads the page.
## The check
@@ -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`: [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`: [Spanish](./translations/es.md), [Russian](./translations/ru.md). Read it before translating, and add a term there when you settle on one.
+70
View File
@@ -0,0 +1,70 @@
# 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) | ACTIVADO / DESACTIVADO |
| 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 |
| 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 |
| NOT AN ALL-CLEAR | NO IMPLICA TODO DESPEJADO |
| click / click to … | haz clic / clic para … |
+31
View File
@@ -421,6 +421,7 @@
"src/localRequestGate.mjs",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -445,6 +446,7 @@
"src/data/layerSnapshot.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -459,6 +461,7 @@
"src/app/stateChannel.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -473,6 +476,7 @@
"src/ui/radioTunerModel.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -489,6 +493,7 @@
"src/ui/cctvPresentation.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -510,6 +515,7 @@
"src/ui/panelRailGeometry.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -541,6 +547,7 @@
"src/ui/panelRailGeometry.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -553,6 +560,7 @@
"src/ui/scenePresentation.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -695,6 +703,7 @@
"src/data/cameraArrival.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -804,6 +813,7 @@
"src/data/cameraArrival.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -861,6 +871,7 @@
"src/data/modelEligibility.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -903,6 +914,7 @@
"src/data/modelEligibility.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -959,6 +971,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -1008,6 +1021,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -1037,6 +1051,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium", "satellite.js"]
@@ -1067,6 +1082,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -1105,6 +1121,7 @@
"src/data/infrastructureOverlayEntry.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
@@ -1135,6 +1152,7 @@
"src/ui/panelRailGeometry.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -1179,6 +1197,7 @@
"src/layers/traffic/roadModes.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium", "pbf", "@mapbox/vector-tile"]
@@ -1205,6 +1224,7 @@
"src/data/cameraSensitivity.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -1249,6 +1269,7 @@
"src/layers/cctv/sourcePolicy.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["hls.js", "cesium"]
@@ -1347,6 +1368,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -1367,6 +1389,7 @@
"src/data/inputOwnership.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -1419,6 +1442,7 @@
"src/maps/imageryHost.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -1469,6 +1493,7 @@
"src/voice/pointerReticle.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -2276,6 +2301,7 @@
"src/app/embed.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": [
@@ -2801,6 +2827,7 @@
"src/sources/infrastructureData.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": [
@@ -2906,6 +2933,7 @@
"src/ui/uiLifetime.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -2937,6 +2965,7 @@
"src/ui/visualSettings.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": ["cesium"]
@@ -2960,6 +2989,7 @@
"src/ui/uiLifetime.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
@@ -3086,6 +3116,7 @@
"src/voice/pointerReticle.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"src/i18n/locales/es.json",
"src/i18n/locales/ru.json"
],
"external": []
+2 -1
View File
@@ -15,9 +15,10 @@ import {
normalizeLocale,
writeStoredLocale,
} from './locale.js';
import es from './locales/es.json' with { type: 'json' };
import ru from './locales/ru.json' with { type: 'json' };
const CATALOGS = Object.freeze({ ru });
const CATALOGS = Object.freeze({ es, ru });
let currentLocale = DEFAULT_LOCALE;
+6 -2
View File
@@ -11,10 +11,14 @@ export const DEFAULT_LOCALE = 'en';
* Shipped locales, in selector order. Adding one: locales/<code>.json, an
* import in index.js, and an entry here (see docs/TRANSLATORS.md).
*/
export const LOCALES = Object.freeze(['en', 'ru']);
export const LOCALES = Object.freeze(['en', 'es', 'ru']);
/** Each locale named in its own language, for the selector. */
export const LOCALE_NAMES = Object.freeze({ en: 'English', ru: 'Русский' });
export const LOCALE_NAMES = Object.freeze({
en: 'English',
es: 'Español',
ru: 'Русский',
});
/** Right-to-left locales. */
const RTL_LOCALES = Object.freeze([]);
+61 -41
View File
@@ -123,14 +123,14 @@
"LIVE TRACK · COURSE ALIGNED": "RASTREO EN VIVO · RUMBO ALINEADO",
"Cockpit vision style": "Estilo de visión de la cabina",
"Current aircraft heading": "Rumbo actual de la aeronave",
"GROUND SPEED · KTS": "VEL. SUELO · KTS",
"GROUND SPEED · KTS": "VEL. SUELO · KN",
"ALTITUDE · FT": "ALTITUD · FT",
"PREV": "ANT",
"PREV": "ANT.",
"Previous cockpit vision style": "Estilo de visión de cabina anterior",
"Previous vision style": "Estilo de visión anterior",
"Current cockpit vision style: NORMAL. Activate for next style.": "Estilo de visión de cabina actual: NORMAL. Actívalo para pasar al siguiente estilo.",
"Current style: NORMAL — click for next": "Estilo actual: NORMAL — haz clic para el siguiente",
"NEXT": "SIG",
"NEXT": "SIG.",
"Next cockpit vision style": "Siguiente estilo de visión de cabina",
"Next vision style": "Siguiente estilo de visión",
"View switcher": "Selector de vistas",
@@ -180,7 +180,7 @@
"Expand panel": "Expandir panel",
"Context mode": "Modo de contexto",
"CONTACTS": "CONTACTOS",
"Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Ronda por los contactos más cercanos del tipo que elijas — aeronaves, embarcaciones, instalaciones. Los satélites se rastrean por separado.",
"Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Recorre los contactos más cercanos del tipo que elijas — aeronaves, buques, instalaciones. Los satélites se rastrean por separado.",
"SPACE MISSIONS": "MISIONES ESPACIALES",
"SELECT CONTEXT": "SELECCIONAR CONTEXTO",
"Contact Context actions": "Acciones de contexto de contacto",
@@ -243,6 +243,7 @@
"DISTANCE UNKNOWN": "DISTANCIA DESCONOCIDA",
"{count} INPUTS UNKNOWN": {
"one": "{count} DATO DESCONOCIDO",
"many": "{count} DE DATOS DESCONOCIDOS",
"other": "{count} DATOS DESCONOCIDOS"
},
"SOURCE STATUS UNAVAILABLE": "ESTADO DE FUENTE NO DISPONIBLE",
@@ -259,6 +260,7 @@
"CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR": "CONTACTO PERDIDO · ÚLTIMA LECTURA CONOCIDA · NO IMPLICA TODO DESPEJADO",
"{count} INPUTS UNKNOWN · NOT AN ALL-CLEAR": {
"one": "{count} DATO DESCONOCIDO · NO IMPLICA TODO DESPEJADO",
"many": "{count} DE DATOS DESCONOCIDOS · NO IMPLICA TODO DESPEJADO",
"other": "{count} DATOS DESCONOCIDOS · NO IMPLICA TODO DESPEJADO"
},
"AVAILABLE INPUTS CURRENT · NOT AN ALL-CLEAR": "DATOS DISPONIBLES ACTUALIZADOS · NO IMPLICA TODO DESPEJADO",
@@ -288,7 +290,7 @@
"{domain} · {age}": "{domain} · {age}",
"{source} · LOCATION QUERY": "{source} · CONSULTA POR UBICACIÓN",
"TIME UNKNOWN": "HORA DESCONOCIDA",
"{count}M AGO": "HACE {count} M",
"{count}M AGO": "HACE {count} MIN",
"{count}H AGO": "HACE {count} H",
"{count}D AGO": "HACE {count} D",
"DIR UNKNOWN": "DIR DESCONOCIDA",
@@ -304,7 +306,7 @@
"Selected data layers could not be cleared": "No se pudieron limpiar las capas de datos seleccionadas",
"ACQUIRING": "OBTENIENDO",
"entity": "entidad",
"SHARED {subject}": "{subject} COMPARTIDA",
"SHARED {subject}": "{subject} COMPARTIDO",
"Shared {subject} follow expired": "El seguimiento de {subject} compartido ha caducado",
"Shared {subject} could not be restored — feed unavailable": "No se pudo restaurar {subject} compartido — fuente no disponible",
"Shared {subject} is unavailable": "{subject} compartido no está disponible",
@@ -318,7 +320,7 @@
"Zoom in to search mapped installations": "Haz zoom para buscar instalaciones mapeadas",
"Nearby installations refreshed": "Instalaciones cercanas actualizadas",
"That layer is unavailable in the current Context mode": "Esa capa no está disponible en el modo de contexto actual",
"start": "iniciar",
"start": "iniciarse",
"stop": "detenerse",
"{layerId} could not {action} cleanly": "{layerId} no pudo {action} correctamente",
"Radio could not {action} cleanly": "Radio no pudo {action} correctamente",
@@ -327,7 +329,7 @@
"CCTV calibration reset": "Calibración de CCTV restablecida",
"CCTV layer unavailable": "Capa de CCTV no disponible",
"MAP": "MAPA",
"CONTACTS — nearest planes · vessels · sites": "CONTACTOS — aeronaves · embarcaciones · sitios más cercanos",
"CONTACTS — nearest planes · vessels · sites": "CONTACTOS — aeronaves · buques · sitios más cercanos",
"SPACE MISSIONS — launches & orbital assets": "MISIONES ESPACIALES — lanzamientos y activos orbitales",
"Space Missions cancellation could not restore the previous layer state": "La cancelación de Misiones espaciales no pudo restaurar el estado previo de las capas",
"Collapse Cockpit display options": "Contraer las opciones de visualización de la cabina",
@@ -348,7 +350,7 @@
"SYNCING DIRECTORY": "SINCRONIZANDO DIRECTORIO",
"SYNCING": "SINCRONIZANDO",
"station": "emisora",
"Ready": "Lista",
"Ready": "Listo",
"VIEWSHED ON": "ÁREA VISIBLE ACTIVADA",
"FRAME · LOADING": "FOTOGRAMA · CARGANDO",
"FRAME · UNAVAILABLE": "FOTOGRAMA · NO DISPONIBLE",
@@ -358,13 +360,15 @@
"CURATED": "CURADA",
"RAW PRIOR": "PREVIA SIN PROCESAR",
"MONITOR": "MONITOR",
"{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}": "{city} · HDG {heading} · FOV {fov} · ALCANCE {range}m · {projection}{calBadge} · {provider}{status}",
"{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}": "{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}",
"{count} cameras loaded · click a camera to activate": {
"one": "{count} cámara cargada · haz clic en una cámara para activarla",
"many": "{count} de cámaras cargadas · haz clic en una cámara para activarla",
"other": "{count} cámaras cargadas · haz clic en una cámara para activarla"
},
"{count} cameras loaded · enable CCTV to activate": {
"one": "{count} cámara cargada · activa CCTV para activarla",
"many": "{count} de cámaras cargadas · activa CCTV para activarlas",
"other": "{count} cámaras cargadas · activa CCTV para activarlas"
},
"No summary available.": "No hay resumen disponible.",
@@ -373,14 +377,14 @@
"Detection overlay: off": "Superposición de detección: desactivada",
"SPARSE": "DISPERSA",
"BALANCED": "EQUILIBRADA",
"DENSE": "DENSA",
"DENSE": "DENSO",
"Resetting to full globe view": "Restableciendo la vista de globo completo",
"Resetting cockpit to full globe view": "Restableciendo la cabina a la vista de globo completo",
"ORBIT": "ÓRBITA",
"SUMMARY": "RESUMEN",
"Awaiting telemetry...": "Esperando telemetría...",
"REC": "REC",
"Scope edge feather": "Suavizado del borde de la mirilla",
"Scope edge feather": "Difuminado del borde de la mirilla",
"Bloom intensity": "Intensidad del bloom",
"Sharpen intensity": "Intensidad de la nitidez",
"Search location by name or coordinates": "Buscar ubicación por nombre o coordenadas",
@@ -391,10 +395,12 @@
"No selected data layers": "No hay capas de datos seleccionadas",
"Cleared {count} data layers": {
"one": "Se limpió {count} capa de datos",
"many": "Se limpiaron {count} de capas de datos",
"other": "Se limpiaron {count} capas de datos"
},
"{count} data layers could not be cleared": {
"one": "No se pudo limpiar {count} capa de datos",
"many": "No se pudieron limpiar {count} de capas de datos",
"other": "No se pudieron limpiar {count} capas de datos"
},
"Visible map targets": "Objetivos visibles en el mapa",
@@ -406,11 +412,11 @@
"CCTV camera": "Cámara CCTV",
"FOCUS": "ENFOCAR",
"COVERAGE OFF": "COBERTURA DESACTIVADA",
"AUTO HOP OFF": "AUTO HOP DESACTIVADO",
"AUTO HOP OFF": "AUTOSALTO DESACTIVADO",
"PROJECTION ON": "PROYECCIÓN ACTIVADA",
"CALIBRATION": "CALIBRACIÓN",
"ADJUST": "AJUSTAR",
"Drag the camera in the world: rings rotate, arrows move, handles set range/FOV": "Arrastra la cámara en el mundo: los anillos rotan, las flechas se mueven, los tiradores ajustan alcance/FOV",
"Drag the camera in the world: rings rotate, arrows move, handles set range/FOV": "Arrastra la cámara en el mundo: los anillos la giran, las flechas la mueven, los tiradores ajustan alcance/FOV",
"Camera pose — click a value to type": "Pose de la cámara — clic en un valor para escribirlo",
"Heading (compass °) — click to type": "Rumbo (° brújula) — clic para escribir",
"Pitch (° up/down) — click to type": "Inclinación (° arriba/abajo) — clic para escribir",
@@ -468,8 +474,8 @@
"FIRMS Active Fires": "Incendios activos FIRMS",
"just now": "justo ahora",
"{count}s ago": "hace {count}s",
"{count}m ago": "hace {count}m",
"{count}h ago": "hace {count}h",
"{count}m ago": "hace {count} min",
"{count}h ago": "hace {count} h",
"never": "nunca",
"loading...": "cargando...",
"UNCERTAIN · {source} · lifecycle state requires reconciliation": "INCIERTO · {source} · el estado del ciclo de vida requiere reconciliación",
@@ -499,7 +505,7 @@
"TUG": "REMOLCADOR",
"PORT TENDER": "AUXILIAR DE PUERTO",
"ANTI-POLLUTION": "ANTICONTAMINACIÓN",
"LAW ENFORCE": "APLICACIÓN DE LA LEY",
"LAW ENFORCE": "POLICÍA",
"MEDICAL": "MÉDICO",
"HIGH-SPEED": "ALTA VELOCIDAD",
"PASSENGER": "PASAJEROS",
@@ -515,7 +521,7 @@
"awaiting first AIS position…": "esperando la primera posición AIS…",
"CCTV ON": "CCTV ACTIVADO",
"COVERAGE ON": "COBERTURA ACTIVADA",
"AUTO HOP ON": "AUTO HOP ACTIVADO",
"AUTO HOP ON": "AUTOSALTO ACTIVADO",
"PROJECTION OFF": "PROYECCIÓN DESACTIVADA",
"ADJUST ON": "AJUSTE ACTIVADO",
"{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE": "{count} CÁMARAS EN ESPERA · NINGUNA CÁMARA SELECCIONADA · CLIC EN UNA CÁMARA PARA ACTIVARLA",
@@ -524,21 +530,21 @@
"HDG {value}°": "HDG {value}°",
"HDG {value}° (ESTIMATED)": "HDG {value}° (ESTIMADO)",
"FOV {value}°": "FOV {value}°",
"COVERAGE {value}km²": "COBERTURA {value}km²",
"OVERLAP {count} cams": "SOLAPE {count} cáms",
"COVERAGE {value}km²": "COBERTURA {value} KM²",
"OVERLAP {count} cams": "SOLAPE {count} cám.",
"ISOLATED VIEW": "VISTA AISLADA",
"PROJ MONITOR": "PROY MONITOR",
"PROJ OFF": "PROY DESACT",
"VIEWSHED": "ÁREA VISIBLE",
"CAL {value}": "CAL {value}",
"SRC {value}": "SRC {value}",
"{value} CONTEXT": "{value} CONTEXTO",
"{value} CONTEXT": "CONTEXTO {value}",
"News": "Noticias",
"Talk": "Debate",
"Talk": "Tertulia",
"Weather / Emergency": "Clima / Emergencias",
"Public Safety": "Seguridad pública",
"Aviation / Marine": "Aviación / Marítima",
"Traffic / Transit": "Tráfico / Tránsito",
"Aviation / Marine": "Aviación / Marina",
"Traffic / Transit": "Tráfico / Transporte",
"Music": "Música",
"Other": "Otros",
"NO STATION AVAILABLE": "NINGUNA EMISORA DISPONIBLE",
@@ -555,7 +561,7 @@
"Connecting directly to broadcaster…": "Conectando directamente con la emisora…",
"Buffering broadcaster stream…": "Almacenando en búfer el flujo de la emisora…",
"Playing {station}": "Reproduciendo {station}",
"Paused {station}": "En pausa {station}",
"Paused {station}": "{station} en pausa",
"Broadcaster stream unavailable": "Flujo de la emisora no disponible",
" · degraded directory": " · directorio degradado",
" · stale directory": " · directorio desactualizado",
@@ -640,19 +646,19 @@
"Showing the full Starlink shell — click for the core catalog only": "Mostrando la capa completa de Starlink — clic para ver solo el catálogo principal",
"STATION": "ESTACIÓN",
"Crewed stations and their visiting vehicles": "Estaciones tripuladas y sus vehículos de visita",
"NAV": "NAVEGACIÓN",
"NAV": "NAV",
"GNSS navigation — GPS, GLONASS, Galileo": "Navegación GNSS — GPS, GLONASS, Galileo",
"GEO": "GEO",
"Geostationary belt — comms and weather, fixed over the equator": "Cinturón geoestacionario — comunicaciones y meteorología, fijos sobre el ecuador",
"VISUAL": "VISUAL",
"Brightest naked-eye objects — CelesTrak visual group": "Objetos más brillantes a simple vista — grupo visual de CelesTrak",
"COMMS": "COMUNICACIONES",
"COMMS": "TELECOM",
"Broadband constellation shell — shown only in DENSE mode": "Capa de constelación de banda ancha — visible solo en modo DENSO",
"refreshing...": "actualizando...",
"KEY REQUIRED": "CLAVE REQUERIDA",
"STALE · cached {age}": "DESACTUALIZADO · en caché hace {age}",
"LIVE · updated {age}": "EN VIVO · actualizado hace {age}",
"<1m ago": "hace <1m",
"LIVE · updated {age}": "EN VIVO · actualizado {age}",
"<1m ago": "hace <1 min",
"FIRE · {frp} MW": "INCENDIO · {frp} MW",
"▲ {frp} MW": "▲ {frp} MW",
"FIRE": "INCENDIO",
@@ -700,7 +706,7 @@
"Military flights": "Vuelos militares",
"AIS vessels": "Buques AIS",
"Mapped installations": "Instalaciones mapeadas",
"CURRENT VIEWPORT ONLY": "SOLO LA VENTANA VISIBLE ACTUAL",
"CURRENT VIEWPORT ONLY": "SOLO LA VISTA ACTUAL",
"BRG {value}°": "BRG {value}°",
"CRS {value}°": "CRS {value}°",
"feed unavailable": "fuente no disponible",
@@ -719,7 +725,7 @@
"SNOW SHOWERS": "CHUBASCOS DE NIEVE",
"THUNDERSTORM": "TORMENTA",
"MIXED CONDITIONS": "CONDICIONES MIXTAS",
"TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB VISTAS PREVIAS · INTRO / ESPACIO SELECCIONA",
"TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB VISTA PREVIA · ENTER / SPACE SELECCIONA",
"awaiting usable AIS positions…": "esperando posiciones AIS utilizables…",
"awaiting first AIS message…": "esperando el primer mensaje AIS…",
"EST. DOWNRANGE": "DIST. ESTIMADA",
@@ -733,12 +739,12 @@
"SAVE KEYS": "GUARDAR CLAVES",
"Saving…": "Guardando…",
"It feels like a forbidden cockpit—then you realize the sources are public and the data is real.": "Parece una cabina prohibida—hasta que descubres que las fuentes son públicas y los datos son reales.",
"Aircraft, vessels and nearby intelligence": "Aviones, barcos e inteligencia cercana",
"Aircraft, vessels and nearby intelligence": "Aeronaves, buques e inteligencia cercana",
"Launches, spacecraft and orbital context": "Lanzamientos, naves y contexto orbital",
"EXPLORE MANUALLY": "EXPLORAR MANUALMENTE",
"Begin with a clean globe": "Empieza con un globo limpio",
"ESC to dismiss": "ESC para cerrar",
"Tip: the GEV MIC button in the dock lets you talk to the map.": "Consejo: el botón MIC de GEV en el dock te permite hablar con el mapa.",
"Tip: the GEV MIC button in the dock lets you talk to the map.": "Consejo: el botón GEV MIC del dock te permite hablar con el mapa.",
"Close key setup": "Cerrar la configuración de claves",
"The globe already flies keyless. Every key below switches on another real feed — paste one and it's saved into this app's local configuration, then the server restarts itself. Server-side keys stay on this machine; Google Maps and Cesium ion run in the browser and must be provider-restricted. Keys you configured elsewhere are shown but never touched.": "El globo ya vuela sin claves. Cada clave de abajo activa otra fuente real — pega una y queda guardada en la configuración local de esta app, y luego el servidor se reinicia solo. Las claves del servidor se quedan en esta máquina; Google Maps y Cesium ion funcionan en el navegador y sus claves deben estar restringidas por el proveedor. Las claves que configuraste en otro lugar se muestran, pero nunca se modifican.",
"ESC to close": "ESC para cerrar",
@@ -759,12 +765,13 @@
"Working…": "Trabajando…",
"Could not open that mission{detail}. Retry or explore manually.": "No se pudo abrir esa misión{detail}. Reintenta o explora manualmente.",
"This browser is blocking storage, so that could not be saved.": "Este navegador bloquea el almacenamiento, así que no se pudo guardar.",
"Live earthquakes and active fires, from USGS and NASA": "Terremotos en vivo y fuegos activos, de USGS y NASA",
"Live earthquakes and active fires, from USGS and NASA": "Terremotos en vivo e incendios activos, de USGS y NASA",
"ENVIRONMENTAL": "MEDIO AMBIENTE",
"EARTH WATCH": "VIGILANCIA TERRESTRE",
"ACTIVE EVENTS": "EVENTOS ACTIVOS",
"POWER UP · {count} KEYS WAITING": {
"one": "ENCENDER · {count} CLAVE EN ESPERA",
"many": "ENCENDER · {count} DE CLAVES EN ESPERA",
"other": "ENCENDER · {count} CLAVES EN ESPERA"
},
"POWERED UP": "ENCENDIDO",
@@ -794,13 +801,13 @@
"VOICE UNAVAILABLE": "VOZ NO DISPONIBLE",
"WebRTC microphone support unavailable": "Compatibilidad con micrófono WebRTC no disponible",
"Requesting microphone": "Solicitando micrófono",
"Hold Space to talk": "Mantén Espacio para hablar",
"Release Space to send": "Suelta la barra espaciadora para enviar",
"Hold Space to talk": "Mantén pulsado Space para hablar",
"Release Space to send": "Suelta Space para enviar",
"Ask or command": "Pregunta o ordena",
"Voice off": "Voz desactivada",
"Running command": "Ejecutando comando",
"Radio did not start": "La radio no se inició",
"Hold Space to speak · tap Space to activate focused controls": "Mantén presionada la barra espaciadora para hablar · púlsala brevemente para activar los controles enfocados",
"Hold Space to speak · tap Space to activate focused controls": "Mantén pulsado Space para hablar · pulsa Space brevemente para activar el control enfocado",
"Voice session could not be started.": "No se pudo iniciar la sesión de voz.",
"VOICE SYSTEM ERROR": "ERROR DEL SISTEMA DE VOZ",
"DISMISS": "CERRAR",
@@ -810,7 +817,7 @@
"{tier} applies next session": "{tier} se aplica en la próxima sesión",
"Voice model tier — applies next session": "Nivel del modelo de voz — se aplica en la próxima sesión",
"Estimated session cost": "Costo estimado de la sesión",
"Voice control — activate to toggle voice; hold Space to speak": "Control de voz — actívalo para conectar o cortar la voz; mantén presionada la barra espaciadora para hablar",
"Voice control — activate to toggle voice; hold Space to speak": "Control de voz — actívalo para conectar o cortar la voz; mantén pulsado Space para hablar",
"Cannot capture shot: camera not ready": "No se puede capturar el plano: la cámara no está lista",
"Shot {n}": "Plano {n}",
"Captured: {scene} / {shot}": "Capturado: {scene} / {shot}",
@@ -829,7 +836,7 @@
"Needs {envVars} — add it in Provider Settings": "Necesita {envVars} — añádela en Ajustes de proveedores",
"The photorealistic 3D planet + place search": "El planeta 3D fotorrealista + búsqueda de lugares",
"Places context + Street View fallback; optional separate key": "Contexto de Places + respaldo de Street View; clave separada opcional",
"Live ships, worldwide": "Barcos en vivo, en todo el mundo",
"Live ships, worldwide": "Buques en vivo, en todo el mundo",
"Live active-fire detections": "Detecciones de incendios activos en vivo",
"Real live traffic (keyless runs a simulation)": "Tráfico real en vivo (sin clave usa una simulación)",
"Bing imagery map stacks + world terrain": "Conjuntos de mapas Bing imagery + terreno mundial",
@@ -838,6 +845,19 @@
"Scene action failed": "La acción de escena falló",
"Project exported": "Proyecto exportado",
"Next session: {pendingId} — this session stays on {modelId}": "Sesión siguiente: {pendingId} — esta sesión sigue en {modelId}",
"Voice model: {pendingId} — click to switch to {target}; applies next session": "Modelo de voz: {pendingId} — toca para cambiar a {target}; se aplica en la próxima sesión",
"Estimated session cost on {modelId} — {responses} response(s). Warns at {warn}, ends the session at {cap}.": "Coste de sesión estimado en {modelId} — {responses} respuesta(s). Avisa a {warn} y cierra la sesión a {cap}."
"Voice model: {pendingId} — click to switch to {target}; applies next session": "Modelo de voz: {pendingId} — haz clic para cambiar a {target}; se aplica en la próxima sesión",
"Estimated session cost on {modelId} — {responses} response(s). Warns at {warn}, ends the session at {cap}.": "Costo de sesión estimado en {modelId} — {responses} respuesta(s). Avisa a {warn} y cierra la sesión a {cap}.",
"Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "Imágenes y cobertura a nivel de calle en la capa Street Level. Gratis: registra una app en el panel de desarrolladores de Mapillary y pega su Client Token",
"Voice control — API key or ChatGPT OAuth": "Control de voz — clave de API u OAuth de ChatGPT",
"WITHIN {km} KM": "A MENOS DE {km} KM",
"Syncing flow · Roads: {source}": "Sincronizando flujo · Vías: {source}",
"LIVE · Roads: {source} · Roads without flow hidden": "EN VIVO · Vías: {source} · Vías sin flujo ocultas",
"LIVE · Roads: {source} · No flow roads in view": "EN VIVO · Vías: {source} · Sin vías con flujo a la vista",
"LIVE · Roads: {source} · Flow {percent}%": "EN VIVO · Vías: {source} · Flujo {percent}%",
"SIMULATED · Roads: {source} · Flow {percent}%": "SIMULADO · Vías: {source} · Flujo {percent}%",
"Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated": "Sincronizando flujo · Vías: OpenStreetMap · Flujo: TomTom · Sin coincidencia: simulado",
"LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov · Unmatched: simulated": "EN VIVO · Vías: OpenStreetMap · Flujo: TomTom · {percent}% cob. · Sin coincidencia: simulado",
"LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov": "EN VIVO · Vías: OpenStreetMap · Flujo: TomTom · {percent}% cob.",
"SIMULATED · Roads: OpenStreetMap · Flow: TomTom (no matches)": "SIMULADO · Vías: OpenStreetMap · Flujo: TomTom (sin coincidencias)",
"Data source unavailable": "Fuente de datos no disponible"
}