mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
The application views install one vue-i18n instance that holds the active locale and the catalogs. src/i18n/index.js stays free of Vue: bindLocale() keeps its locale in the instance's ref, so t(), tn() and the Intl formatters read it from there and a view that translates while rendering renders again when the locale changes. A tag written straight to the ref is normalized. Message formatting, catalogs and call sites are unchanged, and switching language still reloads the page. The instance outlives the views: unmounting one no longer disposes it. The browser build defines vue-i18n's feature flags and drops its message compiler, which Core does not use.
96 lines
8.3 KiB
Markdown
96 lines
8.3 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
|
|
|
|
```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 once at boot, and switching language reloads the page. A module that holds work a reload would lose (an unsaved edit, an open session) registers it with `registerUnsavedWork(() => busy ? msg('unsaved …') : null)` from `src/i18n/index.js`, and the language picker asks before reloading.
|
|
|
|
## 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-*`.
|
|
- **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 |
|
|
| Markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element |
|
|
| 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` |
|