mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(i18n): Contacts panel text and distances in the UI language
The Contacts panel builds its markup in a variable before innerHTML, so the unmarked-text check missed it: the window caption, "Named areas", the "at least" count and each cohort's reason stayed English, and distances read "4.6 km" in every language. The reason stays English in the cohort summary, which the voice analyst reads too; it now also carries its template, and the panel shows it in the UI language. Distances and counts use the locale's number format and unit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
03207ca0fc
commit
896952c085
5 files changed
+79
-23
No files matched your search
@@ -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"]
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 км"
|
||||
}
|
||||
@@ -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 `<li><span class="military-awareness-target unavailable" aria-label="${t('Unavailable')}">${escapeHtml(label)} <span>${formatAwarenessDistance(item.distanceM)}</span></span></li>`;
|
||||
return `<li><span class="military-awareness-target unavailable" aria-label="${t('Unavailable')}">${escapeHtml(label)} <span>${presentAwarenessDistance(item.distanceM)}</span></span></li>`;
|
||||
}
|
||||
const accessibleLabel = label === '—' ? t('Unavailable') : label;
|
||||
return `<li><button type="button" class="military-awareness-target" data-awareness-layer="${escapeHtml(cohort.id)}" data-awareness-id="${escapeHtml(targetId)}" aria-label="${escapeHtml(t('Focus {label}', { label: accessibleLabel }))}">${escapeHtml(label)} <span>${formatAwarenessDistance(item.distanceM)}</span></button>${namedAreasHtml(item.memberNames)}</li>`;
|
||||
return `<li><button type="button" class="military-awareness-target" data-awareness-layer="${escapeHtml(cohort.id)}" data-awareness-id="${escapeHtml(targetId)}" aria-label="${escapeHtml(t('Focus {label}', { label: accessibleLabel }))}">${escapeHtml(label)} <span>${presentAwarenessDistance(item.distanceM)}</span></button>${namedAreasHtml(item.memberNames)}</li>`;
|
||||
})
|
||||
.join('');
|
||||
const pageCount = Math.max(
|
||||
@@ -96,7 +123,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
const coverage = cohort.coverage ? ` · ${escapeHtml(cohort.coverage)}` : '';
|
||||
return `<section class="military-awareness-row ${summary.relationship.toLowerCase()}">
|
||||
<div><strong>${escapeHtml(cohort.label)}</strong><b aria-live="polite">${count}${pageLabel}</b></div>
|
||||
<small>${escapeHtml(cohort.source)}${coverage} · ${escapeHtml(summary.reason)}</small>
|
||||
<small>${escapeHtml(cohort.source)}${coverage} · ${escapeHtml(presentAwarenessReason(summary))}</small>
|
||||
${nearest ? `<ul>${nearest}</ul>` : ''}
|
||||
</section>`;
|
||||
}
|
||||
@@ -176,7 +203,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
|
||||
function namedAreasHtml(names) {
|
||||
return names?.length
|
||||
? `<details class="military-awareness-names"><summary>Named areas (${names.length})</summary><div>${names.map((name) => escapeHtml(name)).join('<br>')}</div></details>`
|
||||
? `<details class="military-awareness-names"><summary>${escapeHtml(t('Named areas ({n})', { n: formatNumber(names.length) }))}</summary><div>${names.map((name) => escapeHtml(name)).join('<br>')}</div></details>`
|
||||
: '';
|
||||
}
|
||||
|
||||
@@ -184,7 +211,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
if (!layerState.enabled || !layerState.results) return hidePanel();
|
||||
const panel = ensurePanel();
|
||||
const { subject, cohorts } = layerState.results;
|
||||
const markup = `<div class="military-awareness-subject">${escapeHtml(subject.label)} · ${formatAwarenessDistance(AWARENESS_RADIUS_M)} FLIGHT / VESSEL WINDOW</div>
|
||||
const markup = `<div class="military-awareness-subject">${escapeHtml(subject.label)} · ${escapeHtml(t('{distance} FLIGHT / VESSEL WINDOW', { distance: presentAwarenessDistance(AWARENESS_RADIUS_M) }))}</div>
|
||||
${namedAreasHtml(subject.memberNames)}
|
||||
${navigationControlsHtml()}
|
||||
${cohorts.map(rowHtml).join('')}
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import { AWARENESS_RADIUS_M } from '../../data/militaryAwarenessEngine.js';
|
||||
|
||||
// The reason stays English for the voice analyst; the Contacts panel shows it
|
||||
// in the UI language from `reasonTemplate`. Kept local so this module stays
|
||||
// free of the i18n import.
|
||||
const msg = (text) => text;
|
||||
const fillTemplate = (template, values = {}) =>
|
||||
template.replace(/\{(\w+)\}/g, (_, name) => String(values[name]));
|
||||
const withReason = (summary, template, values) => ({
|
||||
...summary,
|
||||
reason: fillTemplate(template, values),
|
||||
reasonTemplate: template,
|
||||
reasonValues: values,
|
||||
});
|
||||
import { DEPENDENCIES, AWARENESS_QUERY_LIMIT } from './policy.js';
|
||||
|
||||
export function createQueries({ state: layerState, services, parts, source }) {
|
||||
@@ -119,19 +132,20 @@ export function createQueries({ state: layerState, services, parts, source }) {
|
||||
// A subject window is its own bounded area: name it instead of the viewport.
|
||||
if (coverage?.kind === 'subject' && Number.isFinite(coverage.radiusM)) {
|
||||
const km = Math.round(coverage.radiusM / 1000);
|
||||
return {
|
||||
...summary,
|
||||
reason: summary.count
|
||||
? `mapped matches within ${km} km of the subject`
|
||||
: `none mapped within ${km} km; not a complete 250 km survey`,
|
||||
};
|
||||
return withReason(
|
||||
summary,
|
||||
summary.count
|
||||
? msg('mapped matches within {km} km of the subject')
|
||||
: msg('none mapped within {km} km; not a complete 250 km survey'),
|
||||
{ km },
|
||||
);
|
||||
}
|
||||
return {
|
||||
...summary,
|
||||
reason: summary.count
|
||||
? 'mapped matches from the loaded viewport'
|
||||
: 'viewport feed is not a complete 250 km survey',
|
||||
};
|
||||
return withReason(
|
||||
summary,
|
||||
summary.count
|
||||
? msg('mapped matches from the loaded viewport')
|
||||
: msg('viewport feed is not a complete 250 km survey'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user