Faceplate locale-invariance policy (MOR-1450)¶
Status: adopted — owner ruling, MOR-1450 guided walkthrough scenario 10,
extended by two follow-up owner rulings during review (see "MOR-1450
audit" below).
Scope: frontend/src/lib/i18n/locales/*.json catalog authoring policy.
Companion docs: Translator guide (contributor-facing
rules), Core string inventory (where each
string lives), Locale preference contract (how the
active locale is resolved).
Why this exists¶
The ru-RU and ja-JP pilot catalogs pre-dated this policy and translated a
handful of instrument-panel readouts into the target language — for example
core.vfo.split.label + core.vfo.state.off rendering as "Сплит:
выключено" instead of "Split: off". During the MOR-1450 walkthrough the
owner ruled this out of scope for translation, with the rationale (in
English translation): "We are trying to make the faceplate of a premium,
expensive radio." A radio operator reading SPLIT, RIT, AGC, or a mode name
recognizes the international vocabulary printed on real transceiver
faceplates; substituting a local word breaks that recognition and reads as
a toy, not an instrument.
The rule¶
Instrument/faceplate terminology is locale-invariant. It renders identically — same spelling, same casing — in every bundled locale catalog, matching the en-US source verbatim. Auxiliary prose localizes normally: dialogs, settings screens, help text, tooltips, and error/status/refusal messages (the MOR-1422/MOR-1448 string families) continue to translate as before. This document does not change anything about that half of the catalog.
Locale-invariant domain¶
Faceplate/instrument vocabulary, i.e. anything a real transceiver prints on its front panel or LCD, including but not limited to:
- Mode names:
USB,LSB,CW,FM,AM,RTTY,DATA, ... SPLIT,RIT/XIT- Front-end/DSP control abbreviations:
PRE,ATT,AGC,NB,NR,NF VFOA/B,TX/RXCOMP/VOX/MON- S-meter units
- Band names
- Step labels
- Scope controls:
SPAN,REF,SPEED,HOLD - Other control/section labels that mirror a physical faceplate control
(e.g.
BAND,SCAN,SETUP,MOD IN,RF POWER,TX SETTINGS)
This list is illustrative, not exhaustive — the test is whether the string names a control or reading a physical Icom/Yaesu/Kenwood transceiver would print in English, not whether it happens to already exist in the catalog.
Scope note: this policy — and the FACEPLATE_INVARIANT_KEYS guard list
below — covers i18n catalog keys only. Most of the vocabulary above
(mode names like USB/CW, scope controls like SPAN/REF, most DSP
abbreviations) is hardcoded directly in Svelte components rather than
routed through t() — see core-string-inventory.md's P0/P1/P2 batches,
most of which have not been extracted into the catalog yet. Hardcoded
English literals need no catalog guard; there is nothing for a translator
to change. The guard only protects the subset that already flows through
the i18n runtime.
Value words¶
A faceplate readout is usually a label paired with a value word, and the
value word follows the same rule as the label. OFF stays OFF, not
Выключено; ON stays ON. Example as shipped today (the owner ruled to
leave the current catalog casing as-is — core.vfo.split.label is Split,
core.vfo.state.off is off — rather than force a new casing convention;
a future UI pass may change how this renders visually):
Both halves of the pair — the label and the state word — are locale-invariant, not just the label.
What still localizes¶
Everything that is not a direct instrument readout: dialog titles and body
copy, settings-modal sections, help text, tooltips (title=/aria-label
prose that describes a control rather than being its readout),
error/status messages, and refusal reasons. A faceplate token may appear
inside a translated sentence — the token itself stays literal while the
surrounding prose translates, e.g. core.mobile.tx.notAllowedFreq reads
"TX запрещена на этой частоте" in ru-RU: TX stays English, "запрещена на
этой частоте" translates. Same pattern for the quick-action tooltips
core.vfo.ops.quickSplit / core.vfo.ops.quickDualWatch ("Быстрый Split" /
"Быстрый Dual watch" in ru-RU): the action verb translates, the faceplate
token inside it does not. Note the embedded token matches the exact casing
en-US uses in that specific string (lowercase split mid-sentence in
"Quick split", vs. capitalized Split for the standalone label) — the
i18n-check.mjs substring lint is case-sensitive, so the translation must
reuse whichever casing the English source chose for that string, not the
"canonical" label casing. See core-string-inventory.md's note that
glossary tokens "may appear inside translatable copy" for the general
version of this rule; this document narrows it specifically to the
faceplate domain and adds the value-word corollary above.
Two keys need a specific note because their invariance rationale differs from "it's a faceplate readout":
core.mobile.setupButton("Setup") is an aria-label only — the visible button content is an icon, not text. It is kept English to match the visible title of the SETUP sheet it opens (core.mobile.sheet.setup), which satisfies WCAG 2.5.3 Label in Name: the accessible name must contain the visible label text a sighted user (or a voice-control user reading the screen) would use to refer to the control. This is an accessibility requirement, not a faceplate-recognition one — but it produces the same invariant value, so the key is still onFACEPLATE_INVARIANT_KEYS.core.mobile.sheet.setup("SETUP") is dual-use: it is both the chip/button label that mirrors a physical faceplateSETUPcontrol and a bottom-sheet dialog title. Invariance was chosen because the button reading wins today. If the owner later wants the dialog title localized independently of the button label, the fix is to split this into two catalog keys (one for the button, one for the sheettitle=) rather than relaxing the current key — tracked as a follow-up, not done here.
Enforcement¶
The invariant is enforced structurally, not by regex:
frontend/src/lib/i18n/faceplate-invariant-keys.tsexportsFACEPLATE_INVARIANT_KEYS, the single source of truth for which catalog keys are faceplate vocabulary.frontend/src/lib/i18n/__tests__/faceplate-invariant.test.tsasserts every key on that list resolves to the same string in every bundled locale catalog (en-US, ja-JP, ru-RU) — an explicit divergent value fails the test; omitting the key from a translation is allowed (it falls back to en-US, which trivially satisfies the invariant).
Adding a new faceplate-domain catalog key: add it to
FACEPLATE_INVARIANT_KEYS and either mirror the en-US value verbatim in
every locale file, or leave it absent from non-English catalogs.
This is a different, stricter check than
frontend/scripts/i18n-check.mjs's existing glossary-token lint
(GLOSSARY_TOKENS), which only verifies that a token (e.g. VFO, TX,
and — as of this policy — Split/split/Dual watch/dual watch) still
appears somewhere inside a translated string — the designed gate for
faceplate tokens embedded inside otherwise-translatable prose, such as the
quick-action tooltips above. The keys on FACEPLATE_INVARIANT_KEYS are
the faceplate readout in full, so the entire value must match, not just
contain the token.
MOR-1450 audit¶
Round 1 (initial walkthrough)¶
Auditing the ru-RU and ja-JP catalogs against this policy found the following faceplate-domain keys translated out of English, reverted to the en-US value:
core.statusbar.power.labelOn, core.statusbar.power.labelOff,
core.statusbar.nowPlaying.live, core.mobile.chip.band,
core.mobile.chip.scan, core.mobile.sheet.setup,
core.mobile.setupButton, core.vfo.split.label,
core.vfo.dualWatch.label, core.vfo.state.on, core.vfo.state.off,
core.vfo.state.unknown, core.vfo.txTarget.label (ru-RU also needed the
two power labels; ja-JP already had those two correct, which is what
confirmed the intended policy rather than a judgment call).
Round 2 (independent review + follow-up owner rulings)¶
Independent review found two more direct violations round 1 missed, plus one regression round 1's own revert had introduced:
core.vfo.ops.quickSplit/core.vfo.ops.quickDualWatch— the faceplate token was transliterated inside translatable tooltip prose ("Быстрый сплит" / "クイックスプリット"). Per the "token inside prose" rule these keep translating, but thesplit/dual watchtoken inside them must stay literal: fixed to "Быстрый split" / "クイック split" and "Быстрый dual watch" / "クイック dual watch" (lowercase, matching the exact casing en-US uses in "Quick split" / "Quick dual watch"). AddedSplit/split/Dual watch/dual watchtoi18n-check.mjs'sGLOSSARY_TOKENSso a future regression on this specific pattern fails the substring lint (these four entries are core-local additions, not sourced from the strategy glossary — see the comment at their definition).core.overlay.poweredOff.hint(ru-RU) still read "кнопку ВКЛ" after the power button was relabeled toON— a regression the round-1 revert itself created by changing the button's own label without updating a string that names it. Fixed to "кнопку ON".core.statusbar.power.toggleOn/toggleOff(ru-RU) —ON/OFFwere translated into adjective forms ("ВКЛЮЧЁН"/"ВЫКЛЮЧЕН") instead of staying literal inside the sentence.core-string-inventory.mduses this exact string as its worked example of keepingON/OFFliteral inside prose, and ja-JP's"トランシーバーは ON"was already doing this correctly. Fixed ru-RU to match:"...трансивер ON..."/"...трансивер OFF...".core.toast.readinessNoRadio(ru-RU/ja-JP) named the setup section by its pre-revert local name ("«Настройка»" / "セットアップで") aftercore.mobile.sheet.setupwas relabeled toSETUP. Fixed both to nameSETUP.
Two follow-up owner rulings landed during this round and extend the invariant set (superseding the round-1 "token inside prose" reading for these specific keys, since the owner determined they are full faceplate readouts rather than descriptive dialog titles):
- Mobile bottom-sheet titles
core.mobile.sheet.rfPower,.txSettings,.dataMode,.allModes,.filterSettingsare fully invariant ("RF POWER","TX SETTINGS","DATA MODE","ALL MODES","FILTER SETTINGS") — reverted from ru-RU/ja-JP translations and added toFACEPLATE_INVARIANT_KEYS. core.mobile.chip.essentials("ESSENTIALS") is invariant — this supersedescore-string-inventory.md's P0.6 note callingESSENTIALS"plain English, not glossary" (translatable). The owner ruling for this chip takes precedence over that inventory note going forward.core.mobile.nav.tab.meters("Meters") is invariant per the owner ruling.core.mobile.nav.tab.spectrum/.controls("Spectrum" / "Controls") were not ruled on and remain localized — pending a future ruling, do not treat their current translated state as settled policy.
All round-2 fixes and additions are on FACEPLATE_INVARIANT_KEYS
(29 keys total) except the two quick-action tooltip keys, which stay off
the byte-identical guard by design (they carry legitimate translatable
prose) and are instead covered by the i18n-check.mjs substring lint.