Angular 9 introduces a new implementation for its built-in I18N support called code>@angular/localize. One concept it contains is called global locales. These are bundles with meta data for different date and number formats the CLI can add after compiling the application.
Normally, the CLI uses these bundles under the hood to dramatically speed up the creation of different language versions. However, we can divert it from its intended use to load the meta data dynamically on demand.
This article shows how to leverage this. The used example can be found in my GitHub repo.
Big thanks to Angular's Pete Bacon Darwin who helped me to see the whole picture regarding global locals.
Situation Before Angular 9
Beginning with Angular 5, Angular is shipped with meta data for different locales. It is derived from the Unicode Common Locale Data Repository (CLDR). To support some of them, we had to import and register them before the application starts, e. g. within main.ts
:
import { registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';
import localeDeAt from '@angular/common/locales/de-AT';
import localeEs from '@angular/common/locales/es';
registerLocaleData(localeDe); // de-DE
registerLocaleData(localeDeAt); // de-AT
registerLocaleData(localeEs); // es-ES
[...]
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
In addition to these locales, en-US
is always available.
Obviously, this increases the size of the main bundle as all this meta data is referenced at compile time. As the next chapters show, Angular 9 introduces a way to prevent this situation.
Global Locales
Beginning with version 9, Angular is also providing a special set of bundles registering the locale meta data globally at global.ng.common.locales
. For instance, in the case of German, the meta data would be put to global.ng.common.locales['de']
. Hence, they are called global locales.
These bundles are located within code>node_modules/@angular/common/locales/global:
Global Locals go hand in hand with the new code>@angular/localize package which brings the built-in I18N support to the next level. The idea of this package is to only compile the application once. Then, this compiled version is duplicated for each language one wants to support and each of these duplications is modified.
These modifications involve exchanging texts but also adding the right meta data. For the latter one, we need a way to simply add this meta data after the build. This is what global locals enable.
Obviously, this approach is much faster than the original one which compiled the whole application once per language.
Saying this, normally you don't need to deal directly with these global locals because the CLI does the heavy lifting for you. If you want to try it out, you find a wonderful blog post about code>@angular/localize here.
However, if you don't use code>@angular/localize do deal with translation texts but go with something like ngx-translate
instead, directly dealing with global locales will come in handy. This is also the case if you do use code>@angular/localize but define the translation texts programatically, e. g. after loading them from a custom service in a custom data format (which is called runtime tranlation).
In these cases, loading just the needed meta data on demand instead of loading the meta data for all languages upfront will improve your startup performance.
Lazy Loading
To demonstrate lazy loading global locals by hand, I've created a simple application which is using en-US
by default:
After clicking the button German Version
, the German locale meta data is lazy loaded and used. For this, I've created a simple script loader which is basically creating a script
tag dynamically:
//
// Simplest Possible Script Loader TM
//
@Injectable({
providedIn: 'root'
})
export class SimpleLoaderService {
constructor() { }
// Remeber already loaded files so that we
// don't load it again.
private loadedFiles = new Set<string>();
loadScript(src: string): Promise<void> {
// If the file is already loaded don't do anything.
if (this.loadedFiles.has(src)) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
// Create a script tag and point to java script file
const script = document.createElement('script');
script.src = src;
// Resolve Promise after loading
script.onload = () => {
this.loadedFiles.add(src);
resolve();
};
// Reject Promise on error
script.onerror = () => {
reject();
};
// Add script tag to page
document.body.appendChild(script);
});
}
}
Our demo's AppComponent
holds a date which happens to be the authors birth day (just saying ...) and a property holding the locale to use:
@Component({ [...] })
export class AppComponent {
date = new Date('2020-01-20T17:00+01:00');
lang = 'en-US';
constructor(private loader: SimpleLoaderService) {
}
toGerman() {
this.loader
.loadScript('assets/de.js')
.then(_ => this.lang = 'de-DE')
.catch(err => console.error('Error loading file', err));
}
}
The template displays this date as shown above:
{{ date | date:'long':'':lang }}
The method toGerman
is bound to the shown button and uses our SimpleLoaderService
to load the German
locale. After that, it updates the lang
property.
Testing the Application
To demonstrate that lazy loading really takes happen, you can open Chrome's network tab within the dev tools and click German Version
within the page. You should see that this lazy loads the assets/de.js
file. Also, you should now see a German date.
Conclusion
References
- Great blog article about @angular/localize by Cédric Exbrayat
- Pull Request: Support loading locales from a global
- Example used here
More in Our Workshop
If you want to learn more about such advanced topics, check out our Advanced Angular Workshop which focusses enterprise applications and sustainable architectures.