Translations in TypeScript with Angular $localize
May 31, 2020
Managing translations in Angular has always been somewhat trouble some, and for the longest part a lot of people used
3rd party libraries to do it. In this blog post I want to show you that since Angular 9.0 you can use the $localize
service and even do translations in TypeScript (translations in templates were possible before,
Angular Guide).
Prerequisites
- Experience with Angular
- Already have translations in the template in use with Angulars
i18n
tags - Extract translations tags from the template
- Use XLIF format for translation files (technically possible with other formats but requires additional steps, see below)
- Basic understanding of Tagged Template Strings
(helps understanding the syntax)
If any of those things are missing, I recommend doing a tutorial before starting with this one.
I’m going to use the JIT compiler, but this should also be usable for the AOT compiler.
Setup
If you haven’t already, add the import for $localize
to your lib imports.
import '@angular/localize/init';
For $localize
to work we need to load translations separately. They are not loaded with your regular i18n translations.
For this there is a loadTranslations(myTranslations)
method in the @angular/localize
package available.
They expect Record<MessageIf, TargetMessage>
as type, which is basically just a plain object with key & values, where
they keys are the translation id (! Important: not the translation key, learn more)
and the value is the actual translation.
This script converts your xliff file into said format. For it to work we need a library to convert the xliff format to
JSON.
npm install xliff
import { MessageId, TargetMessage } from '@angular/localize/src/utils';
import xliff from 'xliff';
export async function parseTranslationsForLocalize(translations: string): Promise<Record<MessageId, TargetMessage>> {
const parserResult: any = await xliff.xliff12ToJs(translations);
const xliffContent: any = parserResult.resources['ng2.template'];
return Object.keys(xliffContent)
.reduce((result: Record<MessageId, TargetMessage>, current: string) => {
if (typeof xliffContent[current].target === 'string') {
result[current] = xliffContent[current].target;
} else {
result[current] = xliffContent[current].target
.map((entry: string | {[key: string]: any}) => {
return typeof entry === 'string' ? entry : entry.Standalone['equiv-text'];
})
.map((entry: string) => {
return entry
.replace('{{', '{$')
.replace('}}', '}');
})
.join('');
}
return result;
}, {});
}
If you don’t use xliff but rather PO or JSON as your format, you might build your own conversion method.
Now we can use the parseTranslationsForLocalize
method in our app bootstrapping.
import {
StaticProvider,
TRANSLATIONS,
TRANSLATIONS_FORMAT,
} from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { loadTranslations } from '@angular/localize';
import { MessageId, TargetMessage } from '@angular/localize/src/utils';
const translations: string = require(`xlf-translation-file.xlf`).default;
const bootstrapFn: any = async (extraProviders: Array<StaticProvider>): Promise<any> => {
// Important part. Rest might be different depending on your setup
const parsedTranslations: Record<MessageId, TargetMessage> = await parseTranslationsForLocalize(translations);
loadTranslations(parsedTranslations);
// For more information on the JIT compile check this documentation
// https://v8.angular.io/guide/i18n#merge-with-the-jit-compiler
return platformBrowserDynamic(extraProviders).bootstrapModule(AppModule, {
providers: [
// Loads translations for template
{provide: TRANSLATIONS, useValue: translations},
{provide: TRANSLATIONS_FORMAT, useValue: 'xlf'},
],
});
};
Defining strings in TypeScript
$localize
is a global tagged template function, you can use it everywhere in your TypeScript without importing.
This looks something like this
const text = $localize`:@@YOUR_UNIQUE_TRANSLATION_ID:the key of the translation`
Even translations with parameters are possible. The syntax looks like this
$localize`:@@YOUR_UNIQUE_TRANSLATION_ID:Hi ${this.name}:name:, it's good to see you`
Inside the ${}
is the value you want to display in the translation. The :name:
is the name of the parameter to use in
your xlf file (without the : )
Extraction
Currently Angular (v.9.1.9) does not support extraction of $localize
strings. But it is likely to be available in a future
version.
Until then you can use this workaround. Create a translation-extraction.component.ts
and add your keys from TypeScript there.
As soon as Angular supports the extraction you can just remove this component. It can look something like this
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'translation-extraction',
template: `
<span i18n="@@YOUR_UNIQUE_TRANSLATION_ID">Hi {{name}}, it's good to see you</span>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TranslationExtractionComponent {
public name: any;
}
Make sure to give your variable the same names as in your $localize
calls.
Important notes
- With
$localize
they don’t support ICU-expressions, e.g. for plural handling *(its not clear if this will even be implemented
in the future)*
- In my tests the
$localize
generated different IDs for the same key used in ani18n
template tag. Therefore
I highly recommend always using custom IDs instead of Angulars auto-generated ones
Conclusion
Even though there is still some stuff missing like the extraction and a proper documentation, it is nice that Angular now finally supports translating content in TypeScript files.
Let me know if this helped you or if you have any questions.
Personal Blog written by Nicolas Gehlert, software developer from Freiburg im Breisgau. Developer & Papa. Github | Twitter
Add a comment
Comments
There are no comments available for this blog post yet