mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Choosing a language in the picker now applies it at once. The choice is stored and ?lang is kept in step through replaceState, and the picker keeps focus. Code that writes translated text itself re-presents it on a switch: onLocaleChange() lets an owner listen, removed at teardown, and status writers hand over a function of the locale (voice cards, scene director, SDR, draw hints, imagery split, map sources, Bhote Koshi media) so the words are rebuilt rather than re-sent. The HUD, cockpit, radio, CCTV, launch, awareness and credit texts follow the same hook, and applyDocumentTranslations can be applied again without undoing text code wrote since. The unsaved-work registry and persistLocaleAndReload existed only to guard the reload, so they go, with their catalog entries.
116 lines
9.7 KiB
Markdown
116 lines
9.7 KiB
Markdown
# UI text and translation
|
|
|
|
Every piece of text a user can see goes through `t()`, `tn()` or `msg()` in code, or a `data-i18n*` attribute in markup. The English text itself is the key. English needs no catalog; other languages live in `src/i18n/locales/<code>.json`. Translators work from [TRANSLATORS.md](./TRANSLATORS.md).
|
|
|
|
## Which one to use
|
|
|
|
| | Translates | Use for |
|
|
| ------------------------------- | --------------------------------- | --------------------------------------------- |
|
|
| `t(text, values)` | now | any text shown right where it is written |
|
|
| `tn(count, one, other, values)` | now, picking the form for `count` | text that changes with a number |
|
|
| `msg(text)` | never: returns `text` unchanged | text stored in data and passed to `t()` later |
|
|
|
|
```js
|
|
import { t, tn, msg } from '../i18n/index.js';
|
|
|
|
status.textContent = t('Save keys');
|
|
status.textContent = t('Saved to {store}', { store: storeLabel() });
|
|
|
|
chip.textContent = tn(missing, '{count} KEY WAITING', '{count} KEYS WAITING');
|
|
// en: 1 KEY / 3 KEYS ru: 1 КЛЮЧ / 3 КЛЮЧА / 5 КЛЮЧЕЙ
|
|
|
|
const FEED_STATE_LABELS = { loading: msg('LOADING'), stale: msg('STALE') };
|
|
label.textContent = t(FEED_STATE_LABELS[state]);
|
|
```
|
|
|
|
A module the server, tools or other portable code also imports must not load the catalogs: it imports `msg` from `src/i18n/marker.js`, which only marks the text, and the UI translates it with `t()` where it shows it. (Older modules keep a local `const msg = (text) => text`, which works the same.)
|
|
|
|
Vue views call the same `t()` and `tn()`. The page's vue-i18n instance (`src/ui/i18n.js`), installed in every view, holds the active locale, and `t()`, `tn()` and the formatters read it from there, so a view that translates while rendering renders again when the locale changes. Views don't use vue-i18n's own `t`: English is the key, `context` picks an entry and plural forms follow `Intl.PluralRules`, which only Core's functions do.
|
|
|
|
`msg()` exists for two reasons. `npm run i18n` finds messages only as string literals inside `t(…)`, `tn(…)` and `msg(…)`, so `t(FEED_STATE_LABELS[state])` alone tells it nothing. And a `t()` call at module load runs before the page has picked its language, so it would freeze the English.
|
|
|
|
## Markup
|
|
|
|
Vue views translate in the template, so their text renders in the active locale:
|
|
|
|
```vue
|
|
<span>{{ t('Normal') }}</span>
|
|
<button :title="t('Pin visual presets')">…</button>
|
|
<option value="all">{{ t('All', { context: 'radio tag' }) }}</option>
|
|
```
|
|
|
|
Static page markup that no view renders (the loading screen in `index.html`, a host's own markup) opts in with attributes instead, and `npm run i18n` rejects them in a Vue template:
|
|
|
|
```html
|
|
<span data-i18n>Normal</span>
|
|
<button title="Pin visual presets" data-i18n-title>…</button>
|
|
<input placeholder="Search any location..." data-i18n-placeholder />
|
|
```
|
|
|
|
`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. `data-i18n-context="radio tag"` gives the element's text a context, the catalog entry `radio tag|All`, as `t('All', { context: 'radio tag' })` does in code. Markup is translated at boot and again after every language switch.
|
|
|
|
## Switching language
|
|
|
|
The language picker switches in place, without a reload. A view that calls `t()` while it renders follows by itself. Code that translates text and hands it on (to a store, a published view object, a layer's state, a DOM node) must word it again when the language changes:
|
|
|
|
- **Present again.** `onLocaleChange(listener)` from `src/i18n/index.js` calls the listener after each change and returns the function that removes it; call that at teardown. The listener presents the view again from what it keeps, as `layerPanel.js` and `cockpitController.js` do.
|
|
- **Keep the English, translate when presenting.** Store `msg()` text or a function that words it, not its translation: the voice control takes `update(part, { text: () => t('MIC') })`, the scene director's status and the SDR receiver's messages keep such functions, and `imagerySplit.js` takes its titles that way.
|
|
- **Translated constants in a view** go in `computed`, not in a plain `const` set up once.
|
|
|
|
A toast already on screen keeps its words.
|
|
|
|
## Rules
|
|
|
|
- **Pass literals.** `t('Close')`, not `t(label)` (unless `label` holds a `msg()` value) and never a template literal with `${…}`.
|
|
- **Use placeholders, not concatenation.** `t('Flying to {place}…', { place })`, not `t('Flying to') + ' ' + place`. Word order differs between languages.
|
|
- **Use `tn()` for counts**, not `count === 1 ? … : …`. Russian has three forms.
|
|
- **Translate the label, not the value.** Layer ids, enums, share-link parameters, API fields, voice tool schemas and feed data (callsigns, vessel and place names) stay as they are; translate only the text shown for them.
|
|
- **No `t()` at module top level.** Keep the English in `msg()` and call `t()` when rendering.
|
|
- **Numbers and dates** go through `formatNumber`, `formatDate`, `formatRelativeTime` and `formatUtcDayMonth` from `src/i18n/index.js`, which follow the active language.
|
|
- **Identical English means identical translation,** unless you give it a context: `t('CLEAR', { context: 'action' })` (a button) is its own catalog entry, `action|CLEAR`, while `t('CLEAR')` (the sky) stays separate. English output is the same.
|
|
- **Text that should stay English** (a brand, a code, a unit symbol) is simply not wrapped.
|
|
|
|
## After you add or change text
|
|
|
|
```sh
|
|
npm run i18n # what each language lacks or no longer uses, and unmarked UI text
|
|
npm run i18n -- --missing ru # the missing ru entries as JSON
|
|
npm run i18n -- --prune # drop entries whose English no longer exists
|
|
```
|
|
|
|
Changing the English changes the key: the old translation goes unused and the new text shows as missing. Add the translation to every `src/i18n/locales/<code>.json` (or ask a translator), prune, and run `npm test`; it fails until every language is complete.
|
|
|
|
To see a language, open the app with `?lang=ru`, or use the language picker (the globe button in the bottom-right corner).
|
|
|
|
An empty translation is reported, and so is one left as the English, plural forms included. When English is right on purpose (a code such as CRT, a product name), list it under the language in `src/i18n/same-as-english.json`.
|
|
|
|
## Text the check reports as unmarked
|
|
|
|
`npm run i18n` and `npm test` fail on UI text that is not marked:
|
|
|
|
- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`. In a Vue template, static text or a static `title`, `aria-label` or `placeholder` (rather than `t()`), and any `data-i18n*` attribute.
|
|
- **Code:** string literals that reach the page without `t()`, `tn()` or `msg()`: assigned to `textContent`, `title` and the like; passed to `setAttribute('title' | 'aria-label' | …)`, `createTextNode`, `showToast` and other notice helpers; assigned to a layer's `error`, `lastError`, `loadingLabel`, `statusMessage` or `status` (a plain `error = …` too); or kept in `label:`, `title:`, `status:` and similar properties. Not reported: declarations (`let status = …`), default values, ternary branches and lower-case codes (`'auth-failed'`). The full list is `JS_SINKS` in `scripts/i18n.mjs`.
|
|
|
|
Fix it by marking the text. When the text must stay as it is, say so where it lives:
|
|
|
|
| Case | How |
|
|
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML |
|
|
| Page markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element (not in a Vue template) |
|
|
| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: <reason>` on the line or the line above |
|
|
|
|
Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowercase identifiers are skipped automatically, and so is `src/tools/`, whose text is written for AI clients rather than the page.
|
|
|
|
## Where things live
|
|
|
|
| File | What it holds |
|
|
| ------------------------------- | ------------------------------------------------------------------------------ |
|
|
| `src/i18n/index.js` | `t`, `tn`, `msg`, the Intl formatters, markup translation, switching language |
|
|
| `src/ui/i18n.js` | the page's vue-i18n instance: the active locale and the catalogs |
|
|
| `src/i18n/marker.js` | `msg` alone, for modules the server or tools import (no catalogs) |
|
|
| `src/i18n/locale.js` | shipped languages, their names, choosing the language at boot |
|
|
| `src/i18n/locales/<code>.json` | one catalog per language, keyed by English text |
|
|
| `src/i18n/same-as-english.json` | per language, the entries that stay as in English on purpose |
|
|
| `scripts/i18n.mjs` | `npm run i18n`: extracts messages, checks the catalogs, finds unmarked UI text |
|
|
| `src/i18n/catalog.test.mjs` | runs that check under `npm test` |
|