Skip to content

Translation configurations

@flyze/lib-core-angular 1.0.0-alpha.34· latest

A FyzTranslationConfig is a self-describing translation value: instead of a bare key or a bare string, a config says how its text is produced. It exists because the flyze platform stores translatable texts as data — a text may be a literal the user typed, a set of per-language literals, or a reference into a transloco scope — and the consuming template must not care which.

FyzTranslateService resolves all three kinds. The pipe that renders them is described in translate-pipes.

FyzTranslationConfig is a discriminated union on _type (fyz-translate-multi.model.ts:23):

_typevalueResolved by
staticstringinterpolating the string as-is
dynamicFyzMultiLanguage[]{ language, text } entriespicking the entry whose language matches the active language
scopedFyzTranslocoConfig{ key, scope?, params? }delegating to transloco through FyzTranslocoService

All three may carry their own params. dynamic uses ISO 639-1 codes ('en', 'de') to match transloco’s active language.

FyzTranslateService offers a synchronous translate() and an observable selectTranslate(). Both run the same three steps: normalize the input, merge the params, then switch on _type (fyz-translate.service.ts:49).

The two differ in how they react to a language change:

  • staticselectTranslate emits once and completes. A static text has nothing to re-resolve.
  • dynamicselectTranslate maps TranslocoService.langChanges$, so it re-emits the matching entry on every language switch. translate() reads getActiveLang() || 'en' once. If no entry matches the active language the result is the empty string, not a fallback language.
  • scoped — handed to FyzTranslocoService, which is backed by transloco’s own selectTranslate/translate.

#handleWrongInput (fyz-translate.service.ts:136) coerces anything that is not a config, because these values come from stored data and a bad one must not blank a whole view:

InputResult
null / undefinedthe literal string NULL
a stringtreated as { _type: 'static' }
an object with _typeused as-is
anything elsethe literal string Unknown

NULL and Unknown are deliberately visible in the UI — they are meant to be noticed and fixed at the source, not silently swallowed.

Params are merged least-specific first, so the call site always wins per key (#mergeParams, :162):

value.params < config.params < opts.params
  • value.params — only exists for scoped, where it sits inside the FyzTranslocoConfig. It is the oldest of the three and kept for stored configurations.
  • config.params — defaults carried by the config itself.
  • opts.params — what the template or caller passes.

Merging is per key, not all-or-nothing: a config carrying { name, role } overridden with { name } keeps its role. When none of the three has params, #mergeParams returns undefined, which is what switches interpolation off entirely — see below.

Interpolation goes through transloco’s own transpiler (TRANSLOCO_TRANSPILER, fyz-translate.service.ts:25) rather than a hand-written regex, so {{ … }} behaves exactly as it does inside translation files — including custom delimiters configured through TRANSLOCO_CONFIG.

#interpolate (fyz-translate.service.ts:179) returns the value untouched when there are no params. This is not an optimization: the transpiler replaces every placeholder it cannot resolve with an empty string, so transpiling unconditionally would eat literal braces out of texts that were never meant to be interpolated. A throwing custom transpiler is caught and the raw value returned — a broken transpiler must not break the view.

Because FyzTranslateService injects TRANSLOCO_TRANSPILER, an app must configure transloco (provideTransloco()) before this service is constructible.

Rationale for both choices: ADR 0002.

FyzTranslocoService is the thin layer between a scoped config and transloco. Its job is turning a key plus a scope into the flat key transloco actually holds (fyz-transloco.service.ts:47):

buildScopedKey('HELLO', 'playground2'); // 'playground2.HELLO'
buildScopedKey('HELLO', ['a', 'b']); // 'a.b.HELLO'

reduceRight nests the scopes right to left, then duplicate dots are collapsed and a leading dot stripped — so an empty scope segment does not produce ..HELLO.

Two behaviors worth knowing:

  • Unresolved scoped keys fall back to the raw key. selectTranslate compares transloco’s result against the scoped key it asked for; transloco echoes the key back when it cannot resolve it, and that echo is mapped to the unscoped key (fyz-transloco.service.ts:34). So a missing playground2.HELLO renders as HELLO, not playground2.HELLO.
  • The synchronous translate() has no such fallback (fyz-transloco.service.ts:39). It returns whatever transloco returns, which for a missing key is the fully scoped key. The two methods therefore disagree on missing keys.

Both observable paths end in distinctUntilChanged(), so a subscriber only sees a value when the rendered text actually changed.

Every public method accepts a plain string where a config is expected, and the scoped params live in two places. Neither is an accident — see ADR 0006.