diff --git a/CHANGELOG.md b/CHANGELOG.md index b104e6eb4..08c03cc5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 3d5842f67..0eadf2ac5 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -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/.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 diff --git a/docs/I18N.md b/docs/I18N.md index b878ae66e..21b2697b0 100644 --- a/docs/I18N.md +++ b/docs/I18N.md @@ -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 diff --git a/docs/TRANSLATORS.md b/docs/TRANSLATORS.md index 0cc99fa2e..688611401 100644 --- a/docs/TRANSLATORS.md +++ b/docs/TRANSLATORS.md @@ -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/.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/.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/.md`: [Spanish](./translations/es.md), [Russian](./translations/ru.md). Read it before translating, and add a term there when you settle on one. diff --git a/docs/translations/es.md b/docs/translations/es.md new file mode 100644 index 000000000..e2bb3c027 --- /dev/null +++ b/docs/translations/es.md @@ -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 … | diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 37488c4c6..99ebe1d1f 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -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": [] diff --git a/src/i18n/index.js b/src/i18n/index.js index 4578cfcc9..6ccd2eef1 100644 --- a/src/i18n/index.js +++ b/src/i18n/index.js @@ -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; diff --git a/src/i18n/locale.js b/src/i18n/locale.js index e2489e032..50cf061ce 100644 --- a/src/i18n/locale.js +++ b/src/i18n/locale.js @@ -11,10 +11,14 @@ export const DEFAULT_LOCALE = 'en'; * Shipped locales, in selector order. Adding one: locales/.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([]); diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 54c5dedcf..5ccdd163b 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -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" }