mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
i18n: fix review findings and polish ru/es strings
Fixes the 51 P0–P2 findings from the full review of the i18n branch and the issues found while checking every new string on screen in ru and es. - Translate errors and statuses that still reached ru/es in English: feed and tile errors, provider statuses, the ALPR chip, the traffic mode chip, voice plan step labels, fire feed values, receiver bands, scene save reasons and the Cesium attribution link. - Show preset city names in the UI language (src/ui/placeNames.js); data and everything sent to the model stay English. - Share layer names between the layer panel, HUD and voice card (src/ui/layerNames.js). - Keep long ru/es strings whole: voice card error lines, analyst titles, the voice tier status, the Nepal evidence button, the ALPR retry chip. English layout is unchanged. - Ask before a language switch drops unsaved work. - Catalog fixes: glossary terms (пуск, пресет), gender agreement and consistent "peticiones" in es, fewer calques in both languages. - Fix English text that was itself wrong (e.g. "Exit cockpit to fly to a weather", raw tool ids in voice plan steps). Catalogs: es/ru 2535 messages each, complete. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8029747b7e
commit
00704ddc89
212 files changed
+9871
-909
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
accentForVesselType,
|
||||
displayVesselType,
|
||||
normalizeVesselType,
|
||||
} from '../../data/vesselLabels.js';
|
||||
import { formatNumber, t } from '../../i18n/index.js';
|
||||
|
||||
@@ -127,10 +128,18 @@ export function createCards({
|
||||
return `vessel:unkeyed:${name}:${lat}:${lon}`;
|
||||
}
|
||||
|
||||
/** Uppercased, card-width-bounded AIS type (empty string when unknown). */
|
||||
/**
|
||||
* Uppercased AIS type for the card (empty string when unknown). Free feed
|
||||
* text is bounded to the card width; a translated type token is a short
|
||||
* catalog word and shows whole, never cut mid-word ("AUXILIAR DE PUERTO").
|
||||
*/
|
||||
|
||||
function vesselTypeShort(record) {
|
||||
return displayVesselType(record.type).toUpperCase().slice(0, 14);
|
||||
const feedType = normalizeVesselType(record.type);
|
||||
const shown = displayVesselType(record.type);
|
||||
return shown === feedType
|
||||
? feedType.toUpperCase().slice(0, 14)
|
||||
: shown.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// C6: a vessel with an ambient card gets no detection callout under it (the
|
||||
// card already shows name, speed and course); the speed unit of a callout
|
||||
// follows the UI language.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import aisLiveVesselsLayer, {
|
||||
_setVesselOverlayHostForTest,
|
||||
_setVesselStateForTest,
|
||||
_updateVesselCardsForTest,
|
||||
} from '../../data/aisLiveVessels.js';
|
||||
import { setLocale } from '../../i18n/index.js';
|
||||
|
||||
const record = (mmsi, name, screen, z = 0) => ({
|
||||
name,
|
||||
mmsi,
|
||||
type: 'Cargo',
|
||||
destination: '',
|
||||
speed: 9.5,
|
||||
course: 80,
|
||||
heading: 80,
|
||||
lastPositionUtc: '',
|
||||
missedRefreshes: 0,
|
||||
position: { x: 0, y: 0, z, screen },
|
||||
billboard: null,
|
||||
});
|
||||
|
||||
function detectable(cameraZ) {
|
||||
const carded = record('200', 'GOLDEN BEAR', { x: 40, y: 30 });
|
||||
const plain = record('300', 'BAY PILOT', { x: 41, y: 31 });
|
||||
const originalProjection = Cesium.SceneTransforms.worldToWindowCoordinates;
|
||||
Cesium.SceneTransforms.worldToWindowCoordinates = (_scene, position) =>
|
||||
position.screen;
|
||||
_setVesselOverlayHostForTest({
|
||||
setEntries() {},
|
||||
setVisible() {},
|
||||
clearSource() {},
|
||||
});
|
||||
_setVesselStateForTest({
|
||||
viewer: {
|
||||
scene: {
|
||||
canvas: { clientWidth: 1600, clientHeight: 900 },
|
||||
camera: { positionWC: new Cesium.Cartesian3(0, 0, cameraZ) },
|
||||
},
|
||||
},
|
||||
records: [carded, plain],
|
||||
billboardCollection: { show: true, remove() {} },
|
||||
});
|
||||
try {
|
||||
// Both share one 118 px grid cell, so only one of them gets a card.
|
||||
_updateVesselCardsForTest([carded, plain]);
|
||||
return Object.fromEntries(
|
||||
aisLiveVesselsLayer
|
||||
.getDetectableObjects()
|
||||
.map((object) => [object.id, object]),
|
||||
);
|
||||
} finally {
|
||||
Cesium.SceneTransforms.worldToWindowCoordinates = originalProjection;
|
||||
_setVesselStateForTest({ enabled: false });
|
||||
_setVesselOverlayHostForTest();
|
||||
}
|
||||
}
|
||||
|
||||
test('a vessel with a card on screen draws no detection label', () => {
|
||||
const objects = detectable(2_500);
|
||||
const hidden = Object.values(objects).filter((object) => object.hideLabel);
|
||||
assert.equal(hidden.length, 1, 'exactly the carded vessel hides its label');
|
||||
const shown = Object.values(objects).filter((object) => !object.hideLabel);
|
||||
assert.equal(shown.length, 1, 'the vessel without a card keeps its label');
|
||||
assert.equal(hidden[0].skipLabel, false, 'not treated as the tracked vessel');
|
||||
});
|
||||
|
||||
test('past the card fade the detection label comes back', () => {
|
||||
const objects = detectable(4_000_000);
|
||||
assert.ok(Object.values(objects).every((object) => !object.hideLabel));
|
||||
});
|
||||
|
||||
test('ru: the callout speed unit is translated', () => {
|
||||
setLocale('ru');
|
||||
try {
|
||||
const objects = detectable(4_000_000);
|
||||
assert.equal(objects['GOLDEN BEAR'].metric, '10 уз');
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
assert.equal(detectable(4_000_000)['GOLDEN BEAR'].metric, '10 kn');
|
||||
});
|
||||
@@ -1,6 +1,10 @@
|
||||
import { AIS_FIRST_CONNECT_LABEL } from './recordPolicy.js';
|
||||
import { viewAreaMovedEnough } from './viewArea.js';
|
||||
|
||||
// A portable export (package.json): no i18n here. The text stays English data
|
||||
// and the layer panel shows it translated.
|
||||
const msg = (text) => text;
|
||||
|
||||
/** Own source requests and classified feed state through explicit operations. */
|
||||
export function createIngestion({
|
||||
feed,
|
||||
@@ -69,7 +73,7 @@ export function createIngestion({
|
||||
ownsAisRequest(requestController, requestSessionId) &&
|
||||
error?.name !== 'AbortError'
|
||||
) {
|
||||
markUnavailable(error?.message || 'AIS live load failed');
|
||||
markUnavailable(error?.message || msg('AIS live load failed'));
|
||||
console.warn('[Data:ais-live-vessels]', feed.error, error);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VESSEL_OVERLAY_SOURCE_ID } from '../../data/vesselLabels.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { msg, t } from '../../i18n/index.js';
|
||||
import { AIS_FIRST_CONNECT_GRACE_MS, AIS_HEALTHY_STATUSES } from './policy.js';
|
||||
|
||||
export function createLifecycle({
|
||||
@@ -82,7 +82,7 @@ export function createLifecycle({
|
||||
|
||||
function markAisUnavailable(reason) {
|
||||
settleFirstConnectPhase('unavailable');
|
||||
state.feed.error = reason || 'AIS live load failed';
|
||||
state.feed.error = reason || msg('AIS live load failed');
|
||||
state.feed.stale = state.feed.count > 0;
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ export function createLifecycle({
|
||||
state.lastFocusUpdate = 0;
|
||||
state.activeFocusCount = 0;
|
||||
state.activeLabelCount = 0;
|
||||
state.cardedRecords = new Set();
|
||||
state.selectedRecord = null;
|
||||
state.trail = null;
|
||||
state.trailPositions = [];
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
AIS_FIRST_CONNECT_LABEL,
|
||||
} from './recordPolicy.js';
|
||||
import { VESSEL_LABEL_GRID_PX } from '../../data/vesselLabels.js';
|
||||
import { msg } from '../../i18n/index.js';
|
||||
|
||||
export const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
|
||||
|
||||
@@ -71,12 +72,12 @@ export const DEFAULT_AIS_RUNTIME = Object.freeze({
|
||||
*/
|
||||
|
||||
export const AIS_STATUS_REASON = {
|
||||
'missing-key': 'AISSTREAM_API_KEY not set',
|
||||
unsupported: 'live feed unsupported',
|
||||
connecting: 'connecting to feed…',
|
||||
closed: 'feed disconnected',
|
||||
error: 'feed down',
|
||||
idle: 'feed idle',
|
||||
'missing-key': msg('AISSTREAM_API_KEY not set'),
|
||||
unsupported: msg('live feed unsupported'),
|
||||
connecting: msg('connecting to feed…'),
|
||||
closed: msg('feed disconnected'),
|
||||
error: msg('feed down'),
|
||||
idle: msg('feed idle'),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,20 +60,22 @@ export function createQueries({
|
||||
if (status === 'auth-failed') {
|
||||
// Actionable, not a countdown: retrying cannot fix a rejected credential,
|
||||
// so the chip asks the operator to do the one thing that can.
|
||||
return 'API key rejected — check AISSTREAM_API_KEY';
|
||||
return t('API key rejected — check AISSTREAM_API_KEY');
|
||||
}
|
||||
if (status === 'stale') {
|
||||
const silentSec = Math.round(Number(payload?.silentForMs) / 1000);
|
||||
return Number.isFinite(silentSec) && silentSec > 0
|
||||
? `feed silent ${silentSec}s — no AIS data`
|
||||
: 'feed silent — no AIS data';
|
||||
? t('feed silent {seconds}s — no AIS data', { seconds: silentSec })
|
||||
: t('feed silent — no AIS data');
|
||||
}
|
||||
const attempt = Number(payload?.reconnectAttempt);
|
||||
const suffix =
|
||||
Number.isFinite(attempt) && attempt >= 1 ? ` (attempt ${attempt})` : '';
|
||||
if (Number.isFinite(attempt) && attempt >= 1)
|
||||
return status === 'down'
|
||||
? t('feed down — retrying slowly (attempt {attempt})', { attempt })
|
||||
: t('reconnecting to feed… (attempt {attempt})', { attempt });
|
||||
return status === 'down'
|
||||
? `feed down — retrying slowly${suffix}`
|
||||
: `reconnecting to feed…${suffix}`;
|
||||
? t('feed down — retrying slowly')
|
||||
: t('reconnecting to feed…');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +108,8 @@ export function createQueries({
|
||||
typeof payload.error === 'string' && payload.error.trim()
|
||||
? payload.error.trim()
|
||||
: '';
|
||||
const reason = AIS_STATUS_REASON[status] || 'feed unavailable';
|
||||
// The reasons are English in policy.js (msg) and shown in the UI language.
|
||||
const reason = t(AIS_STATUS_REASON[status] || msg('feed unavailable'));
|
||||
return detail && !AIS_STATUS_REASON[status]
|
||||
? `${reason} (${detail})`
|
||||
: reason;
|
||||
@@ -468,7 +471,7 @@ export function createQueries({
|
||||
* @param {Object} [options={}] - Options from the detection system.
|
||||
* @param {number} [options.maxCount] - Maximum objects to return (defaults to all).
|
||||
* @param {number} [options.seed] - Seed offset for stride sampling.
|
||||
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
|
||||
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean, hideLabel: boolean}>}
|
||||
*/
|
||||
getDetectableObjects(options = {}) {
|
||||
if (
|
||||
@@ -489,6 +492,7 @@ export function createQueries({
|
||||
const start = seed % stride;
|
||||
|
||||
const selected = state.selectedRecord;
|
||||
const carded = state.cardedRecords;
|
||||
const result = [];
|
||||
for (let idx = 0; idx < records.length; idx += 1) {
|
||||
if ((idx - start) % stride !== 0) continue;
|
||||
@@ -503,6 +507,9 @@ export function createQueries({
|
||||
id: record.name || record.mmsi || 'VESSEL',
|
||||
type: 'SEA',
|
||||
skipLabel: record === selected,
|
||||
// An open ambient card already names the vessel with its speed and
|
||||
// course; a detection label under it would only cover them.
|
||||
hideLabel: carded?.has(record) === true,
|
||||
klass: record.type
|
||||
? String(record.type).toUpperCase().slice(0, 14)
|
||||
: undefined,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import {
|
||||
vesselTypeCss,
|
||||
vesselOverlayCohortLimit,
|
||||
VESSEL_OVERLAY_SOURCE_ID,
|
||||
applyVesselOverlayPolicy,
|
||||
VESSEL_CARD_FADE_DISTANCE_M,
|
||||
VESSEL_CARD_FADE_START_RATIO,
|
||||
} from '../../data/vesselLabels.js';
|
||||
import {
|
||||
cameraPoseSignature,
|
||||
@@ -365,6 +367,7 @@ export function createRendering({
|
||||
|
||||
if (!scene || !records.length || maxLabels <= 0) {
|
||||
state.activeLabelCount = entries.length;
|
||||
state.cardedRecords = new Set();
|
||||
publishVesselOverlayEntries(entries);
|
||||
return;
|
||||
}
|
||||
@@ -402,6 +405,9 @@ export function createRendering({
|
||||
if (screen) accepted.push({ x: screen.x, y: screen.y });
|
||||
}
|
||||
const ranked = [...cells.values()].sort((a, b) => b.score - a.score);
|
||||
// Vessels with an ambient card: the card already shows name, speed and
|
||||
// course, so the detection overlay draws no label of its own under it.
|
||||
const carded = new Set();
|
||||
for (const candidate of ranked) {
|
||||
if (entries.length >= maxLabels) break;
|
||||
if (
|
||||
@@ -414,8 +420,10 @@ export function createRendering({
|
||||
continue;
|
||||
accepted.push({ x: candidate.x, y: candidate.y });
|
||||
entries.push(components.cards.buildVesselCard(candidate.record));
|
||||
if (cardFullyShown(scene, candidate.record)) carded.add(candidate.record);
|
||||
}
|
||||
state.activeLabelCount = entries.length;
|
||||
state.cardedRecords = carded;
|
||||
publishVesselOverlayEntries(entries);
|
||||
}
|
||||
|
||||
@@ -448,7 +456,10 @@ export function createRendering({
|
||||
: '';
|
||||
return {
|
||||
...card,
|
||||
accessibilityLabel: `Focus vessel ${card.title}, MMSI ${mmsi}`,
|
||||
accessibilityLabel: t('Focus vessel {name}, MMSI {mmsi}', {
|
||||
name: card.title,
|
||||
mmsi,
|
||||
}),
|
||||
activate: () => {
|
||||
const record = state.records.byMmsi.get(mmsi);
|
||||
if (!record) return false;
|
||||
@@ -465,6 +476,22 @@ export function createRendering({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the host paints this ambient card at full strength: inside the
|
||||
* start of its distance fade (applyVesselOverlayPolicy). Farther out the
|
||||
* card fades away and the detection label stays the vessel's only text.
|
||||
*/
|
||||
function cardFullyShown(scene, record) {
|
||||
const camera = scene.camera?.positionWC;
|
||||
const visual = getVisual(record);
|
||||
const position = visual.billboard?.position || visual.position;
|
||||
if (!camera || !position) return true;
|
||||
return (
|
||||
Cesium.Cartesian3.distance(camera, position) <
|
||||
VESSEL_CARD_FADE_DISTANCE_M * VESSEL_CARD_FADE_START_RATIO
|
||||
);
|
||||
}
|
||||
|
||||
function labelPriority(record, selected) {
|
||||
if (record === selected) return 100000;
|
||||
let score = 0;
|
||||
|
||||
@@ -69,6 +69,8 @@ export function createVesselState({ source, services }) {
|
||||
/** Sprites whose animated emphasis remains outside the 1.0 deadband. */
|
||||
activeFocusCount: 0,
|
||||
activeLabelCount: 0,
|
||||
/** @type {Set<Object>} Records that have an ambient card this pass. */
|
||||
cardedRecords: new Set(),
|
||||
selectedRecord: null,
|
||||
/** @type {{setPositions: Function, clear: Function, destroy: Function}|null} Selected-vessel fading trail */
|
||||
trail: null,
|
||||
|
||||
Reference in new issue
Block a user