Translations¶
The interface and the Race Engineer's callouts can be shown and spoken in English,
Spanish (es), Brazilian Portuguese (pt-BR) and Japanese (ja). The language is a
per-device choice, like the speed units: a phone in Spanish and a laptop in English
can share one logger. User-facing behavior is in
Race Engineer › Language.
What is translated¶
| Part | Where the words come from |
|---|---|
| The interface: every view, the dash and its widgets, the OBS overlays, the error pages, dates | frontend/src/i18n/locales/<lang>.json |
| Spoken callouts, their captions, the callout feed, Replay, the examples under What each category says, and the Analysis view's post-lap coaching notes | frontend/src/i18n/callouts/<lang>.ts |
| A Tracks layout suggestion's reason | tracks.why.* in the catalogs, from the suggestion's match |
| Telemetry readouts, lap times and chart axes | Not translated: 1:32.487, 245 km/h and 12.4 L keep one format in every language |
| Setting names and descriptions in Settings › Advanced | English: they come from the server's settings registry |
| Speech-engine diagnostics (no response from the speech engine …) | English: the same text goes back to the server's log |
Interface catalogs¶
locales/en.json is the source: a flat object keyed area.thing, and the type of
every key — t("nav.live") does not compile if nav.live is missing from it. The
other files have the same keys. English is in the main bundle; every other language is
its own chunk, loaded once before the first render (initI18n in main.tsx) and again
when the language changes, so a page never shows half of one language and half of
another, and an OBS source never downloads a language it does not show.
- Placeholders are
{name}. A number placeholder is formatted for the language (12,345/12.345). - Plurals are separate keys:
settings.pending.changes_one,settings.pending.changes_other, and whatever elseIntl.PluralRulesnames for the language (_few,_many).t(key, { count })picks one; Japanese only needs_other. - Missing keys fall back to English, key by key, with English plural rules.
- Components use
useT(), which re-renders them when the language changes;t()works outside React (toasts, stores) and reads the current language. A memo or effect that builds text — an ECharts option, a tooltip — liststin its dependencies, so switching language rebuilds it. - Tables defined at module level (widget names, channel titles, map layers) hold
MessageKeys, never translated text: they exist before the language is known. The component callst()when it renders. - Markup inside a sentence (a link, a
<strong>):rich(t("x.body"), { link: <a>…</a> })puts the node where the message has{link}, so the translator decides where in the sentence it goes. - Dates and whole numbers follow the interface language:
formatTime,formatTimeShort,formatDurationandformatIntinlib/format.ts, orcurrentLanguage()as theIntllocale.
Which language a page shows¶
The stored choice (localStorage gt7-language-v1, set by the 🌐 menu), else the
browser's languages matched to a shipped one — exact tag, then base language (pt-PT
reads pt-BR, es-MX reads es) — else English. ?lang=es on a page's URL, or in a
hash route's query (#/dash?lang=ja), overrides both for that page load without
storing anything: an OBS browser source or a kiosk tablet has no menu to pick from.
Links built for another device — the overlay builder's Copy URL, Settings ›
Overlays — carry lang unless the page they came from is in English
(withLanguage()).
i18n.test.ts fails when a catalog lacks a key English has or has one English does
not, when a translation drops or adds a placeholder, when a plural set has no
_other, or when a whole English sentence was copied in untranslated.
Callouts¶
The server words every callout in English (text, numbers spelled out by
formatter.py) and sends a message_key and message_args with it. The browser
words a callout in another language from those two, with that language's
phrasebook: one function per message key, typed against the arguments the backend
sends (callouts/types.ts). It is code rather than a template string because
callouts are composed: a gap is "3 décimas" below a second and "1,2 segundos"
above it, the clause about the braking point only exists when the server measured
one, and Japanese puts the corner first.
The arguments describe the English sentence. That is the contract between the
two sides: a clause the English leaves out arrives as null (a lap-time gap under
50 ms, a brake-point difference under the noise floor), and the sector-voice
precision travels as places and rounding, so a phrasebook never re-decides what
is worth saying. Corner names (corner_name) and the server's spoken units
(units) ride along where the sentence uses them. A change to a detector's wording
that adds or drops a clause has to change its arguments too.
The Analysis view's post-lap coaching notes come from the same detector, replayed, and
carry the same message_key/message_args, so they are worded with the same
phrasebook. A Tracks layout suggestion's reasoning travels as match beside its English
why.
Gaps without a set precision are rounded to whole tenths before the unit is
chosen (spokenGap() in callouts/numbers.ts, spoken_gap on the server): 950–999 ms
is ten tenths, said as one second, never "コンマ10秒". With the sector-voice precision
the server sends, quantize() rounds first and carries the same way.
Numbers stay digits outside English — "32,5" in Spanish and Portuguese, "1分32秒5" in Japanese — because voices read their own language's digits correctly; English spells them out because English voices read "1:32.487" badly. The only number a phrasebook spells is 1 before a gendered noun (una vuelta, uma volta), which some voices otherwise read as uno / um.
Fallbacks. A key this build does not know (a newer server), arguments that do
not word cleanly (missing, malformed, or a result containing undefined/NaN), or
a phrasebook that throws: the callout is said and shown in the server's English
rather than half-translated. A device with no voice for the callout language speaks
the English text in an English voice — a Japanese sentence handed to an English
voice is noise or silence — while the captions stay translated, and the Voice panel
says so.
Adding a language¶
- Add a row to
LANGUAGESinfrontend/src/i18n/languages.ts: the code (a BCP 47 tag), the language's own name for itself, and the speech tag voices are matched by. - Copy
locales/en.jsontolocales/<code>.json, translate the values, and add the file toLOADERSini18n/index.ts. Keep every{placeholder}; give each plural the formsIntl.PluralRules("<code>")uses. - Write
callouts/<code>.tsexporting aPhrasebook(the compiler lists every key still missing) and add it toPHRASEBOOKSincallouts/index.ts. - Add it to the
CATALOGSmap ini18n.test.tsand runnpm test.
A native speaker's read of both files is worth more than any check here.