Done with ngx-translate
I’ve been using ngx-translate for as long as I can remember. I find it easier than the native Angular i18n solution.
Translations is often complicated to maintain because the translation file grows as the application grows.
My use cases:
- I don’t want to have all my translations in one single file
- I want to know where to write my translations
- I want to keep a common file because there are still common wordings
- Bonus, I want to make that more readable, reviewable
Project Architecture
This is how my assets are architectured to let the solution works. this can be done in an another way but I don’t variabilize the path to the common files (which can be done if you want to handle it).
- assets
- ...
- i18n
- common
- fr.json
- en.json
- ...
- feature-1
- fr.json
- en.json
- ...
- feature-2
- fr.json
- en.json
- ...
- ...
Custom loader
here is the doc about how to create a custom loader.
Let’s read my suggestion for a custom loader:
// model for a resource to load
export type Resource = { prefix: string; suffix: string };
export class MultiTranslateHttpLoader implements TranslateLoader {
resources: Resources[];
withCommon: boolean;
constructor(
private readonly http: HttpClient,
{ resources, withCommon = true }: { resources: Resource[], withCommon?: boolean }
) {
this.resources = resources;
this.withCommon = withCommon;
}
getTranslation(lang: string): Observable<Record<string, unknown>> {
let resources: Resource[] = [...this.resources];
if (this.withCommon) {
// order matters! like this, all translations from common can be overrode with features' translations
resources = [
{ prefix: './assets/i18n/common/', suffix: '.json' },
...resources
];
}
return forkJoin(resources.map((config: Resource) => {
return this.http.get<Record<string, unknown>>(`${config.prefix}${lang}${config.suffix}`);
})).pipe(
map((response: Record<string, unknown>[]) =>
mergeObjectsRecursively(response)),
);
}
}
export const mergeObjectsRecursively =
(objects: Record<string, unknown>[]): Record<string, unknown> {
const mergedObject: Record<string, unknown> = {};
for (const obj of objects) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
mergedObject[key] = mergeObjectsRecursively([mergedObject[key], obj[key]]);
} else {
mergedObject[key] = obj[key];
}
}
}
}
return mergedObject;
}
How to use it
With common translations:
TranslateModule.(forRoot|forChild)({
loader: {
provide: TranslateLoader,
useFactory: (http: HttpClient): MultiTranslateHttpLoader => {
return new MultiTranslateLoader(http, {
resources: [
{ prefix: './assets/i18n/feature-1/', suffix: '.json' },
{ prefix: './assets/i18n/feature-2/', suffix: '.json' },
...
],
});
},
deps: [HttpClient],
},
})
Without common translations:
TranslateModule.(forRoot|forChild)({
loader: {
provide: TranslateLoader,
useFactory: (http: HttpClient): MultiTranslateHttpLoader => {
return new MultiTranslateLoader(http, {
withCommon: false,
resources: [
{ prefix: './assets/i18n/feature-1/', suffix: '.json' },
{ prefix: './assets/i18n/feature-2/', suffix: '.json' },
...
],
});
},
deps: [HttpClient],
},
})
Result
// assets/i18n/feature-1/en.json
{
"HELLO": "HELLO",
"CIVILITIES": {
"MR": "Mister",
"MS": "Miss"
}
}
// assets/i18n/feature-2/en.json
{
"TITLE": "LONG TITLE",
}
// generated translations
{
"HELLO": "HELLO",
"CIVILITIES": {
"MR": "Mister",
"MS": "Miss"
},
"TITLE": "LONG TITLE",
}
Conclusion
Now you can split your translation files, you will see how it will be easier to read, maintain and review. Don’t forget to use the lazy loading feature (from Angular) and then you will load only translation files required for a specific route.
If you need more details or want to give me your opinion let me a comment, don’t hezitate.
Thanks for reading.
Top comments (0)