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

+18 -17
View File
@@ -5,7 +5,7 @@ import {
DISTANCE_PREFILTER_MARGIN_M,
ANCHOR_REFRESH_M,
} from './policy.js';
import { t } from '../../i18n/index.js';
import { t, msg } from '../../i18n/index.js';
// installationFeedback.js composes its guidance inside the portable
// node/browser graph (the server-side HUD summary reads the same module), so
@@ -13,19 +13,20 @@ import { t } from '../../i18n/index.js';
// boundary: map the closed message vocabulary to the catalog, passing any
// live provider error through verbatim (docs/TRANSLATORS.md).
const INSTALLATION_FEEDBACK_KEYS = Object.freeze({
'Retrying mapped sites…': 'layers.installations.feedback.retrying',
'Fetching mapped sites…': 'layers.installations.feedback.fetching',
'Zoom in to search mapped installations':
'layers.installations.feedback.zoomIn',
'Showing cached mapped sites': 'layers.installations.feedback.cached',
'Mapped sites not loaded': 'layers.installations.feedback.notLoaded',
'Mapped sites loaded': 'layers.installations.feedback.loaded',
'Overpass rate-limited': 'layers.installations.feedback.reason.rateLimited',
'Overpass timed out': 'layers.installations.feedback.reason.timeout',
'Overpass could not complete the query':
'layers.installations.feedback.reason.queryFailed',
'Overpass temporarily unavailable':
'layers.installations.feedback.reason.unavailable',
'Retrying mapped sites…': msg('Retrying mapped sites…'),
'Fetching mapped sites…': msg('Fetching mapped sites…'),
'Zoom in to search mapped installations': msg(
'Zoom in to search mapped installations',
),
'Showing cached mapped sites': msg('Showing cached mapped sites'),
'Mapped sites not loaded': msg('Mapped sites not loaded'),
'Mapped sites loaded': msg('Mapped sites loaded'),
'Overpass rate-limited': msg('Overpass rate-limited'),
'Overpass timed out': msg('Overpass timed out'),
'Overpass could not complete the query': msg(
'Overpass could not complete the query',
),
'Overpass temporarily unavailable': msg('Overpass temporarily unavailable'),
});
const INSTALLATION_RETRY_IN_RE = /^(.*) — retrying in (\d+)s$/;
@@ -38,7 +39,7 @@ function localizeInstallationFeedback(message) {
const retryIn = message.match(INSTALLATION_RETRY_IN_RE);
if (retryIn) {
const reasonKey = INSTALLATION_FEEDBACK_KEYS[retryIn[1]];
return t('layers.installations.feedback.retryIn', {
return t('{reason} — retrying in {seconds}s', {
reason: reasonKey ? t(reasonKey) : retryIn[1],
seconds: retryIn[2],
});
@@ -46,7 +47,7 @@ function localizeInstallationFeedback(message) {
const pending = message.match(INSTALLATION_RETRY_PENDING_RE);
if (pending) {
const reasonKey = INSTALLATION_FEEDBACK_KEYS[pending[1]];
return t('layers.installations.feedback.retryPending', {
return t('{reason} — retry pending', {
reason: reasonKey ? t(reasonKey) : pending[1],
});
}
@@ -280,7 +281,7 @@ export function createControls({ state: layerState, services, parts, source }) {
}),
),
loadingLabel: layerState.loading
? t('layers.installations.loading')
? t('loading mapped installation context')
: '',
coverage: layerState.coverage,
coverageLabel:
+1 -1
View File
@@ -271,7 +271,7 @@ export function createRendering({
entity.gevTrackedId = `installations:${record.id}`;
entity.gevDisplayPosition = () => displayPosition;
entity.gevLabelModel = {
title: record.name || t('layers.installations.fallbackTitle'),
title: record.name || t('MAPPED INSTALLATION'),
details: [
String(record.class || 'installation')
.replaceAll('_', ' ')