translations.json file holding every string keyed by locale, and a use<Name>Translate hook that reads the active language and looks keys up for you. The active language is resolved once, from the ?lang= query param, by the shared useLang hook.
You scaffold this pair with talos translation:create. In a spa the generator produces a hook; backend modules get a Translation class instead. The dictionary format is the same, only the consumer differs. See the translation component for the shared engine both build on.
How it works
Translations live next to the feature that uses them, undersrc/features/<feature>/translations/. The generator writes two files: the dictionary and the hook. The dictionary is written once and never overwritten, so you grow it by hand. The hook wraps the trans/has primitives from @talosjs/utils/trans and binds them to the current language.
The dictionary
translations.json is a tree of keys. Each leaf is an object mapping locale codes to strings. Keys nest freely, so a feature can group its strings however it likes. Strings may contain {{ param }} placeholders that are filled at call time, and a key may carry plural variants selected by a count argument.
translations.json
- Locale leaves. The active language (from
useLang) selects which leaf is used.titlewithlang = "fr"yields"Paramètres". - Interpolation.
{{ param }}placeholders are replaced by the matching entry inparams.greetingwith{ name: "Ada" }yields"Hello Ada". - Pluralization. When you pass
count, the resolver picks among sibling keys:_zerowhencountis0,_pluralwhencountis plural, and the base entry otherwise.{{ count }}itself is available as an interpolation param. Soitemswithcount = 0yields"No items",count = 1yields"1 item", andcount = 5yields"5 items". - Fallback. When no entry matches a key, the hook returns the key string unchanged, so missing translations surface as the key rather than crashing.
Resolving the language
The active language comes from a single shared hook. It reads the?lang= search param off the current URL and defaults to en, so ?lang=fr switches the whole app to French without any extra state.
src/shared/hooks/useLang.ts
useLang, changing the query param re-renders the consuming components in the new language, with no separate language store to keep in sync. See routing for how search params flow through the router.
The generated hook
talos translation:create writes a use<Name>Translate hook per feature. It imports the feature’s translations.json, resolves the language with useLang, and exposes trans and has bound to that language and dictionary.
src/features/settings/translations/useSettingsTranslate.ts
Using it in a component
Call the hook at the top of a component, then translate by key. Passparams for interpolation and count to drive plural selection; use has to branch on whether a key exists.
src/features/settings/components/SettingsHeader.tsx
?lang=fr flips every string this hook resolves to French, falling back to en when no lang is present.
Working with translations
Keep each feature’s strings in its owntranslations/translations.json next to the code that uses them, and don’t reach into another feature’s dictionary. The generator writes the dictionary once, so add new keys and locales by hand; it is never regenerated over a populated dictionary by design. Missing keys render as the key itself, which makes a forgotten translation visible in the UI rather than a runtime error, so you can search for raw keys when auditing coverage.
Resolve language only through useLang/?lang=, rather than threading a language prop through components or duplicating the logic. For countable strings, always pass count so _zero/_plural selection works, and reference {{ count }} in those entries.
Related
- Spa overview — how spa modules are organized.
- Spa structure and features — where
translations/lives. - Routing — how search params like
?lang=reach the hook. - Translation component — the shared
trans/hasengine. translation:create— the generator command reference.- Internationalization (advanced) — the model across module types.