Dynamic Import of Locales in Angular

Dynamic Import of Locales in Angular

Lazy load your locale when changing countries

The other day I read ‘Using TypeScript Dynamic Imports in Angular’ by Netanel Basal where he says:

TypeScript 2.4 added support for dynamic *import() expressions, which allow us to asynchronously load and execute *ECMAScript modules on demand.

This sounded like something I had to try! I already had the perfect use case from my previous article ‘Custom Angular Pipes and Dynamic Locale.’ The problem with having all these languages is that for each one you need to import the data of the locale. That is a lot of extra code taking up bandwidth.

It would be great if we can dynamically import the language when needed. All sounds good in theory. Let’s see if it also works in practice.

This article was written while I was trying to get the imports working. I have not censored the errors I ran into in the hope that it will help someone who runs into the same issues.

At the end, I have a link to example code on GitHub.

And now let’s start our journey!

The Goal

  • Remove import statements for locales

  • Replace switch statement with dynamic import

  • Lazy loading

We want to remove the imports of locales:

import { registerLocaleData } from '@angular/common';
import localeNorwegian from '@angular/common/locales/nb';
import localeSwedish from '@angular/common/locales/sv';

And we want to replace the switch statement that registers the locale data with the dynamic import.

switch (culture) {
  case 'nb-NO': {
    registerLocaleData(localeNorwegian);
    break;
  }
  case 'sv-SE': {
    registerLocaleData(localeSwedish);
    break;
  }
}

esNext

Before we start, we need to open tsconfig.json and change module type to esNext since dynamic imports are not in the official ES spec yet. Otherwise, we will get this error:

Dynamic import is only supported when ‘ — module’ flag is ‘commonjs’ or ‘esNext’

esNext is a place holder for features that are on the standard track but is not in an official ES spec yet. e.g. import.meta and import() expressions. As TC39 adds these features to a versioned spec, new --module ES20xx will be added to reflect that."

Dynamic Import

The Dynamic Imports allow us to load our code on demand asynchronously.

So now let’s follow the article where it says:

import("xlsx").then(xlsx => {
    // JUST USE THE LIBRARY    
});

And change our code to:

import('@angular/common/locales/nb')
  .then(lang => registerLocaleData(lang));

But we get an error:

Uncaught (in promise): TypeError: Cannot read property ‘toLowerCase’ of undefined at Object.registerLocaleData

It seems we need to use the default property.

import('@angular/common/locales/nb')
  .then(lang => registerLocaleData(lang.**default**));

No errors! But I wonder why we don’t get the default by default?

Like this blog post? Share it on Twitter! 🐦

Promises

If you have some code right after the import that updates the values that use you might get this error:

Missing locale data for the locale "nb-NO".

This is because your module did not import before the pipe tried to use it.

Since fetching an ECMAScript module on demand is an asynchronous operation, an import() expression always returns a promise.

So we have to wait until the import is done. To make the code clearer let’s move the importing to a function.

And now we can use that method with some callback code:

this.localeInitializer().then(() => {
   // Update values here
});

Callbacks added with then() will be called after the success or failure of the asynchronous operation.

If we build this code we can see that we are getting a new chunk:

So we can see that we are now getting that language we added in the dynamic import in our build.

More Languages

So this is great and all but we do not want to hard-code all the languages that we import. Instead, let’s use the characters before the hyphen from our culture string to get our ‘localeId.’

const localeId = culture.substring(0, culture.indexOf('-'));

And then let’s refactor localeInitializer()so we can pass it that id.

Now we get a ton of these warnings:

We are trying to import too many files, some of which break the build. If we look in the folder ‘node_modules\@angular\common\locales’ we see that every language has one JavaScript file and one TypeScript definition file. Since we only need the .js files, we can change our code to reflect it.

import(`@angular/common/locales/${localeId}.js`)

I’m using a Template Literal, and now the build works again. We get lots of chunks now.

It is bundling all the language files in the locale folder to individual chunks. This way we can lazy load the language we need.

Magic Comments

In my case, I only have two languages so bundling all of them seems like overkill. Since we are using the CLI, I thought that perhaps there is no way actually to define which languages we want to bundle.

But there is some magic we can do! And they are called webpack magic comments. By utilizing this magic comment we can send in the languages we want to bundle to webpack:

/* webpackInclude: /(nb|sv)\.js$/ */

With this code, our function is complete and looks like this:

 localeInitializer(localeId: string): Promise<any> {
    return import(
      /* webpackInclude: /(nb|sv)\.js$/ */
      `@angular/common/locales/${localeId}.js`
      ).then(module => registerLocaleData(module.default));
  }

You can also exclude files if that is more convenient.

/* webpackExclude: /(languages|here)\.js$/ */

And now when we recompile we only get the extra chunks we want.

For more options you can check the documentation.

Lazy Loading

We have our language chunks bundled and ready but let’s see if they are lazy loaded by using the network tab in the developer tools.

Yes, we can see that when we start the application that no language chunks are loaded. And when we press the buttons for Norway or Sweden, the chunk for the corresponding country is downloaded.

That’s great! So with research, grit and occasional magic, we were able to do what we set out to do. I was not sure that this article would end happily, but I’m glad it did.

“man sitting on mountain cliff facing white clouds rising one hand at golden hour” by [Ian Stauffer](https://cdn.hashnode.com/res/hashnode/image/upload/v1618075298886/U151_ru4u.html) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)“man sitting on mountain cliff facing white clouds rising one hand at golden hour” by Ian Stauffer on Unsplash

You can download the code from GitHub if you want to try it out.

Call to Action

I always enjoy feedback so please 👏, 📝 and tweet 🐦. Follow me on Twitter and Medium for blog updates.

Did you find this article valuable?

Support Michael Karén by becoming a sponsor. Any amount is appreciated!