DEV Community 👩‍💻👨‍💻

Sean Perkins
Sean Perkins

Posted on

Lazy translation assets with Angular, Transloco and Nx Buildable Libs

When dealing with applications that support multiple languages, it becomes increasingly more costly to have single translation asset files to contain all the key-value pairs for your application.

Instead, making use of webpack's inline loaders and Nx's buildable libs; you can fetch smaller translation assets only for a feature's scope, without requesting additional data.

Requirements

  • Nx workspace (mono-repo) with buildable libs.
  • Angular
  • Transloco as the translation library for handling your translations.
  • JSON files to contain your translation key-value pairs (en.json)

Tutorial

Shared i18n Library

Available/Supported Locales

In a shared library (i.e.: @workspace/i18n) create a constant of your application's supported locales.

export const AVAILABLE_LANGS = [
    'en',
    'ja',
    'fr-ca',
    'zh-cn'
];
Enter fullscreen mode Exit fullscreen mode

Inline Loader Factory

In the same shared library, create a file for simplifying the interaction of using webpack's inline loader with the expected structure to use with Transloco's library.

import { AVAILABLE_LANGS } from './available-langs.const';

export function InlineLoaderFactory(loader: (lang: string) => Promise<any>) {
    return AVAILABLE_LANGS.reduce((acc: Translation, lang: string) => {
        acc[lang] = () => loader(lang);
        return acc;
    }, {});
}
Enter fullscreen mode Exit fullscreen mode

Make sure to export your inline loader factory file to the root barrel of your shared Nx library.

Feature Lib

For this tutorial, we are separating out the translation assets for an authentication experience.

We will have an Nx library for authentication/web in libs/authentication/web of our Nx mono-repo.

Buildable libs

When creating a new library make sure to use the --buildable modifier to run the schematic for making your lib buildable.

If you are adding this to an existing lib that isn't already buildable you will need to:

  • Create an ng-package.json file in the root of your lib:
{
    "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
    "dest": "../../../dist/libs/authentication/web",
    "lib": {
        "entryFile": "src/index.ts"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Create a tsconfig.lib.prod.json:
{
    "extends": "./tsconfig.lib.json",
    "compilerOptions": {
        "declarationMap": false
    },
    "angularCompilerOptions": {
        "compilationMode": "partial"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Update your workspace.json/project.json (depending if you are using standalone configs) to include a build target:
"build": {
    "executor": "@nrwl/angular:ng-packagr-lite",
    "outputs": ["dist/libs/authentication/web"],
    "options": {
        "project": "libs/authentication/web/ng-package.json"
    },
    "configurations": {
        "production": {
            "tsConfig": "libs/authentication/web/tsconfig.lib.prod.json"
        },
        "development": {
            "tsConfig": "libs/authentication/web/tsconfig.lib.json"
        }
    },
    "defaultConfiguration": "production"
},
Enter fullscreen mode Exit fullscreen mode
  • Create a package.json of your library's workspace qualified name:
{
    "name": "@workspace/authentication/web",
    "version": "0.0.1",
    "peerDependencies": {
        "@angular/common": "^12.2.0",
        "@angular/core": "^12.2.0"
    },
    "dependencies": {
        "tslib": "^2.3.0"
    }
}

Enter fullscreen mode Exit fullscreen mode

Assets

In your lib, create a new directory called assets that is located at the root (/libs/authentication/web/assets/).

In this folder, add all of the JSON translation files for your feature and supported locales.

/assets/
       en.json
       ja.json
       fr-ca.json
       zh-cn.json
Enter fullscreen mode Exit fullscreen mode

Next, update your ng-package.json to include your assets in the build process.

{
    "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
    "dest": "../../../dist/libs/client/authentication/web",
    "lib": {
        "entryFile": "src/index.ts"
    },
    "assets": ["./assets"]
}
Enter fullscreen mode Exit fullscreen mode

This step is extremely important. Without it, your JSON files will be purged from the generated /dist when the library is built. This will cause your webpack inline loaders to fail to load the translation assets only in a built environment.

Configuring Transloco for Feature Module

In your feature's main Angular module, configure Transloco to make use of webpack's inline loader and our library's assets.

import { NgModule } from '@angular/core';
import { TRANSLOCO_SCOPE } from '@ngneat/transloco';

@NgModule({
    imports: [...], // Lazy routing module and feature modules
    providers: [
        {
            provide: TRANSLOCO_SCOPE,
            useValue: {
                scope: 'auth',
                loader: InlineLoaderFactory((lang: string) => import(`../../assets/i18n/${lang}.json`))
       }
    ]
})
export class AuthenticationModule {}
Enter fullscreen mode Exit fullscreen mode

The scope value, is the key prefix that will be added to all translation key-value pairs that are loaded from your assets directory.

Assuming you have a translation file such as:

{
    "login_form.title": "Enter your email"
}
Enter fullscreen mode Exit fullscreen mode

The key will be re-written within the context of your feature module to be accessed as auth.login_form.title. You can learn more about scopes within Transloco here.

Using your translations

In the component's module making use of Transloco, import the TranslocoModule to make use of Transloco's built in pipes and directives.

import { TranslocoModule } from '@ngneat/transloco';

@NgModule({
    imports: [TranslocoModule]
})
Enter fullscreen mode Exit fullscreen mode

Now, remembering the scope you defined earlier, you can use your translation in the component's template:

<p>{{ 'auth.login_form.title' | transloco }}</p> 
Enter fullscreen mode Exit fullscreen mode

Verifying and Validating

Build output

You should confirm that ng-packgr is configured correctly to generate and output your JSON assets alongside your generated JS bundles.

Using Nx's build command, build your library and validate the JSON assets are in the assets directory of the dist output.

nx build authentication-web
Enter fullscreen mode Exit fullscreen mode

Dist directory for built library showing JSON assets in the assets directory

Network traffic

After making this change, you should be able to confirm that webpack's inline-loaders are loading the translation assets when your feature module is requested by your application (when making use of lazy loading).

Network tab in browser showing translation asset loading

UI validation

Lastly you should confirm in your application's interface that your translation keys are translated from auth.login_form.title to your expected value for the active locale.

Translated app interface

Final thoughts

Lazy loading translation assets can be super impactful to application performance. With this change in our authentication experience, we are able to request 3.0kb of translation assets for our locale, instead of 166kb for our entire application's translation asset. As our application continues to scale, our impact of translations stay consistent to the growth; instead of all users being impacted for translation assets for features they are not using.

Lastly, by using Nx buildable libs to manage our separated feature and translation assets; our library is never rebuilt for other translation changes in our application, resulting in lower CI build times and cost savings.

Top comments (3)

Collapse
luchitogaro profile image
Luchitogaro

Hello, Good tutorial but do you know how to manage the language changes with the "@workspace/i18n" ? E.g. From the library or Angular App change the language setting "en" or "es"...? Thanks.

Collapse
seanperkins profile image
Sean Perkins Author

The library @workspace/i18n should just contain the translation asset files. The logic for handling language changes should be handled from the Transloco library. The library has an option for handling live translation changes (when you change the active language it will re-render pipes/bindings), but it will come with a slight performance hit for all the watchers. Alternatively you can change the language and reload your application to render all text in the correct language.

Collapse
shaharkazaz profile image
Shahar Kazaz • Edited on

Nice article!
BTW prefer the structural directive over the pipe with using Transloco 😉

🌚 Life is too short to browse without dark mode