refactor(i18n): key messages by English text; add Russian

Replace the semantic keys with the English source text, so code reads as the
UI does and English needs no catalog: t('Save keys'), tn(count, one, other)
for plurals, msg() for text stored in data, and bare data-i18n* markup that
translates the element's own English. Other languages are one flat JSON each.

- Simplify src/i18n: drop namespaces, the configurable locale pair
  (GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE) and its build defines. Locale
  comes from ?lang=, the stored choice, then browser languages.
- Add scripts/i18n.mjs (npm run i18n): extracts every message from t/tn/msg
  and markup, and fails on missing, unused, placeholder-mismatched or
  wrongly-shaped plural entries. npm test runs it via catalog.test.mjs.
- Add Russian (src/i18n/locales/ru.json, 833 messages), started from the
  community catalog in #312 and reviewed against the current English.
- Fix data-i18n placed on parents that would wipe their children (cockpit
  LEVEL and local weather cells, first-run EXPLORE MANUALLY tile), and a
  doubled degree sign in the TRACK ACQUIRED signal.
- Rewrite docs/TRANSLATORS.md for the new workflow; drop sections the earlier
  merge resurrected in docs/CURRENT-STATE.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-12 13:48:56 +04:00
1 parent dfbaeafbd8
commit 13efd411b6
112 files changed
+4419 -4856

No files matched your search

+20 -18
View File
@@ -26,7 +26,7 @@ import {
getSceneAppendRecipeById,
} from './recipes.js';
import { sceneLayerPlan, sceneRequiresContextModeExit } from './scenePolicy.js';
import { t } from '../i18n/index.js';
import { t, msg } from '../i18n/index.js';
import { createSceneDataPacks } from './dataPacks/controller.js';
import { createSceneInteractions } from './interactions.js';
import { createSceneSharing } from './sharing.js';
@@ -67,11 +67,11 @@ import {
// boundary + persisted data); only the presentation of an UNRENAMED built-in
// localizes, so an operator's renamed scene is never masked by the catalog.
const RECIPE_TITLE_KEYS = Object.freeze({
'flights-radar': 'setup.scenes.recipe.flightsRadar',
'orbital-watch': 'setup.scenes.recipe.orbitalWatch',
'thermal-threats': 'setup.scenes.recipe.thermalThreats',
'city-overload': 'setup.scenes.recipe.cityOverload',
'omniscience-pullback': 'setup.scenes.recipe.omnisciencePullback',
'flights-radar': msg('Global Flights Radar'),
'orbital-watch': msg('Orbital Watch'),
'thermal-threats': msg('Thermal Threat Board'),
'city-overload': msg('City Overload'),
'omniscience-pullback': msg('Omniscience Pullback'),
});
/**
@@ -198,7 +198,7 @@ export class SceneDirector {
this._presentation = {
status: this._storageReadError
? 'Saved project could not be read; storage preserved. Import a valid file to resume saving.'
: t('setup.scenes.statusReady'),
: t('Ready'),
progress: 0,
runtime: '',
playbackActive: false,
@@ -396,7 +396,7 @@ export class SceneDirector {
_shotOutcome(type, scene, shot, index = scene.shots.indexOf(shot)) {
if (type === 'shot-loaded')
this._presentation.status = t('setup.scenes.status.loaded', {
this._presentation.status = t('Loaded: {scene} / {shot}', {
scene: scene.title,
shot: shot.title,
});
@@ -954,14 +954,14 @@ export class SceneDirector {
const camera = this.styleManager.getCameraState();
if (!camera) {
this._updateStatus(t('setup.scenes.status.captureCameraNotReady'));
this._updateStatus(t('Cannot capture shot: camera not ready'));
return;
}
const shot = normalizeShot(
{
id: uid('shot'),
title: t('setup.scenes.status.shotTitleDefault', {
title: t('Shot {n}', {
n: scene.shots.length + 1,
}),
durationSec: DEFAULT_SHOT_DURATION_SEC,
@@ -978,7 +978,7 @@ export class SceneDirector {
this._saveProject();
this._shotOutcome('shot-captured', scene, shot);
this._updateStatus(
t('setup.scenes.status.captured', {
t('Captured: {scene} / {shot}', {
scene: scene.title,
shot: shot.title,
}),
@@ -995,7 +995,7 @@ export class SceneDirector {
const shot = scene.shots.find((item) => item.id === this._selectedShotId);
if (!shot) {
this._updateStatus(t('setup.scenes.status.selectShotFirst'));
this._updateStatus(t('Select a shot first'));
return;
}
@@ -1011,7 +1011,7 @@ export class SceneDirector {
this._saveProject();
this._shotOutcome('shot-updated', scene, shot);
this._updateStatus(
t('setup.scenes.status.updated', {
t('Updated: {scene} / {shot}', {
scene: scene.title,
shot: shot.title,
}),
@@ -1610,7 +1610,7 @@ export class SceneDirector {
this._claimingCamera = false;
}
if (claimed === false) {
this._updateStatus(t('setup.scenes.status.cameraUnavailable'));
this._updateStatus(t('Camera unavailable — exit cockpit first'));
return false;
}
return true;
@@ -1745,7 +1745,7 @@ export class SceneDirector {
if (!queue.length) return { started: false, reason: 'scene-complete' };
}
if (!queue.length) {
this._updateStatus(t('setup.scenes.status.noShotsToRun'));
this._updateStatus(t('No shots to run'));
return { started: false, reason: 'no-shots' };
}
@@ -1829,7 +1829,7 @@ export class SceneDirector {
});
} catch (error) {
this._updateStatus(
t('setup.scenes.status.runError', {
t('Error: {message}', {
message: error.message || 'run failed',
}),
);
@@ -2006,7 +2006,7 @@ export class SceneDirector {
link.click();
link.remove();
URL.revokeObjectURL(url);
this._presentation.status = t('setup.scenes.status.projectExported');
this._presentation.status = t('Project exported');
this._publish({ type: 'project-exported', project: this._project });
}
@@ -2268,7 +2268,9 @@ export class SceneDirector {
`[Scenes] Could not exit ${mode}:`,
result.error || 'unknown reason',
);
this._updateStatus(t('setup.scenes.status.contextExitFailed', { mode }));
this._updateStatus(
t('Could not exit {mode} — scene layers may be refused', { mode }),
);
this._logEvent('context_mode_exit_failed', {
mode,
error: result.error || null,
+2 -2
View File
@@ -16,7 +16,7 @@ export function createScenePlaybackAdapter(director, defaultShotDurationSec) {
director._renderSceneSelect();
director._renderShotList();
director._updateStatus(
t('setup.scenes.status.runningShot', {
t('Running {index}/{total}: {scene} / {shot}', {
index: index + 1,
total,
scene: scene.title,
@@ -78,7 +78,7 @@ export function createScenePlaybackAdapter(director, defaultShotDurationSec) {
},
complete() {
director._setProgress(1);
director._updateStatus(t('setup.scenes.status.runComplete'));
director._updateStatus(t('Scene run complete'));
director._logEvent('scene_run_complete', {});
},
};