@quadrel-enterprise-ui/language
v21.0.0
Published
Library for loading dynamic and static translation files
Readme
qd-language
TranslateLoader for @ngx-translate/core. Merges static and dynamic translation sources
into one set of texts. The app decides which sources to use, and it can decide at runtime
from backend config — no rebuild needed.
Coming from version 20? See the migration guide.
Setup
@NgModule({
imports: [QdLanguageModule.forRoot()],
providers: [provideHttpClient()]
})
export class AppModule {}// The library loads the files, the app picks the language.
translate.setFallbackLang('en');
translate.use('en');The two entry points
There is one implementation and two ways to reach it.
| | QdLanguageModule.forRoot(options) | provideQdLanguage(options) |
| ------------------------------ | ----------------------------------- | ---------------------------- |
| Use in | NgModule apps | standalone apps |
| Registers | loader + sources | loader + sources — identical |
| Also exports TranslateModule | yes | no |
forRoot() is a one-line wrapper: it calls provideQdLanguage() and passes the options
straight through. There is no second code path, so the two cannot drift apart.
static forRoot(options: QdLanguageOptions = {}): ModuleWithProviders<QdLanguageModule> {
return { ngModule: QdLanguageModule, providers: [provideQdLanguage(options)] };
}The one difference is the TranslateModule export. Components declared in the module that
imports QdLanguageModule get the translate pipe from it. With provideQdLanguage()
every module imports TranslateModule itself.
This applies to feature modules either way.
QdLanguageModuleonly covers the module that imports it — usuallyAppModule. Every feature module still needs its ownTranslateModuleimport, otherwise its templates show raw keys although the files loaded.
Static sources
Files shipped with the app, one per language. They are always loaded, in every stage.
1. Default — nothing to configure
Loads app texts from ./assets/i18n/{lang}.json and framework texts from
./assets/i18n/framework/{lang}.json.
@NgModule({
imports: [QdLanguageModule.forRoot()],
providers: [provideHttpClient()]
})
export class AppModule {}2. Own folders
Setting static replaces the default list, so list every folder you want — including the
framework one. Order matters: later folders win, so keep the framework first. The last
entry shows that suffix defaults to .json.
@NgModule({
imports: [
QdLanguageModule.forRoot({
static: [
{ prefix: './assets/i18n/framework/', suffix: '.json' },
{ prefix: './assets/i18n/', suffix: '.json' },
{ prefix: './assets/i18n/legal/' }
]
})
]
})
export class AppModule {}3. Folders decided by backend config
A function instead of a list. It runs once, on the first translation load, which is always
after APP_INITIALIZER — so backend config is available. It may use inject().
@NgModule({
imports: [
QdLanguageModule.forRoot({
static: () => {
const config = inject(MyAppConfigService);
return [
{ prefix: './assets/i18n/framework/' },
{ prefix: './assets/i18n/' },
...(config.betaTextsEnabled ? [{ prefix: './assets/i18n/beta/' }] : [])
];
}
})
]
})
export class AppModule {}Dynamic sources
Texts fetched from a translation service at runtime. They are off by default and, once configured, load only in DEV unless you list more stages. See Environments.
4. Dynamic loading from Weblate
project and component are the slugs shown in the Weblate UI — they say nothing about
your app. The request goes to
https://weblate.nivel.bazg.admin.ch/api/translations/my-project/my-component/{lang}/file/.
@NgModule({
imports: [
QdLanguageModule.forRoot({
dynamic: [{ project: 'my-project', component: 'my-component' }]
})
]
})
export class AppModule {}List several sources to load more than one component. Each one is a separate request, and they are merged in the order you write them.
@NgModule({
imports: [
QdLanguageModule.forRoot({
dynamic: [
{ project: 'my-project', component: 'checkout' },
{ project: 'my-project', component: 'orders' }
]
})
]
})
export class AppModule {}5. Dynamic loading from another Weblate instance
Only the instance changes, never the path:
https://weblate.nivel.bazg.admin.ch /api/translations/my-project/checkout/de/file/
└──────── instance ───────────────┘ └──── project, component, language ─────────┘Set it on a single source:
@NgModule({
imports: [
QdLanguageModule.forRoot({
dynamic: [{ project: 'my-project', component: 'my-component', url: 'https://weblate.example.ch' }]
})
]
})
export class AppModule {}Or once for all of them, so you do not repeat the instance on every source:
@NgModule({
imports: [
QdLanguageModule.forRoot({
dynamic: [
{ project: 'my-project', component: 'checkout' },
{ project: 'my-project', component: 'orders' }
]
})
],
providers: [{ provide: QD_TRANSLATION_PROVIDER_URL, useValue: 'https://weblate.example.ch' }]
})
export class AppModule {}A source with its own url still wins over the token.
6. Dynamic loading switched by backend config
Return an empty array to use no dynamic source at all.
@NgModule({
imports: [
QdLanguageModule.forRoot({
dynamic: () => {
const config = inject(MyAppConfigService);
return config.externalTranslationsEnabled ? [{ project: 'my-project', component: 'my-component' }] : [];
}
})
]
})
export class AppModule {}7. Dynamic loading from any other endpoint
A function as url builds the whole request URL itself, so the source does not have to be
Weblate. The response must be a flat JSON object of translation keys.
@NgModule({
imports: [
QdLanguageModule.forRoot({
dynamic: [{ url: (lang: string) => `${appEnvironment.BACKEND_SERVICE_API}ui-api/i18n/${lang}` }]
})
]
})
export class AppModule {}8. Standalone app
Same options, no NgModule. Remember to import TranslateModule where you use the pipe.
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideAppInitializer(() => inject(MyAppConfigService).load()),
provideQdLanguage({ dynamic: [{ project: 'my-project', component: 'my-component' }] })
]
};Environments
A configured dynamic source loads in DEV only. Any other stage has to be listed explicitly — that decision belongs to the team that owns the app.
@NgModule({
imports: [
QdLanguageModule.forRoot({
dynamic: [{ project: 'my-project', component: 'my-component', environments: ['DEV', 'REF'] }]
})
]
})
export class AppModule {}The library compares against QD_CURRENT_ENVIRONMENT, ignoring case. Fill that token from
pamsEnvironment, which your backend returns from the auth config endpoint.
@NgModule({
providers: [{ provide: QD_CURRENT_ENVIRONMENT, useFactory: () => inject(MyAppConfigService).pamsEnvironment }]
})
export class AppModule {}Without the token no dynamic source is loaded and the library warns once. This keeps an app that is wired up incorrectly from calling a translation provider by accident.
How sources are merged
All sources are merged into one object. Later sources win over earlier ones, and static sources always come before dynamic ones.
// './assets/i18n/framework/de.json' → { "i18n.qd.dialog.save": "Speichern" }
// './assets/i18n/de.json' → { "demo.title": "Lokal" }
// Weblate → { "demo.title": "Aus Weblate" }
// Result → { "i18n.qd.dialog.save": "Speichern", "demo.title": "Aus Weblate" }When a source fails
Every source catches its own errors, returns nothing and logs one message. The other sources still load, so a broken endpoint never leaves the app without texts.
A dynamic source that does not answer within 5 seconds is dropped as well. Without that limit it would hold back every other source, because all of them are awaited together.
Quadrel Framework | QdLanguage - Translation file not found: ./assets/i18n/de.json
Quadrel Framework | QdLanguage - Dynamic translations not loaded: https://weblate…/file/
Quadrel Framework | QdLanguage - Dynamic translations timed out after 5000 ms: https://weblate…/file/
Quadrel Framework | QdLanguage - Source did not return a translation object: ./assets/i18n/de.jsonThe last message means the response was not a flat JSON object — an array, null, or an
HTML error page returned with status 200. Such a source is ignored instead of being merged.
Keep at least one static source. An app whose only source is dynamic shows raw keys as soon as the network fails.
Notes
@ngx-translate/coreis a peer dependency. Your app has to install it.
