-
Notifications
You must be signed in to change notification settings - Fork 0
i18n
Every user-facing string in the SPA goes through @ngx-translate. The catalogs are static JSON fetched at runtime rather than compiled in, English is canonical, and Spanish is held at exact key-for-key parity by a gate that runs on every pull request. This page is the mechanism, the enforcement, and the one directory that will waste your afternoon.
Runtime label overrides — renaming "Job" to "Work Order" for an install — are a completely separate system backed by database rows and the terminology pipe. Do not reach for the catalogs to do that; see Customizing an Install.
public/assets/i18n/en.json canonical source
public/assets/i18n/es.json must match en.json key-for-key, both directions
They are nested JSON, flattened to dotted keys (quotes.detail.title) by both the runtime and the linter. app.config.ts wires provideTranslateHttpLoader({ prefix: '/assets/i18n/', suffix: '.json' }) with a default language of en, and an APP_INITIALIZER reads localStorage.language, defaults it to en, and awaits the catalog load before the app finishes bootstrapping. That is deliberate — no flash of raw keys on first paint — but it also means a catalog that fails to fetch stalls startup rather than degrading to English.
LanguageService owns the switch: it calls translate.use(), sets a signal, writes localStorage.language and sets document.documentElement.lang. Two things follow. The choice is per browser, not per user account — a shared kiosk browser keeps whatever the last person picked. And a SupportedLanguage union type plus an availableLanguages list in that service are the picker's source of truth, so a catalog file alone does not make a language selectable.
One place the choice leaves the browser: trainingLangInterceptor appends ?lang= to training-content GET requests so the API serves localised module content, omitting the parameter for en because the API treats English as the fallback base. Training content is translated server-side; UI chrome is translated here.
scripts/lint-i18n.mjs is the gate, and it checks three separate things.
1. The src/assets trap. Covered in its own section below — it runs first and hard-fails.
2. Every statically extractable key exists in en.json. The script walks src/** for .ts and .html (skipping .spec.ts, because specs use a mocked loader and synthetic fixture keys) and matches these forms:
| Form | Example |
|---|---|
| Template pipe, either quote style |
{{ 'foo.bar' | translate }}, [label]="'foo.bar' | translate"
|
| The directive | [translate]="'foo.bar'" |
| Programmatic |
translate.instant('foo.bar'), .get(…), .stream(…)
|
| Config properties that flow into a translate call |
labelKey: 'foo.bar', and its siblings i18nKey, tooltipKey, placeholderKey, messageKey, titleKey, descKey, hintKey, emptyMessageKey, emptyHelpKey, addLabelKey, missingMessageKey, displayNameKey, sourceLabelKey
|
What it cannot see: a template-literal key (translate.instant(`prefix.${variant}`)), a key held in a variable, and a key supplied by the server. Those go in scripts/.lint-i18n-allow, one per line, with a trailing * for a prefix match — and a comment saying why, so the next reviewer can tell a permanent dynamic key from a temporary excuse.
The regex detail worth knowing, because it was a real bug: the pipe pattern ends with translate(?!\w), which matches translate followed by any non-identifier character including a closing quote. An earlier form required whitespace or a delimiter and therefore silently skipped every ... | translate" property binding — a whole category of missing keys sailed through the gate while looking checked.
3. Parity, in both directions. A key in en.json and not in es.json fails (untranslated). A key in es.json with no English counterpart fails (orphan). There is no "mostly translated" state. Add a key and add its Spanish counterpart in the same change; delete one and delete both.
Angular's static-asset directory migrated from src/assets/ to public/. public/assets/i18n/ is the only bundled catalog path, per the assets entry in angular.json. Editing src/assets/i18n/ instead is a tar pit, because it is green everywhere that would normally catch you:
-
tsc— no opinion, it's JSON. -
ng build— succeeds; the file simply isn't an asset. - Unit tests — pass; they use a mocked
TranslateLoaderand never read either file. - The i18n lint itself, historically — it used to read
src/assetstoo, so it agreed with you.
The only symptom is a raw foo.bar token rendering in the running app, which is exactly the class of bug the gate exists to prevent. src/assets/i18n/ was deleted, added to .gitignore so an editor's auto-save buffer cannot resurrect it, and the linter now carries a trap that fires when the directory exists and contains a key that public/assets/i18n/ does not have. An empty placeholder from a stray editor buffer is tolerated; a real divergent edit hard-fails with the orphan keys listed.
A known bug in that script: its final failure summary tells you to "add to src/assets/i18n/en.json" — the exact path the same script hard-fails on a few lines earlier. Ignore the message and edit public/assets/i18n/en.json.
The linter walks a sibling ../forge-api checkout for .cs files and extracts string literals beginning workflow., validators. or terminology.. That is not scope creep: the API stamps those keys into database rows — workflow step definitions, entity-readiness validator records — and the SPA renders them through translate. A key that only ever appears in C# has precisely the same failure mode as one that only appears in a template.
If the sibling repo is not checked out, that half of the check silently does not run. It is conditional on the directory existing, with no warning. CI's nightly job goes out of its way to check out forge-api and symlink it into place for the same reason; a local run in an isolated clone will not. See forge-api for what stamps those keys.
More than dropping in a JSON file:
- Add
public/assets/i18n/<code>.jsonwith every key fromen.json. - Extend
SupportedLanguageandavailableLanguagesinshared/services/language.service.ts. - Extend the parity comparison in
scripts/lint-i18n.mjs. A comment there refers to appending the language to aPARITY_LANGSconstant — that constant does not exist. The English/Spanish key sets are compared directly, so a third language needs a small edit to the comparison itself, not a list entry. Without it, the new catalog is completely unchecked.
The catalogs are not in the service worker's cache. ngsw-config.json prefetches the app shell and lazily caches media and fonts; /assets/i18n/*.json matches no asset group, and the data groups only cover /api/v1/**. A cold offline start therefore has the bundle and no strings, and the bootstrap initializer is the thing that waits on them. Worth knowing before you reason about what the PWA does on a dead network — see Mobile and Offline.
A missing key is invisible to every other gate. tsc, the production build and Vitest all pass with a key that does not exist. lint:i18n is the only thing standing between a typo and a raw dotted token in front of a customer, which is why it is a required CI step rather than a nicety — see Standards and Ratchets.
forge-ui · Apache 2.0 · built by Armory Works — this wiki covers the frontend; product-level truth lives on the Forge wiki.
Peer repos