mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(i18n): Spanish review fixes; report translations left in English
A screenshot review of the Spanish UI found: - Text left in English. Catalog entries copied from English (Bloom, Cyber, Pin, NEWS, SITE, ON/OFF, RANGE, PITCH, HGT…) and the radio music genres, which never went through t(). The genres are translated in es and ru now. - Wording: gender agreement, calques of the English, hints that name buttons that do not exist, and one concept worded two ways. - Layout: the DESACTIVADO pill squeezed layer names until words broke mid-word; the scene and camera selects showed one word; a collapsed rail title pushed its button out of the card; the image slot line and a comparison readout were cut or split. The layout fixes are scoped to languages other than English, so English stays pixel-identical. Visual-style slider values now use the locale's decimal separator (0,85). npm run i18n now reports a translation that is identical to the English, unless src/i18n/same-as-english.json lists it for that language (codes, product names, words spelled the same). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9f1e409193
commit
7db0abc396
16 files changed
+407
-111
No files matched your search
+10
-7
@@ -58,6 +58,8 @@ Changing the English changes the key, so the old translation goes unused and the
|
||||
|
||||
To see a language, open the app with `?lang=ru`, or use the language switch in the Visual Presets dock.
|
||||
|
||||
A translation that is exactly the English ("Bloom" → "Bloom") is reported as still English. When it is right on purpose, such as a code (HDG, CRT), a product name or a word spelled the same in that language, list it under the language in `src/i18n/same-as-english.json`.
|
||||
|
||||
## Text the check reports as unmarked
|
||||
|
||||
`npm run i18n` also lists UI text that is not marked at all, and fails while there is any; `npm test` runs the same check:
|
||||
@@ -77,10 +79,11 @@ Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowerca
|
||||
|
||||
## Where things live
|
||||
|
||||
| File | What it holds |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------ |
|
||||
| `src/i18n/index.js` | `t`, `tn`, `msg`, the Intl formatters, markup translation, switching language |
|
||||
| `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 |
|
||||
| `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` |
|
||||
| File | What it holds |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `src/i18n/index.js` | `t`, `tn`, `msg`, the Intl formatters, markup translation, switching language |
|
||||
| `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` |
|
||||
+1
-1
@@ -32,7 +32,7 @@ npm run i18n -- --missing ru # missing ru entries as JSON, ready to transla
|
||||
npm run i18n -- --prune # drop entries the code no longer uses
|
||||
```
|
||||
|
||||
`npm test` runs the same check (`src/i18n/catalog.test.mjs`). It fails when a translation is missing, when an entry is no longer used, when placeholders differ from the English, or when a plural entry lacks a form.
|
||||
`npm test` runs the same check (`src/i18n/catalog.test.mjs`). It fails when a translation is missing, when an entry is no longer used, when placeholders differ from the English, when a plural entry lacks a form, or when a translation is still the English text. If an entry is right as it is in English (a code such as HDG, a product name, a word spelled the same in your language), 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.
|
||||
|
||||
|
||||
+29
-1
@@ -4,6 +4,10 @@
|
||||
// npm run i18n -- --prune also drop entries the code no longer uses
|
||||
// npm run i18n -- --missing ru print what ru lacks, as JSON to translate
|
||||
//
|
||||
// A translation identical to the English is reported too, unless it is
|
||||
// listed for that locale in src/i18n/same-as-english.json (codes, product
|
||||
// names, words spelled the same in both languages).
|
||||
//
|
||||
// Messages are the English literals passed to t(), tn() and msg() in src/,
|
||||
// plus the markup opted in with data-i18n* in src/ui/templates and index.html.
|
||||
// It also lists UI text that is not marked at all (see findUnmarked).
|
||||
@@ -14,6 +18,7 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const LOCALES_DIR = path.join(ROOT, 'src/i18n/locales');
|
||||
const SAME_AS_ENGLISH = path.join(ROOT, 'src/i18n/same-as-english.json');
|
||||
|
||||
const STRING = String.raw`'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"|\`(?:\\.|[^\`\\$])*\``;
|
||||
const CALL = new RegExp(
|
||||
@@ -424,11 +429,26 @@ export function shippedLocales() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Messages a locale keeps exactly as in English, on purpose. */
|
||||
export function readSameAsEnglish(locale) {
|
||||
const lists = JSON.parse(readFileSync(SAME_AS_ENGLISH, 'utf8'));
|
||||
return new Set(lists[locale] || []);
|
||||
}
|
||||
|
||||
/** The English a message shows: its text without the "context|" prefix. */
|
||||
const englishOf = (text) => text.slice(text.indexOf('|') + 1);
|
||||
|
||||
/**
|
||||
* Compare one catalog with the extracted messages.
|
||||
* @param {Set<string>} [sameAsEnglish] Messages allowed to read as English.
|
||||
* @returns {{missing: string[], stale: string[], errors: string[]}}
|
||||
*/
|
||||
export function checkCatalog(locale, catalog, messages) {
|
||||
export function checkCatalog(
|
||||
locale,
|
||||
catalog,
|
||||
messages,
|
||||
sameAsEnglish = readSameAsEnglish(locale),
|
||||
) {
|
||||
const categories = new Intl.PluralRules(locale).resolvedOptions()
|
||||
.pluralCategories;
|
||||
const missing = [];
|
||||
@@ -456,6 +476,14 @@ export function checkCatalog(locale, catalog, messages) {
|
||||
errors.push(`"${text}" must be a string`);
|
||||
} else if (placeholders(entry) !== placeholders(text)) {
|
||||
errors.push(`"${text}" placeholders differ: ${entry}`);
|
||||
} else if (
|
||||
entry === englishOf(text) &&
|
||||
/\p{L}{2}/u.test(entry.replace(/\{[^}]*\}/g, '')) &&
|
||||
!sameAsEnglish.has(text)
|
||||
) {
|
||||
errors.push(
|
||||
`"${text}" is still English: translate it, or list it under "${locale}" in src/i18n/same-as-english.json`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const stale = Object.keys(catalog).filter((text) => !messages.has(text));
|
||||
|
||||
@@ -66,6 +66,28 @@ test('checkCatalog reports missing, stale, placeholder and plural problems', ()
|
||||
assert.equal(result.errors.length, 2);
|
||||
});
|
||||
|
||||
test('checkCatalog reports a translation left in English unless it is allowed', () => {
|
||||
const fixture = new Map([
|
||||
['Bloom', { plural: false }],
|
||||
['HDG', { plural: false }],
|
||||
['voice|READY', { plural: false }],
|
||||
['{state} · {source}', { plural: false }],
|
||||
]);
|
||||
const catalog = {
|
||||
Bloom: 'Bloom',
|
||||
HDG: 'HDG',
|
||||
'voice|READY': 'READY',
|
||||
'{state} · {source}': '{state} · {source}',
|
||||
};
|
||||
const errors = (allowed) =>
|
||||
checkCatalog('es', catalog, fixture, new Set(allowed)).errors;
|
||||
assert.deepEqual(
|
||||
errors([]).map((error) => error.split(' is still English')[0]),
|
||||
['"Bloom"', '"HDG"', '"voice|READY"'],
|
||||
);
|
||||
assert.deepEqual(errors(['Bloom', 'HDG', 'voice|READY']), []);
|
||||
});
|
||||
|
||||
test('unmarkedMarkup finds text and attributes without data-i18n*', () => {
|
||||
const texts = (html) => unmarkedMarkup('f.html', html).map((f) => f.text);
|
||||
assert.deepEqual(texts('<span>Close</span>'), ['Close']);
|
||||
|
||||
+93
-67
@@ -31,7 +31,7 @@
|
||||
"RETRYING MAPPED SITES": "REINTENTANDO SITIOS MAPEADOS",
|
||||
"FETCHING MAPPED SITES": "OBTENIENDO SITIOS MAPEADOS",
|
||||
"TURNING OFF LIVE DATA": "DESACTIVANDO DATOS EN VIVO",
|
||||
"REFRESHING LIVE DATA": "ACTUALIZANDO DATOS EN VIVO",
|
||||
"REFRESHING LIVE DATA": "ACTUALIZANDO DATOS",
|
||||
"Close data attribution": "Cerrar la atribución de datos",
|
||||
"Aircraft cockpit view": "Vista de cabina de la aeronave",
|
||||
"EXIT COCKPIT": "SALIR DE CABINA",
|
||||
@@ -103,8 +103,8 @@
|
||||
"Celestial": "Celeste",
|
||||
"Hide UI chrome": "Ocultar los elementos de la interfaz",
|
||||
"Clean UI": "Interfaz limpia",
|
||||
"Bloom / Glow": "Bloom / Resplandor",
|
||||
"Bloom": "Bloom",
|
||||
"Bloom / Glow": "Resplandor",
|
||||
"Bloom": "Resplandor",
|
||||
"Sharpening": "Nitidez",
|
||||
"Sharpen": "Nitidez",
|
||||
"LOCATION": "UBICACIÓN",
|
||||
@@ -222,8 +222,8 @@
|
||||
"Collapse Radio section": "Contraer la sección de Radio",
|
||||
"Searched location": "Ubicación buscada",
|
||||
"Disable cockpit weather effects": "Desactivar los efectos climáticos de la cabina",
|
||||
"ON": "ACTIVADO",
|
||||
"OFF": "DESACTIVADO",
|
||||
"ON": "ACTIVO",
|
||||
"OFF": "INACTIVO",
|
||||
"Restore real aircraft": "Restaurar la aeronave real",
|
||||
"Night vision": "Visión nocturna",
|
||||
"Thermal": "Térmica",
|
||||
@@ -277,8 +277,8 @@
|
||||
"Expand briefing panel": "Expandir el panel de información",
|
||||
"CYCLE ON": "CICLO: SÍ",
|
||||
"CYCLE OFF": "CICLO: NO",
|
||||
"Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "Detén el cambio automático de páginas. Anterior, Siguiente y las pestañas SIG/NEWS/LOCAL siguen disponibles.",
|
||||
"REGIONAL NEWS": "NOTICIAS REGIONALES",
|
||||
"Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "Detén el cambio automático de páginas. Anterior, Siguiente y las pestañas SIG/NOTICIAS/LOCAL siguen disponibles.",
|
||||
"REGIONAL NEWS": "NOTICIAS DE ZONA",
|
||||
"LOCAL INFO": "INFORMACIÓN LOCAL",
|
||||
"LATEST LOCATION-MATCHED REPORTING": "ÚLTIMOS REPORTES SEGÚN UBICACIÓN",
|
||||
"PLACE / CONDITIONS / POSITION": "LUGAR / CLIMA / POSICIÓN",
|
||||
@@ -358,9 +358,9 @@
|
||||
"CAL · {badge}": "CAL · {badge}",
|
||||
"CALIBRATED": "CALIBRADA",
|
||||
"CURATED": "CURADA",
|
||||
"RAW PRIOR": "PREVIA SIN PROCESAR",
|
||||
"RAW PRIOR": "ESTIMACIÓN INICIAL",
|
||||
"MONITOR": "MONITOR",
|
||||
"{city} · HDG {heading} · FOV {fov} · RANGE {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}": "{city} · HDG {heading} · FOV {fov} · DIST. {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",
|
||||
@@ -385,7 +385,7 @@
|
||||
"Awaiting telemetry...": "Esperando telemetría...",
|
||||
"REC": "REC",
|
||||
"Scope edge feather": "Difuminado del borde de la mirilla",
|
||||
"Bloom intensity": "Intensidad del bloom",
|
||||
"Bloom intensity": "Intensidad del resplandor",
|
||||
"Sharpen intensity": "Intensidad de la nitidez",
|
||||
"Search location by name or coordinates": "Buscar ubicación por nombre o coordenadas",
|
||||
"Expand LOCATION": "Expandir UBICACIÓN",
|
||||
@@ -412,7 +412,7 @@
|
||||
"CCTV camera": "Cámara CCTV",
|
||||
"FOCUS": "ENFOCAR",
|
||||
"COVERAGE OFF": "COBERTURA DESACTIVADA",
|
||||
"AUTO HOP OFF": "AUTOSALTO DESACTIVADO",
|
||||
"AUTO HOP OFF": "SALTO AUTOMÁTICO: NO",
|
||||
"PROJECTION ON": "PROYECCIÓN ACTIVADA",
|
||||
"CALIBRATION": "CALIBRACIÓN",
|
||||
"ADJUST": "AJUSTAR",
|
||||
@@ -435,7 +435,7 @@
|
||||
"STATION TAG": "ETIQUETA DE EMISORA",
|
||||
"Filter stations by station tag": "Filtrar emisoras por etiqueta de emisora",
|
||||
"NO STATION SELECTED": "NINGUNA EMISORA SELECCIONADA",
|
||||
"Enable Radio, then choose a globe marker or use next.": "Activa Radio y elige un marcador del globo o usa siguiente.",
|
||||
"Enable Radio, then choose a globe marker or use next.": "Activa Radio y elige un marcador del globo o pulsa SIG.",
|
||||
"DIRECTORY BAND": "BANDA DEL DIRECTORIO",
|
||||
"DRAG TO TUNE": "ARRASTRA PARA SINTONIZAR",
|
||||
"ALL · DRAG THE NEEDLE": "TODAS · ARRASTRA LA AGUJA",
|
||||
@@ -450,7 +450,7 @@
|
||||
"Radio volume": "Volumen de Radio",
|
||||
"Radio off": "Radio desactivada",
|
||||
"STATION SITE": "SITIO DE LA EMISORA",
|
||||
"Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.": "El audio se conecta directamente con la emisora después de pulsar reproducir. Tu IP es visible para esa emisora.",
|
||||
"Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.": "El audio se conecta directamente con la emisora después de pulsar ESCUCHAR. Tu IP es visible para esa emisora.",
|
||||
"LOADING": "CARGANDO",
|
||||
"DEGRADED": "DEGRADADO",
|
||||
"STALE": "DESACTUALIZADO",
|
||||
@@ -458,7 +458,7 @@
|
||||
"PARTIAL": "PARCIAL",
|
||||
"UNCERTAIN": "INCIERTO",
|
||||
"ENABLING": "ACTIVANDO",
|
||||
"DISABLING": "DESACTIVANDO",
|
||||
"DISABLING": "APAGANDO",
|
||||
"Military Flights": "Vuelos militares",
|
||||
"Satellites": "Satélites",
|
||||
"Space Missions (30d)": "Misiones espaciales (30d)",
|
||||
@@ -476,7 +476,7 @@
|
||||
"{count}s ago": "hace {count}s",
|
||||
"{count}m ago": "hace {count} min",
|
||||
"{count}h ago": "hace {count} h",
|
||||
"never": "nunca",
|
||||
"never": "sin actualizar",
|
||||
"loading...": "cargando...",
|
||||
"UNCERTAIN · {source} · lifecycle state requires reconciliation": "INCIERTO · {source} · el estado del ciclo de vida requiere reconciliación",
|
||||
"{state} · {source}": "{state} · {source}",
|
||||
@@ -517,11 +517,11 @@
|
||||
"POS: {time}Z": "POS: {time}Z",
|
||||
"AIS: --": "AIS: --",
|
||||
"AIS: {name}": "AIS: {name}",
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}",
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} VEL: {speed} HDG: {heading}",
|
||||
"awaiting first AIS position…": "esperando la primera posición AIS…",
|
||||
"CCTV ON": "CCTV ACTIVADO",
|
||||
"COVERAGE ON": "COBERTURA ACTIVADA",
|
||||
"AUTO HOP ON": "AUTOSALTO ACTIVADO",
|
||||
"AUTO HOP ON": "SALTO AUTOMÁTICO: SÍ",
|
||||
"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",
|
||||
@@ -555,9 +555,9 @@
|
||||
"Reconcile Radio — lifecycle uncertain": "Reconciliar Radio — ciclo de vida incierto",
|
||||
"Loading station directory…": "Cargando el directorio de emisoras…",
|
||||
"Directory metadata only": "Solo metadatos del directorio",
|
||||
"Choose a globe marker or use next.": "Elige un marcador del globo o usa siguiente.",
|
||||
"Choose a globe marker or use next.": "Elige un marcador del globo o pulsa SIG.",
|
||||
"TAGS · {tags}": "ETIQUETAS · {tags}",
|
||||
"Ready — playback starts only from your action": "Lista — la reproducción comienza solo con tu acción",
|
||||
"Ready — playback starts only from your action": "Preparada: la reproducción empieza cuando pulses ESCUCHAR",
|
||||
"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}",
|
||||
@@ -600,7 +600,7 @@
|
||||
"Next mission": "Misión siguiente",
|
||||
"SHOW ALL / DESELECT": "MOSTRAR TODAS / DESELECCIONAR",
|
||||
"NO MISSIONS AVAILABLE IN THE CURRENT 30-DAY WINDOW": "SIN MISIONES DISPONIBLES EN LA VENTANA ACTUAL DE 30 DÍAS",
|
||||
"{count} / 30D": "{count} / 30D",
|
||||
"{count} / 30D": "{count} / 30 D",
|
||||
"DATE UNAVAILABLE": "FECHA NO DISPONIBLE",
|
||||
"UNSPECIFIED OPERATOR": "OPERADOR NO ESPECIFICADO",
|
||||
"Select {mission}": "Seleccionar {mission}",
|
||||
@@ -655,7 +655,7 @@
|
||||
"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",
|
||||
"KEY REQUIRED": "CLAVE REQUERIDA",
|
||||
"STALE · cached {age}": "DESACTUALIZADO · en caché hace {age}",
|
||||
"LIVE · updated {age}": "EN VIVO · actualizado {age}",
|
||||
"<1m ago": "hace <1 min",
|
||||
@@ -663,7 +663,7 @@
|
||||
"▲ {frp} MW": "▲ {frp} MW",
|
||||
"FIRE": "INCENDIO",
|
||||
"FIRES": "INCENDIOS",
|
||||
"{value} conf": "{value} conf",
|
||||
"{value} conf": "{value} de confianza",
|
||||
"{value} ago": "hace {value}",
|
||||
"max {value} MW": "máx {value} MW",
|
||||
"new {value}": "nueva hace {value}",
|
||||
@@ -748,7 +748,7 @@
|
||||
"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",
|
||||
"The Google Maps key buys the photorealistic planet — everything else stacks on top.": "La clave de Google Maps compra el planeta fotorrealista — todo lo demás se monta encima.",
|
||||
"The Google Maps key buys the photorealistic planet — everything else stacks on top.": "La clave de Google Maps activa el planeta fotorrealista; todo lo demás se superpone.",
|
||||
"SCENES": "ESCENAS",
|
||||
"Scene recipe": "Receta de escena",
|
||||
"NEW": "NUEVA",
|
||||
@@ -831,7 +831,7 @@
|
||||
"Could not exit {mode} — scene layers may be refused": "No se pudo salir de {mode} — las capas de la escena pueden rechazarse",
|
||||
"REMOVE": "ELIMINAR",
|
||||
"Remove {title} from this app's saved keys": "Elimina {title} de las claves guardadas de esta app",
|
||||
"Needs {envVars} — add it in Provider Settings": "Necesita {envVars} — añádela en Ajustes de proveedores",
|
||||
"Needs {envVars} — add it in Provider Settings": "Necesita {envVars} — se añade en Ajustes de proveedores",
|
||||
"Places context + Street View fallback; optional separate key": "Contexto de Places + respaldo de Street View; clave separada opcional",
|
||||
"Live ships, worldwide": "Buques en vivo, en todo el mundo",
|
||||
"Live active-fire detections": "Detecciones de incendios activos en vivo",
|
||||
@@ -872,8 +872,8 @@
|
||||
"WHOT/BHOT": "WHOT/BHOT",
|
||||
"Ironbow": "Ironbow",
|
||||
"Pick a shape, then click the map.": "Elige una forma y haz clic en el mapa.",
|
||||
"Enter to place the pin, Esc to cancel.": "Enter para colocar el pin, Esc para cancelar.",
|
||||
"Click where the pin goes.": "Haz clic donde va el pin.",
|
||||
"Enter to place the pin, Esc to cancel.": "Enter para colocar el marcador, Esc para cancelar.",
|
||||
"Click where the pin goes.": "Haz clic donde quieras colocar el marcador.",
|
||||
"Click {count} more points.": {
|
||||
"one": "Haz clic en {count} punto más.",
|
||||
"many": "Haz clic en {count} de puntos más.",
|
||||
@@ -892,7 +892,7 @@
|
||||
"Shape to draw": "Forma a dibujar",
|
||||
"Area": "Área",
|
||||
"Line": "Línea",
|
||||
"Pin": "Pin",
|
||||
"Pin": "Marcador",
|
||||
"Label (optional)": "Etiqueta (opcional)",
|
||||
"Label for the drawn shape": "Etiqueta de la forma dibujada",
|
||||
"Colour of the drawn shape": "Color de la forma dibujada",
|
||||
@@ -910,7 +910,7 @@
|
||||
"Bike Share": "Bicis compartidas",
|
||||
"Cameras": "Cámaras",
|
||||
"Street Level": "A pie de calle",
|
||||
"Mapped ALPR Cameras": "Cámaras ALPR cartografiadas",
|
||||
"Mapped ALPR Cameras": "Cámaras ALPR mapeadas",
|
||||
"Data Centers": "Centros de datos",
|
||||
"Active Fires": "Incendios activos",
|
||||
"Street Level could not start": "No se pudo iniciar A pie de calle",
|
||||
@@ -946,9 +946,9 @@
|
||||
"Switch a provider on to draw its coverage.": "Activa un proveedor para dibujar su cobertura.",
|
||||
"Loading this sequence…": "Cargando esta secuencia…",
|
||||
"{n} images in this sequence · Esc clears": {
|
||||
"one": "{n} imagen en esta secuencia · Esc la quita",
|
||||
"many": "{n} de imágenes en esta secuencia · Esc la quita",
|
||||
"other": "{n} imágenes en esta secuencia · Esc la quita"
|
||||
"one": "{n} imagen en esta secuencia · Esc quita la selección",
|
||||
"many": "{n} de imágenes en esta secuencia · Esc quita la selección",
|
||||
"other": "{n} imágenes en esta secuencia · Esc quita la selección"
|
||||
},
|
||||
"{n} sequences in view · click a line for its photos": {
|
||||
"one": "{n} secuencia a la vista · haz clic en una línea para ver sus fotos",
|
||||
@@ -1038,7 +1038,7 @@
|
||||
"Play reconstruction": "Reproducir la reconstrucción",
|
||||
"Load previous scene shot": "Cargar el plano anterior de la escena",
|
||||
"Load next scene shot": "Cargar el siguiente plano de la escena",
|
||||
"SCHEMATIC CORRIDOR, NOT MODELED ARRIVAL TIME.": "CORREDOR ESQUEMÁTICO, NO TIEMPO DE LLEGADA MODELADO.",
|
||||
"SCHEMATIC CORRIDOR, NOT MODELED ARRIVAL TIME.": "CORREDOR ESQUEMÁTICO; NO ES UN TIEMPO DE LLEGADA MODELADO.",
|
||||
"Rasuwa · 26 Aug 2026": "Rasuwa · 26 ago 2026",
|
||||
"Event reconstruction": "Reconstrucción del evento",
|
||||
"INITIATION": "INICIO",
|
||||
@@ -1047,8 +1047,8 @@
|
||||
"BORDER ARRIVAL": "LLEGADA A LA FRONTERA",
|
||||
"PROPAGATION": "PROPAGACIÓN",
|
||||
"DOWNSTREAM PASSAGE": "PASO AGUAS ABAJO",
|
||||
"CONSEQUENCE": "CONSECUENCIA",
|
||||
"DOWNSTREAM CONSEQUENCE": "CONSECUENCIA AGUAS ABAJO",
|
||||
"CONSEQUENCE": "SECUELAS",
|
||||
"DOWNSTREAM CONSEQUENCE": "SECUELAS AGUAS ABAJO",
|
||||
"EVIDENCE": "EVIDENCIA",
|
||||
"{count} GEOLOCATED SOURCE-MAP PLACEMENTS": {
|
||||
"one": "{count} UBICACIÓN GEOLOCALIZADA EN EL MAPA DE FUENTES",
|
||||
@@ -1064,7 +1064,7 @@
|
||||
"Close event layer": "Cerrar la capa del evento",
|
||||
"OBSERVED IMAGERY": "IMÁGENES OBSERVADAS",
|
||||
"SCHEMATIC CORRIDOR": "CORREDOR ESQUEMÁTICO",
|
||||
"2021 HISTORICAL REFERENCE / 2026 POST-EVENT": "REFERENCIA HISTÓRICA 2021 / POSTERIOR AL EVENTO 2026",
|
||||
"2021 HISTORICAL REFERENCE / 2026 POST-EVENT": "HISTÓRICA 2021 / TRAS EL EVENTO 2026",
|
||||
"Historical reference and post-event image split": "División entre la imagen de referencia histórica y la posterior al evento",
|
||||
"RECONSTRUCTION CLOCK": "RELOJ DE LA RECONSTRUCCIÓN",
|
||||
"Schematic downstream progression": "Avance esquemático aguas abajo",
|
||||
@@ -1123,12 +1123,12 @@
|
||||
"{envVar} saved — paste to replace": "{envVar} guardada — pega otra para reemplazarla",
|
||||
"paste {envVar}": "pega {envVar}",
|
||||
"Project keys: {status}": "Claves del proyecto: {status}",
|
||||
"Mapped camera locations": "Ubicaciones de cámaras cartografiadas",
|
||||
"Mapped camera locations": "Ubicaciones de cámaras mapeadas",
|
||||
"SHOW NEAREST": "MOSTRAR LA MÁS CERCANA",
|
||||
"Stop following the current object before navigating to a camera": "Deja de seguir el objeto actual antes de ir a una cámara",
|
||||
"Move to the nearest loaded camera and show its details": "Ir a la cámara cargada más cercana y mostrar sus detalles",
|
||||
"Camera badges": "Iconos de cámaras",
|
||||
"Cyan cameras turn coral when selected. Wedges illustrate mapped direction, not measured coverage. Nearby cameras may be outside the screen.": "Las cámaras cian se vuelven coral al seleccionarlas. Las cuñas ilustran la dirección cartografiada, no la cobertura medida. Las cámaras cercanas pueden estar fuera de la pantalla.",
|
||||
"Cyan cameras turn coral when selected. Wedges illustrate mapped direction, not measured coverage. Nearby cameras may be outside the screen.": "Las cámaras cian se vuelven coral al seleccionarlas. Las cuñas ilustran la dirección mapeada, no la cobertura medida. Las cámaras cercanas pueden estar fuera de la pantalla.",
|
||||
"{count} nearby · {onScreen} on screen": {
|
||||
"one": "{count} cercana · {onScreen} en pantalla",
|
||||
"many": "{count} de cámaras cercanas · {onScreen} en pantalla",
|
||||
@@ -1139,16 +1139,16 @@
|
||||
"many": "{count} de cámaras cercanas",
|
||||
"other": "{count} cercanas"
|
||||
},
|
||||
"retrying mapped ALPR cameras": "reintentando cámaras ALPR cartografiadas",
|
||||
"loading mapped ALPR cameras": "cargando cámaras ALPR cartografiadas",
|
||||
"retrying mapped ALPR cameras": "reintentando cámaras ALPR mapeadas",
|
||||
"loading mapped ALPR cameras": "cargando cámaras ALPR mapeadas",
|
||||
"No ALPR data for this area — US and Canada only": "Sin datos ALPR para esta zona — solo EE. UU. y Canadá",
|
||||
"Zoom in to load mapped cameras": "Haz zoom para cargar las cámaras cartografiadas",
|
||||
"Zoom in to load mapped cameras": "Haz zoom para cargar las cámaras mapeadas",
|
||||
"Showing cached locations": "Mostrando ubicaciones en caché",
|
||||
"Coverage limited — zoom in": "Cobertura limitada — haz zoom",
|
||||
"No ALPR data for this area": "Sin datos ALPR para esta zona",
|
||||
"None on screen — nearby cameras are outside the view": "Ninguna en pantalla — las cámaras cercanas están fuera de la vista",
|
||||
"ALPR camera": "Cámara ALPR",
|
||||
"OpenStreetMap · community mapped": "OpenStreetMap · cartografiado por la comunidad",
|
||||
"OpenStreetMap · community mapped": "OpenStreetMap · mapeado por la comunidad",
|
||||
"Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.": "Atlántico y Pacífico Norte oriental/central; no es una cobertura mundial de ciclones.",
|
||||
"Potential tropical cyclone": "Posible ciclón tropical",
|
||||
"Hurricane": "Huracán",
|
||||
@@ -1231,17 +1231,17 @@
|
||||
"Fully contained": "Totalmente contenido",
|
||||
"Colour shows reported containment. Perimeters are simplified to about 100 m.": "El color indica la contención notificada. Los perímetros se simplifican a unos 100 m.",
|
||||
"MORE DETAIL · 30 m": "MÁS DETALLE · 30 m",
|
||||
"Sentinel-2 and Landsat via HLS, 30 m, every 2–6 days": "Sentinel-2 y Landsat vía HLS, 30 m, cada 2–6 días",
|
||||
"Sentinel-2 and Landsat via HLS, 30 m, every 2–6 days": "Sentinel‑2 y Landsat vía HLS, 30 m, cada 2–6 días",
|
||||
"DAILY OVERVIEW · 250 m": "VISTA GENERAL DIARIA · 250 m",
|
||||
"VIIRS daily mosaic, 250 m, follows broad changes; little detail under 25 km": "Mosaico diario de VIIRS, 250 m, sigue los cambios amplios; poco detalle por debajo de 25 km",
|
||||
"Catalog truncated · a newer clear day may exist": "Catálogo truncado · puede existir un día despejado más reciente",
|
||||
"{label} catalog unavailable · {detail}": "Catálogo de {label} no disponible · {detail}",
|
||||
"Daily overview for today may still be empty until the pass": "La vista general diaria de hoy puede seguir vacía hasta el paso del satélite",
|
||||
"Sentinel-2": "Sentinel-2",
|
||||
"Sentinel-2": "Sentinel‑2",
|
||||
"Landsat 8/9": "Landsat 8/9",
|
||||
"Daily overview": "Vista general diaria",
|
||||
"No frame within {gap} min of {time}": "Ningún fotograma a menos de {gap} min de {time}",
|
||||
"No frame within {gap} h of {time}": "Ningún fotograma a menos de {gap} h de {time}",
|
||||
"No frame within {gap} min of {time}": "Sin fotograma a ±{gap} min de {time}",
|
||||
"No frame within {gap} h of {time}": "Sin fotograma a ±{gap} h de {time}",
|
||||
"{hours}h {minutes}m ago": "hace {hours} h {minutes} min",
|
||||
"{minutes}m ago": "hace {minutes} min",
|
||||
"{state} · {time} · {lag}": "{state} · {time} · {lag}",
|
||||
@@ -1451,10 +1451,10 @@
|
||||
"Tilt map to oblique view": "Inclinar el mapa a una vista oblicua",
|
||||
"Reset map to north up. Current heading {heading} degrees": "Orientar el mapa al norte. Rumbo actual: {heading} grados",
|
||||
"HDG": "HDG",
|
||||
"PITCH": "PITCH",
|
||||
"PITCH": "INCL.",
|
||||
"FOV": "FOV",
|
||||
"RANGE": "RANGE",
|
||||
"HGT": "HGT",
|
||||
"RANGE": "DIST.",
|
||||
"HGT": "ALT.",
|
||||
"{value} KM/H": "{value} KM/H",
|
||||
"NORMAL": "NORMAL",
|
||||
"ANIME": "ANIME",
|
||||
@@ -1515,7 +1515,7 @@
|
||||
"Mode": "Modo",
|
||||
"Imagery on Esri · Google 3D returns when cleared": "Imágenes sobre Esri · Google 3D vuelve al borrarlas",
|
||||
"Imagery opacity": "Opacidad de las imágenes",
|
||||
"SWAP": "CAMBIO",
|
||||
"SWAP": "INVERTIR",
|
||||
"VS BASEMAP": "VS MAPA BASE",
|
||||
"Trade the two sides of the divider": "Intercambiar los dos lados del divisor",
|
||||
"A / B": "A / B",
|
||||
@@ -1568,7 +1568,7 @@
|
||||
"START HERE · {reason}": "EMPIEZA AQUÍ · {reason}",
|
||||
"Daily overview shows little detail in a box this small": "La vista general diaria muestra poco detalle en un recuadro tan pequeño",
|
||||
"{percent}% cloud": "{percent}% de nubes",
|
||||
"Imagery: NASA GIBS and Worldview · HLS (Sentinel-2, Landsat 8/9) and VIIRS": "Imágenes: NASA GIBS y Worldview · HLS (Sentinel-2, Landsat 8/9) y VIIRS",
|
||||
"Imagery: NASA GIBS and Worldview · HLS (Sentinel-2, Landsat 8/9) and VIIRS": "Imágenes: NASA GIBS y Worldview · HLS (Sentinel‑2, Landsat 8/9) y VIIRS",
|
||||
"{min}–{max}% cloud": "{min}–{max}% de nubes",
|
||||
"Details": "Detalles",
|
||||
"Times, coverage, sources": "Horas, cobertura, fuentes",
|
||||
@@ -1593,7 +1593,7 @@
|
||||
"Select a box or use the view": "Selecciona un recuadro o usa la vista",
|
||||
"Searching the last 30 days": "Buscando en los últimos 30 días",
|
||||
"No days to show for this box": "No hay días que mostrar para este recuadro",
|
||||
"Drag the divider · SWAP trades sides": "Arrastra el divisor · CAMBIO invierte los lados",
|
||||
"Drag the divider · SWAP trades sides": "Arrastra el divisor · INVERTIR cambia los lados",
|
||||
"Export failed · {error}": "La exportación falló · {error}",
|
||||
"← → preview · A or B pins the focused day": "← → vista previa · A o B fija el día enfocado",
|
||||
"← → preview the other side · A or B pins it": "← → vista previa del otro lado · A o B lo fija",
|
||||
@@ -1608,7 +1608,7 @@
|
||||
"ESC EXIT": "ESC SALIR",
|
||||
"C TOGGLE": "C ALTERNAR",
|
||||
"SIG": "SIG",
|
||||
"NEWS": "NEWS",
|
||||
"NEWS": "NOTICIAS",
|
||||
"LOCAL": "LOCAL",
|
||||
"Cockpit briefing pages": "Páginas de información de la cabina",
|
||||
"MM": "MM",
|
||||
@@ -1616,11 +1616,11 @@
|
||||
"FT": "FT",
|
||||
"FLT": "FLT",
|
||||
"MIL": "MIL",
|
||||
"SITE": "SITE",
|
||||
"SITE": "SITIOS",
|
||||
"WX": "WX",
|
||||
"DIRECTORY: RADIO BROWSER": "DIRECTORIO: RADIO BROWSER",
|
||||
"Use your location for faster ADS-B position decoding": "Usa tu ubicación para decodificar más rápido las posiciones ADS-B",
|
||||
"CHANGE DEVICE": "CAMBIAR DISPOSITIVO",
|
||||
"CHANGE DEVICE": "CAMBIAR",
|
||||
"Choose a different RTL-SDR or receiver channel": "Elige otro RTL-SDR u otro canal del receptor",
|
||||
"TUNE": "SINTONIZAR",
|
||||
"◀ SEEK": "◀ BUSCAR",
|
||||
@@ -1644,11 +1644,11 @@
|
||||
"POSITIONED": "POSICIONADAS",
|
||||
"SDR VOL": "VOL. SDR",
|
||||
"LOCAL RTL-SDR": "RTL-SDR LOCAL",
|
||||
"Cyber sonar contact scanner": "Escáner sonar de contactos Cyber",
|
||||
"Cyber sonar contact scanner": "Escáner sonar de contactos Ciber",
|
||||
"Sonar ring count": "Número de anillos del sonar",
|
||||
"Sonar ring range": "Alcance de los anillos del sonar",
|
||||
"Sonar intensity": "Intensidad del sonar",
|
||||
"Cyber contact and label opacity": "Opacidad de contactos y etiquetas Cyber",
|
||||
"Cyber contact and label opacity": "Opacidad de contactos y etiquetas Ciber",
|
||||
"Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes": "Opacidad mínima de puntos, iconos, modelos, corchetes y etiquetas del mapa entre pasadas del sonar",
|
||||
"Sonar sector width": "Ancho del sector del sonar",
|
||||
"Rings": "Anillos",
|
||||
@@ -1656,7 +1656,7 @@
|
||||
"Power": "Potencia",
|
||||
"Opacity": "Opacidad",
|
||||
"Sector": "Sector",
|
||||
"Cyber": "Cyber",
|
||||
"Cyber": "Ciber",
|
||||
"Sonar": "Sonar",
|
||||
"Turn Street Level on or off": "Activar o desactivar A pie de calle",
|
||||
"Toggle straight-down and tilted map views": "Alternar entre la vista cenital y la vista inclinada del mapa",
|
||||
@@ -1671,7 +1671,7 @@
|
||||
"{time} · {minutes} min ago": "{time} · hace {minutes} min",
|
||||
"Active weather": "Clima activo",
|
||||
"MINI": "MINI",
|
||||
"STD": "STD",
|
||||
"STD": "EST.",
|
||||
"standard": "estándar",
|
||||
"mini": "mini",
|
||||
"COST UNKNOWN": "COSTO DESCONOCIDO",
|
||||
@@ -1786,7 +1786,7 @@
|
||||
"Cyclone advisories": "Avisos de ciclones",
|
||||
"EDIT DETAILS": "EDITAR DETALLES",
|
||||
"SHARE SCENE": "COMPARTIR ESCENA",
|
||||
"ON/OFF": "ON/OFF",
|
||||
"ON/OFF": "ENC./APAG.",
|
||||
"Voice assistant": "Asistente de voz",
|
||||
"VOICE": "VOZ",
|
||||
"Dismiss voice card": "Cerrar la tarjeta de voz",
|
||||
@@ -1811,7 +1811,7 @@
|
||||
"overview": "vista general",
|
||||
"{percent}% scene cloud": "{percent}% de nubes en la escena",
|
||||
"{min}–{max}% scene cloud": "{min}–{max}% de nubes en la escena",
|
||||
"Sentinel-2 via HLS": "Sentinel-2 vía HLS",
|
||||
"Sentinel-2 via HLS": "Sentinel‑2 vía HLS",
|
||||
"Landsat 8/9 via HLS": "Landsat 8/9 vía HLS",
|
||||
"coverage|full": "completa",
|
||||
"coverage|partial": "parcial",
|
||||
@@ -1887,13 +1887,13 @@
|
||||
"Use the {direction} lane": "Usa el carril {direction}",
|
||||
"Use the {direction} lane onto {road}": "Usa el carril {direction} hacia {road}",
|
||||
"voice|OFF": "APAG.",
|
||||
"voice|CONNECTING": "CONEX…",
|
||||
"voice|LISTENING": "OYENDO",
|
||||
"voice|CONNECTING": "CONECT.",
|
||||
"voice|LISTENING": "ESCUCHO",
|
||||
"voice|EXECUTING": "ACCIÓN",
|
||||
"voice|ERROR": "ERROR",
|
||||
"MIC": "MIC",
|
||||
"WORKING": "EN CURSO",
|
||||
"DONE": "LISTO",
|
||||
"DONE": "HECHO",
|
||||
"Outline traced": "Contorno trazado",
|
||||
"Outline unavailable": "Contorno no disponible",
|
||||
"in progress": "en curso",
|
||||
@@ -2020,7 +2020,7 @@
|
||||
"Globe imagery unavailable": "Imágenes del globo no disponibles",
|
||||
"Globe field image unavailable": "Imagen del campo en el globo no disponible",
|
||||
"status chip|UNAVAILABLE": "NO DISP.",
|
||||
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Usa contenido registrado o medios vinculados. Esos archivos externos no se incluyen en un paquete de escena. Sus avisos originales siguen vigentes.",
|
||||
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Incluye contenido registrado o medios vinculados. Esos archivos externos no se incluyen en un paquete de escena. Sus avisos originales siguen vigentes.",
|
||||
"{state} {layers}": "{state}: {layers}",
|
||||
"Selected Space Mission": "Misión espacial seleccionada",
|
||||
"{duration}s + {hold}s": "{duration} s + {hold} s",
|
||||
@@ -2028,7 +2028,7 @@
|
||||
"subject": "sujeto",
|
||||
"{value}m": "{value} m",
|
||||
"Google 3D tiles unavailable — check the key's API restrictions, quota, or network": "Teselas de Google 3D no disponibles: revisa las restricciones de API de la clave, la cuota o la red",
|
||||
"Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Necesita {envVars} — añádela en Ajustes de proveedores — o un token de Cesium ion para la ruta alojada en ion",
|
||||
"Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Necesita {envVars} — se añade en Ajustes de proveedores — o un token de Cesium ion para la ruta alojada en ion",
|
||||
"Comparison in use by another scene": "La comparación está en uso por otra escena",
|
||||
"A location reading needs a loaded forecast and the Earth at the center of the view.": "Una lectura en un punto necesita un pronóstico cargado y la Tierra en el centro de la vista.",
|
||||
"{mode} {band} {locality} | {region} | ALT {alt} | WINDOW {window} | SUN {sun}° | ONA {ona}° | {zone}": "{mode} · {band} · {locality} | {region} | ALT {alt} | VENTANA {window} | SUN {sun}° | ONA {ona}° | {zone}",
|
||||
@@ -2041,11 +2041,37 @@
|
||||
"zoom in to a street to open {name}": "acércate a una calle para abrir {name}",
|
||||
"Google Street View": "Google Street View",
|
||||
"GOOGLE STREET VIEW": "GOOGLE STREET VIEW",
|
||||
"Google refused GOOGLE_MAPS_API_KEY for Street View — check that the Maps JavaScript API is enabled for it and this site is an allowed referrer": "Google rechazó GOOGLE_MAPS_API_KEY para Street View — comprueba que la API Maps JavaScript esté habilitada para ella y que este sitio sea un referente permitido",
|
||||
"Google refused GOOGLE_MAPS_API_KEY for Street View — check that the Maps JavaScript API is enabled for it and this site is an allowed referrer": "Google rechazó GOOGLE_MAPS_API_KEY para Street View — comprueba que la clave tenga habilitada la API Maps JavaScript y que este sitio figure entre sus referentes HTTP permitidos",
|
||||
"Street View did not answer — check the network, and that the Maps JavaScript API is enabled for GOOGLE_MAPS_API_KEY": "Street View no respondió — comprueba la red y que la API Maps JavaScript esté habilitada para GOOGLE_MAPS_API_KEY",
|
||||
"click a street to open Street View": "haz clic en una calle para abrir Street View",
|
||||
" or ": " o ",
|
||||
"OPEN {labels}": "ABRIR {labels}",
|
||||
"Open {names} at the centre of the view": "Abrir {names} en el centro de la vista",
|
||||
"Zoom in to a street to open {names} here": "Acércate a una calle para abrir {names} aquí"
|
||||
"Zoom in to a street to open {names} here": "Acércate a una calle para abrir {names} aquí",
|
||||
"radio tag|All": "Todas",
|
||||
"Alternative": "Alternativa",
|
||||
"Ambient": "Ambiental",
|
||||
"Blues": "Blues",
|
||||
"Classical": "Clásica",
|
||||
"Country": "Country",
|
||||
"Dance": "Dance",
|
||||
"Electronic": "Electrónica",
|
||||
"Folk": "Folk",
|
||||
"Funk": "Funk",
|
||||
"Hip-Hop": "Hip-Hop",
|
||||
"House": "House",
|
||||
"Indie": "Indie",
|
||||
"Jazz": "Jazz",
|
||||
"Latin": "Latina",
|
||||
"Metal": "Metal",
|
||||
"Oldies": "Clásicos de siempre",
|
||||
"Pop": "Pop",
|
||||
"Punk": "Punk",
|
||||
"R&B": "R&B",
|
||||
"Reggae": "Reggae",
|
||||
"Rock": "Rock",
|
||||
"Soul": "Soul",
|
||||
"Techno": "Techno",
|
||||
"Trance": "Trance",
|
||||
"World": "Músicas del mundo"
|
||||
}
|
||||
@@ -1247,13 +1247,13 @@
|
||||
"Fully contained": "Полностью локализован",
|
||||
"Colour shows reported containment. Perimeters are simplified to about 100 m.": "Цвет показывает заявленную степень локализации. Контуры упрощены примерно до 100 м.",
|
||||
"MORE DETAIL · 30 m": "БОЛЬШЕ ДЕТАЛЕЙ · 30 м",
|
||||
"Sentinel-2 and Landsat via HLS, 30 m, every 2–6 days": "Sentinel-2 и Landsat через HLS, 30 м, каждые 2–6 дней",
|
||||
"Sentinel-2 and Landsat via HLS, 30 m, every 2–6 days": "Sentinel‑2 и Landsat через HLS, 30 м, каждые 2–6 дней",
|
||||
"DAILY OVERVIEW · 250 m": "ЕЖЕДНЕВНЫЙ ОБЗОР · 250 м",
|
||||
"VIIRS daily mosaic, 250 m, follows broad changes; little detail under 25 km": "Ежедневная мозаика VIIRS, 250 м, показывает крупные изменения; мало деталей в масштабе менее 25 км",
|
||||
"Catalog truncated · a newer clear day may exist": "Каталог усечён · может быть более новый ясный день",
|
||||
"{label} catalog unavailable · {detail}": "Каталог {label} недоступен · {detail}",
|
||||
"Daily overview for today may still be empty until the pass": "Ежедневный обзор за сегодня может оставаться пустым до пролёта спутника",
|
||||
"Sentinel-2": "Sentinel-2",
|
||||
"Sentinel-2": "Sentinel‑2",
|
||||
"Landsat 8/9": "Landsat 8/9",
|
||||
"Daily overview": "Ежедневный обзор",
|
||||
"No frame within {gap} min of {time}": "Нет кадра в пределах {gap} мин от {time}",
|
||||
@@ -1592,7 +1592,7 @@
|
||||
"START HERE · {reason}": "НАЧНИТЕ ЗДЕСЬ · {reason}",
|
||||
"Daily overview shows little detail in a box this small": "В такой маленькой области ежедневный обзор почти не показывает деталей",
|
||||
"{percent}% cloud": "облачность {percent}%",
|
||||
"Imagery: NASA GIBS and Worldview · HLS (Sentinel-2, Landsat 8/9) and VIIRS": "Снимки: NASA GIBS и Worldview · HLS (Sentinel-2, Landsat 8/9) и VIIRS",
|
||||
"Imagery: NASA GIBS and Worldview · HLS (Sentinel-2, Landsat 8/9) and VIIRS": "Снимки: NASA GIBS и Worldview · HLS (Sentinel‑2, Landsat 8/9) и VIIRS",
|
||||
"{min}–{max}% cloud": "облачность {min}–{max}%",
|
||||
"Details": "Подробности",
|
||||
"Times, coverage, sources": "Время, покрытие, источники",
|
||||
@@ -1842,7 +1842,7 @@
|
||||
"overview": "обзор",
|
||||
"{percent}% scene cloud": "облачность сцены {percent}%",
|
||||
"{min}–{max}% scene cloud": "облачность сцены {min}–{max}%",
|
||||
"Sentinel-2 via HLS": "Sentinel-2 через HLS",
|
||||
"Sentinel-2 via HLS": "Sentinel‑2 через HLS",
|
||||
"Landsat 8/9 via HLS": "Landsat 8/9 через HLS",
|
||||
"coverage|full": "полное",
|
||||
"coverage|partial": "частичное",
|
||||
@@ -2079,5 +2079,31 @@
|
||||
" or ": " или ",
|
||||
"OPEN {labels}": "ОТКРЫТЬ {labels}",
|
||||
"Open {names} at the centre of the view": "Открыть {names} в центре вида",
|
||||
"Zoom in to a street to open {names} here": "Приблизьтесь к улице, чтобы открыть здесь {names}"
|
||||
"Zoom in to a street to open {names} here": "Приблизьтесь к улице, чтобы открыть здесь {names}",
|
||||
"radio tag|All": "Все",
|
||||
"Alternative": "Альтернатива",
|
||||
"Ambient": "Эмбиент",
|
||||
"Blues": "Блюз",
|
||||
"Classical": "Классика",
|
||||
"Country": "Кантри",
|
||||
"Dance": "Танцевальная",
|
||||
"Electronic": "Электроника",
|
||||
"Folk": "Фолк",
|
||||
"Funk": "Фанк",
|
||||
"Hip-Hop": "Хип-хоп",
|
||||
"House": "Хаус",
|
||||
"Indie": "Инди",
|
||||
"Jazz": "Джаз",
|
||||
"Latin": "Латиноамериканская",
|
||||
"Metal": "Метал",
|
||||
"Oldies": "Ретро",
|
||||
"Pop": "Поп",
|
||||
"Punk": "Панк",
|
||||
"R&B": "R&B",
|
||||
"Reggae": "Регги",
|
||||
"Rock": "Рок",
|
||||
"Soul": "Соул",
|
||||
"Techno": "Техно",
|
||||
"Trance": "Транс",
|
||||
"World": "Музыка мира"
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"es": [
|
||||
"AIS: --",
|
||||
"AIS: {name}",
|
||||
"ANIME",
|
||||
"ASIA",
|
||||
"AUTO",
|
||||
"Anime",
|
||||
"BRG {value}°",
|
||||
"BRG —",
|
||||
"Blues",
|
||||
"CAL {value}",
|
||||
"CAL · {badge}",
|
||||
"CCTV",
|
||||
"CRS {value}°",
|
||||
"CRT",
|
||||
"Country",
|
||||
"DEST ---°",
|
||||
"DEST {bearing}",
|
||||
"Dance",
|
||||
"ENE",
|
||||
"ERROR",
|
||||
"ESE",
|
||||
"Error: {message}",
|
||||
"FLIR",
|
||||
"FLT",
|
||||
"FM MHZ",
|
||||
"FOV",
|
||||
"FOV {value}°",
|
||||
"FT",
|
||||
"Folk",
|
||||
"Funk",
|
||||
"GEO",
|
||||
"GOOGLE STREET VIEW",
|
||||
"Global",
|
||||
"Global · 60°S–60°N",
|
||||
"Google Street View",
|
||||
"HDG",
|
||||
"HDG {value}°",
|
||||
"Hip-Hop",
|
||||
"House",
|
||||
"Indie",
|
||||
"Ironbow",
|
||||
"Jazz",
|
||||
"LOCAL",
|
||||
"Landsat 8/9",
|
||||
"MIC",
|
||||
"MIL",
|
||||
"MINI",
|
||||
"MM",
|
||||
"MONITOR",
|
||||
"MSG/S",
|
||||
"Metal",
|
||||
"NAV",
|
||||
"NE",
|
||||
"NNE",
|
||||
"NOIR",
|
||||
"NORMAL",
|
||||
"NVG",
|
||||
"Noir",
|
||||
"Normal",
|
||||
"POS: {time}Z",
|
||||
"PRECIP",
|
||||
"Plan",
|
||||
"Pop",
|
||||
"Punk",
|
||||
"R&B",
|
||||
"RADIO",
|
||||
"REC",
|
||||
"Radio",
|
||||
"Reggae",
|
||||
"Rock",
|
||||
"SAR",
|
||||
"SE",
|
||||
"SECTOR {lat} {lon}",
|
||||
"SIG",
|
||||
"SRC {value}",
|
||||
"SSE",
|
||||
"Sector",
|
||||
"Sonar",
|
||||
"Soul",
|
||||
"TEMP",
|
||||
"Techno",
|
||||
"Trance",
|
||||
"VISUAL",
|
||||
"VS",
|
||||
"WHOT/BHOT",
|
||||
"WX",
|
||||
"audio {state}",
|
||||
"dB",
|
||||
"ft",
|
||||
"hPa",
|
||||
"km",
|
||||
"km/h",
|
||||
"min",
|
||||
"mini",
|
||||
"mph",
|
||||
"nominal",
|
||||
"panel",
|
||||
"view band|GLOBAL",
|
||||
"voice|ERROR",
|
||||
"{value} KM",
|
||||
"{value} KM/H",
|
||||
"{value} KM/S",
|
||||
"ΔE",
|
||||
"ΔN",
|
||||
"▲ {frp} MW"
|
||||
],
|
||||
"ru": [
|
||||
"AIS: --",
|
||||
"AIS: {name}",
|
||||
"CCTV",
|
||||
"CRT",
|
||||
"FLIR",
|
||||
"GEO",
|
||||
"Ironbow",
|
||||
"Landsat 8/9",
|
||||
"NAV",
|
||||
"NVG",
|
||||
"R&B",
|
||||
"REC",
|
||||
"VS",
|
||||
"WHOT/BHOT",
|
||||
"mini"
|
||||
]
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export function createCategories({
|
||||
function buildRadioCategories(stations) {
|
||||
const rows = Array.isArray(stations) ? stations : [];
|
||||
const categories = [
|
||||
{ id: 'all', label: t('All') },
|
||||
{ id: 'all', label: t('All', { context: 'radio tag' }) },
|
||||
{ id: 'news', label: t('News') },
|
||||
{ id: 'talk', label: t('Talk') },
|
||||
{ id: 'weather', label: t('Weather / Emergency') },
|
||||
@@ -89,7 +89,7 @@ export function createCategories({
|
||||
for (const [genre, label] of MUSIC_GENRES) {
|
||||
const id = `genre:${genre}`;
|
||||
if (rows.some((station) => stationMatchesRadioCategory(station, id))) {
|
||||
categories.push({ id, label });
|
||||
categories.push({ id, label: t(label) });
|
||||
}
|
||||
}
|
||||
categories.push({ id: 'other', label: t('Other') });
|
||||
|
||||
+29
-25
@@ -62,32 +62,36 @@ export const DEFAULT_RADIO_FILTER = 'all';
|
||||
|
||||
export const GLOBAL_RADIO_ALTITUDE_M = 2_000_000;
|
||||
|
||||
// Genre labels are marked for i18n extraction; categories.js translates them.
|
||||
// Kept local so this module stays free of the i18n import.
|
||||
const msg = (text) => text;
|
||||
|
||||
export const MUSIC_GENRES = Object.freeze([
|
||||
['alternative', 'Alternative'],
|
||||
['ambient', 'Ambient'],
|
||||
['blues', 'Blues'],
|
||||
['classical', 'Classical'],
|
||||
['country', 'Country'],
|
||||
['dance', 'Dance'],
|
||||
['electronic', 'Electronic'],
|
||||
['folk', 'Folk'],
|
||||
['funk', 'Funk'],
|
||||
['hip hop', 'Hip-Hop'],
|
||||
['house', 'House'],
|
||||
['indie', 'Indie'],
|
||||
['jazz', 'Jazz'],
|
||||
['latin', 'Latin'],
|
||||
['metal', 'Metal'],
|
||||
['oldies', 'Oldies'],
|
||||
['pop', 'Pop'],
|
||||
['punk', 'Punk'],
|
||||
['r&b', 'R&B'],
|
||||
['reggae', 'Reggae'],
|
||||
['rock', 'Rock'],
|
||||
['soul', 'Soul'],
|
||||
['techno', 'Techno'],
|
||||
['trance', 'Trance'],
|
||||
['world', 'World'],
|
||||
['alternative', msg('Alternative')],
|
||||
['ambient', msg('Ambient')],
|
||||
['blues', msg('Blues')],
|
||||
['classical', msg('Classical')],
|
||||
['country', msg('Country')],
|
||||
['dance', msg('Dance')],
|
||||
['electronic', msg('Electronic')],
|
||||
['folk', msg('Folk')],
|
||||
['funk', msg('Funk')],
|
||||
['hip hop', msg('Hip-Hop')],
|
||||
['house', msg('House')],
|
||||
['indie', msg('Indie')],
|
||||
['jazz', msg('Jazz')],
|
||||
['latin', msg('Latin')],
|
||||
['metal', msg('Metal')],
|
||||
['oldies', msg('Oldies')],
|
||||
['pop', msg('Pop')],
|
||||
['punk', msg('Punk')],
|
||||
['r&b', msg('R&B')],
|
||||
['reggae', msg('Reggae')],
|
||||
['rock', msg('Rock')],
|
||||
['soul', msg('Soul')],
|
||||
['techno', msg('Techno')],
|
||||
['trance', msg('Trance')],
|
||||
['world', msg('World')],
|
||||
]);
|
||||
|
||||
export const CATEGORY_MATCHERS = Object.freeze({
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
import { formatNumber, t } from '../i18n/index.js';
|
||||
|
||||
/** A slider value in the UI language: 0.85 in English, 0,85 in Spanish. */
|
||||
const formatParam = (value, digits) =>
|
||||
formatNumber(value, {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
useGrouping: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Own generated parameter rows and their input listeners.
|
||||
@@ -44,11 +52,14 @@ export function createStyleParameters({
|
||||
const digits = metadata.max <= 1 ? 2 : 1;
|
||||
const valueDisplay = documentRef.createElement('span');
|
||||
valueDisplay.className = 'param-value';
|
||||
valueDisplay.textContent = parseFloat(slider.value).toFixed(digits);
|
||||
valueDisplay.textContent = formatParam(
|
||||
parseFloat(slider.value),
|
||||
digits,
|
||||
);
|
||||
const onInput = () => {
|
||||
const value = parseFloat(slider.value);
|
||||
writeValue(name, value);
|
||||
valueDisplay.textContent = value.toFixed(digits);
|
||||
valueDisplay.textContent = formatParam(value, digits);
|
||||
onChange();
|
||||
};
|
||||
slider.addEventListener('input', onInput);
|
||||
|
||||
@@ -399,6 +399,12 @@
|
||||
color: rgba(222, 240, 245, 0.88);
|
||||
}
|
||||
|
||||
/* A longer translated heading wraps; the readout beside it stays one piece. */
|
||||
html:not([lang='en']) .bhote-event-section-head span:last-child {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bhote-event-range {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
|
||||
@@ -404,3 +404,13 @@ html:not([lang='en']) #cctv-camera-select {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* The camera name gets its own row; PREV / NEXT share the row below it. */
|
||||
html:not([lang='en']) .cctv-controls:has(#cctv-camera-select) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
html:not([lang='en']) #cctv-camera-select {
|
||||
flex: 1 1 100%;
|
||||
order: -1;
|
||||
}
|
||||
@@ -221,6 +221,18 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* A longer translated title in a collapsed rail launcher tightens its
|
||||
tracking rather than pushing the disclosure button out of the card. */
|
||||
html:not([lang='en'])
|
||||
#right-context-rail
|
||||
.panel-collapsible.collapsed
|
||||
.panel-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.panel-divider {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
|
||||
@@ -647,6 +647,13 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Translated layer names are longer: break between words, and hyphenate a
|
||||
word that still does not fit instead of splitting it anywhere. */
|
||||
html:not([lang='en']) .data-name {
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.data-toggle-right {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
|
||||
@@ -366,6 +366,15 @@
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
/* A translated slot line (date · source · resolution · state) is longer:
|
||||
let it take a second line rather than cut off the state at the end. */
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-slot {
|
||||
height: auto;
|
||||
min-height: 20px;
|
||||
}
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-slot-value {
|
||||
white-space: normal;
|
||||
}
|
||||
.recent-imagery-readout .ri-unpin {
|
||||
width: 22px;
|
||||
padding: 0;
|
||||
|
||||
@@ -471,3 +471,9 @@ html:not([lang='en']) #scene-select {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* A translated scene name needs the width: the select takes its own row and
|
||||
NEW / DELETE share the next one. */
|
||||
html:not([lang='en']) #scene-select {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
Reference in new issue
Block a user