i18n: fix the final pre-merge review findings

Fixes the P1 and the confirmed P2 findings of the final review of the
i18n branch.

- CI: the air-quality model no longer imports i18n (portable module),
  so check:boundaries passes again.
- Air quality layer: named in the layer panel, HUD and voice; its
  Weather card, bands and errors translate. A test keeps the layer-name
  table in step with the layer registry.
- Layer statuses that still reached ru/es in English: GNSS, CelesTrak,
  Transit loading and feed errors, launches, perimeters, ALPR, ECCC,
  wind "field unavailable", Local ADS-B streaming lines.
- Voice and HUD: preset ids show city names ("Fly to San Francisco"),
  the HUD city translates, cockpit refusals cover frame, aircraft and
  satellite; empty Transit notes translate on the analyst card.
- tn() keeps a caller's formatted count ("12,345 map objects").
- Cyber HUD: the language button no longer covers the bottom-right
  card or the cockpit position readout.
- CCTV buttons wrap only in non-English locales; English layout matches
  main again.
- Switching language asks first when Provider Settings holds unsaved
  keys.

Catalogs: es/ru 2611 messages each, complete.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-16 16:39:25 +04:00
1 parent fada8b161b
commit 3adce33cc6
43 files changed
+1350 -97

No files matched your search

