diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 0bbf132fa..e5cef57f8 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -1176,7 +1176,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/ui/unitLabels.js" ], "external": ["cesium"] }, diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index ed910dfff..a71a61cde 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -2075,5 +2075,12 @@ "Trance": "Trance", "World": "Músicas del mundo", "{speed}KT": "{speed} KN", - "{heading}DEG": "{heading}°" + "{heading}DEG": "{heading}°", + "At least {count}": "Al menos {count}", + "Named areas ({n})": "Zonas con nombre ({n})", + "{distance} FLIGHT / VESSEL WINDOW": "VENTANA DE VUELOS / BUQUES DE {distance}", + "mapped matches within {km} km of the subject": "coincidencias mapeadas a menos de {km} km del objetivo", + "none mapped within {km} km; not a complete 250 km survey": "ninguna mapeada a menos de {km} km; no es un barrido completo de 250 km", + "mapped matches from the loaded viewport": "coincidencias mapeadas en la vista cargada", + "viewport feed is not a complete 250 km survey": "los datos de la vista no son un barrido completo de 250 km" } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 52325ffe3..3608e5fad 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -2107,5 +2107,12 @@ "Trance": "Транс", "World": "Музыка мира", "{speed}KT": "{speed} УЗ", - "{heading}DEG": "{heading}°" + "{heading}DEG": "{heading}°", + "At least {count}": "Не менее {count}", + "Named areas ({n})": "Именованные зоны ({n})", + "{distance} FLIGHT / VESSEL WINDOW": "ОКНО РЕЙСОВ / СУДОВ {distance}", + "mapped matches within {km} km of the subject": "совпадения на карте в радиусе {km} км от объекта", + "none mapped within {km} km; not a complete 250 km survey": "на карте нет в радиусе {km} км; это не полный обзор 250 км", + "mapped matches from the loaded viewport": "совпадения на карте в загруженной области", + "viewport feed is not a complete 250 km survey": "данные видимой области — не полный обзор 250 км" } diff --git a/src/layers/awareness/panel.js b/src/layers/awareness/panel.js index 3539c437f..5fe2a0ac8 100644 --- a/src/layers/awareness/panel.js +++ b/src/layers/awareness/panel.js @@ -1,9 +1,9 @@ import { formatAwarenessLabel, - formatAwarenessDistance, AWARENESS_RADIUS_M, } from '../../data/militaryAwarenessEngine.js'; -import { t } from '../../i18n/index.js'; +import { formatNumber, t, translateKnown } from '../../i18n/index.js'; +import { formatQuantity } from '../../ui/unitLabels.js'; import { AWARENESS_PAGE_SIZE, AWARENESS_PAGE_ROTATE_MS } from './policy.js'; /** Format a cohort total without presenting a retained cap as exact. */ @@ -15,6 +15,33 @@ export function formatAwarenessCount(summary = {}) { : count; } +/** The count as the panel shows it, in the UI language. */ +function presentAwarenessCount(summary = {}) { + if (!Number.isFinite(summary.count)) return '?'; + const count = formatNumber(summary.count); + return summary.complete === false || summary.truncated === true + ? t('At least {count}', { count }) + : count; +} + +/** A distance as the panel shows it: 850 m, 4.6 km, 120 km, in the UI language. */ +function presentAwarenessDistance(meters) { + if (!Number.isFinite(meters) || meters < 0) return '—'; + if (meters < 1000) return formatQuantity(Math.round(meters), 'm'); + const digits = meters < 10000 ? 1 : 0; + return formatQuantity(meters / 1000, 'km', { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }); +} + +/** The cohort's reason, kept English in data, shown in the UI language. */ +function presentAwarenessReason(summary = {}) { + return summary.reasonTemplate + ? t(summary.reasonTemplate, summary.reasonValues) + : translateKnown(summary.reason || ''); +} + export function createPanel({ state: layerState, services, parts, source }) { function ensurePanel() { if (layerState.panel) return layerState.panel; @@ -71,7 +98,7 @@ export function createPanel({ state: layerState, services, parts, source }) { function rowHtml(cohort) { const summary = cohort.summary; - const count = formatAwarenessCount(summary); + const count = presentAwarenessCount(summary); const page = layerState.cohortPages.get(cohort.id) || 0; const nearest = summary.nearest .slice(page, page + AWARENESS_PAGE_SIZE) @@ -79,10 +106,10 @@ export function createPanel({ state: layerState, services, parts, source }) { const label = formatAwarenessLabel(item); const targetId = item.icao24 || item.mmsi || item.id; if (!targetId) { - return `