fix(i18n): map cards and wind readout in the UI language; final review wording

A final review of both PDFs found text the earlier checks missed:

- Map cards: fire confidence (high/nominal/low) and age (24h), vessel
  types (TOWING, TANKER) and installation classes (BASE, AIRFIELD) showed
  in English. They now go through the catalog; the data stays English.
- The wind readout kept "10-14 14:00 UTC" and "29.93°N 97.74°W"; it now
  uses the locale's date, decimal separator and hemisphere letters.
- Catalog: one term per concept (label/подпись, basemap/подложка,
  PROVIDER CONTROLS, HEARD, zoom in, REGIONAL NEWS), calques and broken
  grammar in the scene editor, provider settings and Bhote Koshi notes,
  and Spanish wording that read wrong ("restringidas por el proveedor",
  "Incluye … no se incluyen", "justo ahora", "Sal").

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-14 18:41:28 +04:00
1 parent eed05721f2
commit 8c844d7741
11 files changed
+251 -134

No files matched your search

+31 -5
View File
@@ -332,11 +332,11 @@ export function createModel({
function buildSelectedFireCard(fire, nowMs) {
const meta = [
t('{value} conf', {
value: confidenceBucket(fire.confidence),
value: confidenceLabel(confidenceBucket(fire.confidence)),
}),
];
if (fire.acqMs > 0) {
const age = formatAge(nowMs - fire.acqMs);
const age = formatAgeLabel(nowMs - fire.acqMs);
if (age) meta.push(t('{value} ago', { value: age }));
}
const sat = satelliteShortName(fire.satellite);
@@ -374,9 +374,9 @@ export function createModel({
function buildFireCard(candidate, nowMs) {
const fire = candidate.fire;
const meta = [confidenceBucket(fire.confidence)];
const meta = [confidenceLabel(confidenceBucket(fire.confidence))];
if (fire.acqMs > 0) {
const age = formatAge(nowMs - fire.acqMs);
const age = formatAgeLabel(nowMs - fire.acqMs);
if (age) meta.push(age);
}
const sat = satelliteShortName(fire.satellite) || fire.sensor;
@@ -409,7 +409,7 @@ export function createModel({
const noun = cell.count === 1 ? t('FIRE') : t('FIRES');
const parts = [t('max {value} MW', { value: formatFrp(cell.maxFrp) })];
if (cell.newestAcqMs > 0) {
const age = formatAge(nowMs - cell.newestAcqMs);
const age = formatAgeLabel(nowMs - cell.newestAcqMs);
if (age) parts.push(t('new {value}', { value: age }));
}
return {
@@ -503,6 +503,32 @@ export function createModel({
return t('{count}h ago', { count: Math.round(minutes / 60) });
}
/** A confidence bucket as a card shows it, in the UI language. */
function confidenceLabel(bucket) {
switch (bucket) {
case 'high':
return t('high', { context: 'fire confidence' });
case 'nominal':
return t('nominal', { context: 'fire confidence' });
case 'low':
return t('low', { context: 'fire confidence' });
default:
return bucket;
}
}
/** formatAge for a card: "<1h", "24h", "3d" in the UI language ("24 ч"). */
function formatAgeLabel(deltaMs) {
if (!Number.isFinite(deltaMs) || deltaMs < 0) return '';
const hours = deltaMs / 3600000;
if (hours < 1) return t('<1h', { context: 'age' });
if (hours < 48)
return t('{count}h', { context: 'age', count: Math.round(hours) });
return t('{count}d', { context: 'age', count: Math.round(hours / 24) });
}
/** Normalized 0..1 confidence → low/nominal/high display bucket. */
function confidenceBucket(confidence) {
+29
View File
@@ -0,0 +1,29 @@
import { t } from '../../i18n/index.js';
/**
* An installation class as a map card shows it: AIRFIELD, NAVAL BASE…, in
* the UI language. The class itself stays an English identifier in data.
* @param {string} klass
* @returns {string}
*/
export function installationClassLabel(klass) {
const key = String(klass || 'installation');
switch (key) {
case 'airfield':
return t('AIRFIELD', { context: 'installation class' });
case 'naval_base':
return t('NAVAL BASE', { context: 'installation class' });
case 'range':
return t('RANGE', { context: 'installation class' });
case 'military_land':
return t('MILITARY LAND', { context: 'installation class' });
case 'base':
return t('BASE', { context: 'installation class' });
case 'places_candidate':
return t('PLACES CANDIDATE', { context: 'installation class' });
case 'installation':
return t('INSTALLATION', { context: 'installation class' });
default:
return key.replaceAll('_', ' ').toUpperCase();
}
}
+2 -3
View File
@@ -1,3 +1,4 @@
import { installationClassLabel } from './classLabel.js';
import * as Cesium from 'cesium';
import {
MILITARY_LABEL_CAP,
@@ -177,9 +178,7 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
marker.entry.selected = record.id === state.selectedId;
marker.entry.details = marker.entry.selected
? [
String(record.class || 'installation')
.replaceAll('_', ' ')
.toUpperCase(),
installationClassLabel(record.class),
...(record.memberNames || []).slice(0, 3),
]
: [];
+2 -3
View File
@@ -1,3 +1,4 @@
import { installationClassLabel } from './classLabel.js';
import * as Cesium from 'cesium';
import { t } from '../../i18n/index.js';
import { LAYER_ID, MAX_RENDERED, COLOR_BY_CLASS } from './policy.js';
@@ -273,9 +274,7 @@ export function createRendering({
entity.gevLabelModel = {
title: record.name || t('MAPPED INSTALLATION'),
details: [
String(record.class || 'installation')
.replaceAll('_', ' ')
.toUpperCase(),
installationClassLabel(record.class),
...(record.memberNames || []).slice(0, 3),
],
accent: COLOR_BY_CLASS[record.class] || '#9ca6b0',
+2 -2
View File
@@ -1,6 +1,6 @@
import {
accentForVesselType,
normalizeVesselType,
displayVesselType,
} from '../../data/vesselLabels.js';
import { formatNumber, t } from '../../i18n/index.js';
@@ -130,7 +130,7 @@ export function createCards({
/** Uppercased, card-width-bounded AIS type (empty string when unknown). */
function vesselTypeShort(record) {
return normalizeVesselType(record.type).toUpperCase().slice(0, 14);
return displayVesselType(record.type).toUpperCase().slice(0, 14);
}
/**
+37 -3
View File
@@ -1,5 +1,5 @@
import { formatWindSpeed, WIND_UNITS } from './inspection.js';
import { t } from '../../i18n/index.js';
import { formatNumber, formatUtcDayMonth, t } from '../../i18n/index.js';
import { formatQuantity, unitLabel } from '../../ui/unitLabels.js';
/** Reformat a captured sample; changing units never samples the map again. */
@@ -62,12 +62,46 @@ function windText(reading) {
: `${speed}${t(' from {direction}', { direction: t(reading.from) })}`;
}
/** "29.93°N · 97.74°W" from the English reading, in the active language. */
function displayCoordinates(coordinates) {
const match = /^(\d+(?:\.\d+)?)°([NS]) · (\d+(?:\.\d+)?)°([EW])$/.exec(
coordinates,
);
if (!match) return coordinates.replace(' · ', ' ');
const part = (value, hemisphere) => {
const decimals = value.split('.')[1]?.length || 0;
const amount = formatNumber(Number(value), {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
useGrouping: false,
});
switch (hemisphere) {
case 'N':
return t('{value}°N', { value: amount });
case 'S':
return t('{value}°S', { value: amount });
case 'E':
return t('{value}°E', { value: amount });
default:
return t('{value}°W', { value: amount });
}
};
return `${part(match[1], match[2])} ${part(match[3], match[4])}`;
}
/** "2026-10-14 14:00 UTC" from the reading, as "10-14 14:00 UTC" / "14.10 …". */
function displayValidTime(validTime) {
const match = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) UTC$/.exec(validTime || '');
if (!match) return validTime?.replace(/^\d{4}-/, '');
return `${formatUtcDayMonth(match[1])} ${match[2]} UTC`;
}
/** Portable result block consumed by the WEATHER card. */
export function windReadingResult(reading) {
const coordinates =
reading.coordinates === 'No surface reading'
? t('No surface reading')
: reading.coordinates.replace(' · ', ' ');
: displayCoordinates(reading.coordinates);
return {
id: 'reading',
label: t('WIND AT {coordinates}', { coordinates }),
@@ -80,7 +114,7 @@ export function windReadingResult(reading) {
id: 'meta',
text: t('{model} · valid {time}', {
model: reading.model,
time: reading.validTime?.replace(/^\d{4}-/, ''),
time: displayValidTime(reading.validTime),
}),
},
...(reading.scalarValue