Skip to content

Runtime scoped translations

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

Translations for a flyze platform scope are not shipped in the bundle — they are configured per installation and fetched at runtime. FyzTranslateTranslocoConfigService loads such a configuration and pushes it into transloco, so a scoped FyzTranslationConfig resolves afterwards exactly like a compiled-in translation file.

Two pieces make that work: a data provider the app supplies, and a pivot from the flyze shape into transloco’s shape.

interface FyzScopedTranslocoConfig {
scope: string;
translations: { [key: string]: FyzMultiLanguage[] }; // key -> per-language texts
}

Keyed by translation key, with all languages of that key together (fyz-scoped-transloco-config.model.ts). Transloco wants the opposite: one flat { key: text } object per scope/lang. applyConfigLocal does the transpose.

The app implements AbstractFyzTranslateConfigDataService — two methods, loadConfig(scope) and saveConfig(config), both observable (fyz-translate-config-data.service.abstract.ts:7) — and provides it under the FYZ_TRANSLATE_CONFIG_DATA_SERVICE token.

FyzTranslateProviderService resolves it in its constructor with a try/catch around Injector.get, falling back to a private no-op implementation that returns an empty config (fyz-translate-provider.service.ts:23).

That is why nothing has to be provided: an app that never configures runtime translations still gets a working FyzTranslateTranslocoConfigService whose loadConfig succeeds with nothing in it, instead of an injection error at bootstrap. The missing provider is reported with console.info, not a warning — it is an expected state, not a misconfiguration.

The lookup happens once, in the constructor of a providedIn: 'root' service. A provider registered later — for example inside a lazy-loaded route’s injector — is never seen.

applyConfigLocal (fyz-translate-transloco-config.service.ts:54):

  1. If the scope is already known, removeConfigLocal runs first (see the limitation below).
  2. The key -> languages map is pivoted into language -> { key: text }.
  3. For each language, TranslocoService.setTranslation(translations, scope/lang, { merge: true }).
  4. The config is remembered in an internal Map keyed by scope.

merge: true is what makes the result additive: an existing scope/lang namespace keeps the keys the new config does not mention. A key present in some languages but not others simply does not exist for the missing ones — nothing is filled in, and the scoped-key fallback in translation-config then renders the bare key.

loadConfig(scope, apply = true) and saveConfig(config, apply = true) both delegate to the data service and apply the result via tap on the way through (fyz-translate-transloco-config.service.ts:29). Pass apply: false to fetch or persist without touching the active translations. Note that both are cold: nothing happens until the returned observable is subscribed.

Limitation — removeConfigLocal removes nothing

Section titled “Limitation — removeConfigLocal removes nothing”

fyz-translate-transloco-config.service.ts:82:

const all = structuredClone(this.translocoService.getTranslation());
all.forEach((translations, key) => {
Object.keys(translations).forEach((key) => {
// inner `key` shadows the language
if (key.startsWith(scope)) delete translations[key];
});
this.translocoService.setTranslation(translations, `${key}`, { merge: false });
});

getTranslation() returns a Map<lang, Translation>, so the outer key is a language namespace like playground2/de while the inner key is a translation key such as GREETING. Translation keys inside a scoped namespace do not carry the scope prefix, so key.startsWith(scope) is false for all of them and nothing is deleted. What does happen is a setTranslation(..., merge: false) for every language in the store, rewriting each namespace with its own unchanged content.

Practical effect: re-applying a config for a scope that already exists never drops keys the new config omits — despite step 1 above, the result is still purely additive. To genuinely clear a scope, an app has to call TranslocoService.setTranslation({}, 'scope/lang', { merge: false }) itself.