+12
View File
@@ -234,3 +234,15 @@ test('persistLocaleAndReload stores the choice, keeps the hash, strips ?lang', (
['reload'],
]);
});
test('tn() shows a caller-formatted count and still picks the form by number', () => {
const label = (n) =>
tn(n, '{count} map object', '{count} map objects', {
count: formatNumber(n),
});
assert.equal(label(12345), '12,345 map objects');
assert.equal(tn(12345, '{count} file', '{count} files'), '12345 files');
setLocale('ru');
assert.equal(label(12345), '12 345 объектов на карте');
assert.equal(label(1), '1 объект на карте');
});
+5 -3
View File
@@ -94,11 +94,13 @@ export function t(text, values) {
/**
* Translate a count-dependent message. The catalog entry for `other` holds
* one form per Intl.PluralRules category of the locale ({ one, few, many, other }
* for Russian). `{count}` is always available to the pattern.
* for Russian). `{count}` is always available to the pattern: the raw number,
* unless `values.count` gives its display text, e.g. `formatNumber(n)`.
* @param {number} count
* @param {string} one English singular, e.g. '{count} camera'
* @param {string} other English plural, e.g. '{count} cameras'
* @param {object} [values] Extra interpolation values
* @param {object} [values] Extra interpolation values; a `count` here is
* shown in place of the number (the plural form still follows `count`)
* @returns {string}
*/
export function tn(count, one, other, values) {
@@ -111,7 +113,7 @@ export function tn(count, one, other, values) {
} else {
pattern = pluralRules(DEFAULT_LOCALE).select(count) === 'one' ? one : other;
}
return interpolate(pattern, { ...values, count });
return interpolate(pattern, { count, ...values });
}
/**
+82 -1
View File
@@ -2796,5 +2796,86 @@
},
"FOV {value}° (DATASHEET)": "FOV {value}° (FICHA TÉCNICA)",
"SPEC FOV {span}° (CAPABILITY)": "FOV DE FICHA {span}° (CAPACIDAD)",
"{spec} · SPECS: CCTV-DATABASE.COM": "{spec} · ESPECIFICACIONES: CCTV-DATABASE.COM"
"{spec} · SPECS: CCTV-DATABASE.COM": "{spec} · ESPECIFICACIONES: CCTV-DATABASE.COM",
"Canada only · ECCC AQHI network": "Solo Canadá · red AQHI de ECCC",
"{count} stations reporting": {
"one": "{count} estación con datos",
"many": "{count} de estaciones con datos",
"other": "{count} estaciones con datos"
},
"No readings available": "No hay lecturas disponibles",
"No current readings": "Sin lecturas actuales",
"Low health risk": "Riesgo bajo para la salud",
"Moderate health risk": "Riesgo moderado para la salud",
"High health risk": "Riesgo alto para la salud",
"Very High health risk": "Riesgo muy alto para la salud",
"Air quality request failed": "Falló la petición de calidad del aire",
"ECCC request timed out": "La petición a ECCC agotó el tiempo de espera",
"Malformed AQHI station response": "Respuesta de estaciones AQHI mal formada",
"Malformed AQHI observation response": "Respuesta de observaciones AQHI mal formada",
"No AQHI stations available": "No hay estaciones AQHI disponibles",
"{n} air quality": {
"one": "{n} estación de calidad del aire",
"many": "{n} de estaciones de calidad del aire",
"other": "{n} estaciones de calidad del aire"
},
"GNSS feed rate limited (HTTP 429)": "Fuente GNSS: límite de peticiones superado (HTTP 429)",
"GNSS feed unavailable (HTTP {status})": "Fuente GNSS no disponible (HTTP {status})",
"GNSS snapshot needs a view anchor": "La instantánea GNSS necesita un punto de referencia de la vista",
"Malformed GNSS integrity snapshot": "Instantánea de integridad GNSS mal formada",
"GNSS integrity snapshot has no observation time": "La instantánea de integridad GNSS no tiene hora de observación",
"{feed} feed unavailable": "Fuente {feed} no disponible",
"Loading {feeds}": "Cargando {feeds}",
"{count} CelesTrak groups unavailable": {
"one": "{count} grupo de CelesTrak no disponible",
"many": "{count} de grupos de CelesTrak no disponibles",
"other": "{count} grupos de CelesTrak no disponibles"
},
"Malformed launch snapshot": "Instantánea de lanzamientos mal formada",
"Malformed perimeter snapshot": "Instantánea de perímetros mal formada",
"Overpass returned an incomplete camera response": "Overpass devolvió una respuesta de cámaras incompleta",
"Fly below {km} km to a covered region": "Desciende por debajo de {km} km sobre una región con cobertura",
"No feed here yet · {n} regions available": {
"one": "Aún no hay fuente aquí · {n} región disponible",
"many": "Aún no hay fuente aquí · {n} de regiones disponibles",
"other": "Aún no hay fuente aquí · {n} regiones disponibles"
},
"Wind speed field unavailable": "Campo de velocidad del viento no disponible",
"Decoder feeds": "Fuentes del decodificador",
"WebUSB + decoder feeds": "WebUSB + fuentes del decodificador",
"opening receiver": "abriendo el receptor",
"checking decoder feeds": "comprobando las fuentes del decodificador",
"listening": "escuchando",
"USB error": "error de USB",
"{count} heard": {
"one": "{count} aeronave captada",
"many": "{count} de aeronaves captadas",
"other": "{count} aeronaves captadas"
},
"{rate} msg/s": "{rate} msg/s",
"USB {rate} msg/s": "USB {rate} msg/s",
"{count} feeds live": {
"one": "{count} fuente en vivo",
"many": "{count} de fuentes en vivo",
"other": "{count} fuentes en vivo"
},
"feeds {names} stale": {
"one": "fuente {names} desactualizada",
"many": "fuentes {names} desactualizadas",
"other": "fuentes {names} desactualizadas"
},
"feeds {names} unreachable": {
"one": "fuente {names} inaccesible",
"many": "fuentes {names} inaccesibles",
"other": "fuentes {names} inaccesibles"
},
"feeds {names} invalid": {
"one": "fuente {names} no válida",
"many": "fuentes {names} no válidas",
"other": "fuentes {names} no válidas"
},
"unsaved provider keys": "las claves de proveedores sin guardar",
"Exit cockpit to fly to an aircraft": "Sal de la cabina para volar a una aeronave",
"Exit cockpit to fly to a satellite": "Sal de la cabina para volar a un satélite",
"Exit cockpit to frame contacts": "Sal de la cabina para encuadrar contactos"
}
+91 -1
View File
@@ -2854,5 +2854,95 @@
},
"FOV {value}° (DATASHEET)": "ОБЗОР {value}° (ПО ПАСПОРТУ)",
"SPEC FOV {span}° (CAPABILITY)": "ОБЗОР ПО ПАСПОРТУ {span}° (ВОЗМОЖНОСТИ)",
"{spec} · SPECS: CCTV-DATABASE.COM": "{spec} · ХАРАКТЕРИСТИКИ: CCTV-DATABASE.COM"
"{spec} · SPECS: CCTV-DATABASE.COM": "{spec} · ХАРАКТЕРИСТИКИ: CCTV-DATABASE.COM",
"Canada only · ECCC AQHI network": "Только Канада · сеть AQHI ECCC",
"{count} stations reporting": {
"one": "{count} станция передаёт данные",
"few": "{count} станции передают данные",
"many": "{count} станций передают данные",
"other": "{count} станции передают данные"
},
"No readings available": "Нет доступных показаний",
"No current readings": "Нет актуальных показаний",
"Low health risk": "Низкий риск для здоровья",
"Moderate health risk": "Умеренный риск для здоровья",
"High health risk": "Высокий риск для здоровья",
"Very High health risk": "Очень высокий риск для здоровья",
"Air quality request failed": "Не удалось запросить данные о качестве воздуха",
"ECCC request timed out": "Истекло время ожидания запроса к ECCC",
"Malformed AQHI station response": "Некорректный ответ со станциями AQHI",
"Malformed AQHI observation response": "Некорректный ответ с наблюдениями AQHI",
"No AQHI stations available": "Нет доступных станций AQHI",
"{n} air quality": {
"one": "{n} станция качества воздуха",
"few": "{n} станции качества воздуха",
"many": "{n} станций качества воздуха",
"other": "{n} станции качества воздуха"
},
"GNSS feed rate limited (HTTP 429)": "Поток GNSS: превышен лимит запросов (HTTP 429)",
"GNSS feed unavailable (HTTP {status})": "Поток GNSS недоступен (HTTP {status})",
"GNSS snapshot needs a view anchor": "Для снимка GNSS нужна опорная точка вида",
"Malformed GNSS integrity snapshot": "Некорректный снимок целостности GNSS",
"GNSS integrity snapshot has no observation time": "В снимке целостности GNSS нет времени наблюдения",
"{feed} feed unavailable": "Поток {feed} недоступен",
"Loading {feeds}": "Загрузка: {feeds}",
"{count} CelesTrak groups unavailable": {
"one": "{count} группа CelesTrak недоступна",
"few": "{count} группы CelesTrak недоступны",
"many": "{count} групп CelesTrak недоступны",
"other": "{count} группы CelesTrak недоступны"
},
"Malformed launch snapshot": "Некорректный снимок запусков",
"Malformed perimeter snapshot": "Некорректный снимок периметров",
"Overpass returned an incomplete camera response": "Overpass вернул неполный ответ о камерах",
"Fly below {km} km to a covered region": "Опуститесь ниже {km} км над регионом с покрытием",
"No feed here yet · {n} regions available": {
"one": "Здесь пока нет потока данных · доступен {n} регион",
"few": "Здесь пока нет потока данных · доступно {n} региона",
"many": "Здесь пока нет потока данных · доступно {n} регионов",
"other": "Здесь пока нет потока данных · доступно {n} региона"
},
"Wind speed field unavailable": "Поле скорости ветра недоступно",
"Decoder feeds": "Потоки декодера",
"WebUSB + decoder feeds": "WebUSB + потоки декодера",
"opening receiver": "открытие приёмника",
"checking decoder feeds": "проверка потоков декодера",
"listening": "приём",
"USB error": "ошибка USB",
"{count} heard": {
"one": "слышно бортов: {count}",
"few": "слышно бортов: {count}",
"many": "слышно бортов: {count}",
"other": "слышно бортов: {count}"
},
"{rate} msg/s": "{rate} сообщ./с",
"USB {rate} msg/s": "USB {rate} сообщ./с",
"{count} feeds live": {
"one": "{count} поток онлайн",
"few": "{count} потока онлайн",
"many": "{count} потоков онлайн",
"other": "{count} потока онлайн"
},
"feeds {names} stale": {
"one": "поток {names} устарел",
"few": "потоки {names} устарели",
"many": "потоки {names} устарели",
"other": "потоки {names} устарели"
},
"feeds {names} unreachable": {
"one": "поток {names} недоступен",
"few": "потоки {names} недоступны",
"many": "потоки {names} недоступны",
"other": "потоки {names} недоступны"
},
"feeds {names} invalid": {
"one": "поток {names} некорректен",
"few": "потоки {names} некорректны",
"many": "потоки {names} некорректны",
"other": "потоки {names} некорректны"
},
"unsaved provider keys": "несохранённые ключи провайдеров",
"Exit cockpit to fly to an aircraft": "Выйдите из кабины, чтобы перелететь к самолёту",
"Exit cockpit to fly to a satellite": "Выйдите из кабины, чтобы перелететь к спутнику",
"Exit cockpit to frame contacts": "Выйдите из кабины, чтобы показать контакты в кадре"
}
+2
View File
@@ -88,6 +88,7 @@
"Soul",
"Techno",
"Trance",
"USB {rate} msg/s",
"VISUAL",
"VS",
"WHOT/BHOT",
@@ -107,6 +108,7 @@
"panel",
"view band|GLOBAL",
"voice|ERROR",
"{rate} msg/s",
"{speed} kn",
"{type} SPD: {speed} HDG: {heading}",
"{value} KM",