Internationalization vs. localization: setting the stage

It's common to mix up internationalization (i18n) and localization (l10n), and to wonder where one ends and the other begins. Internationalization refers to designing an application so it can be adapted to various locales around the globe, whereas localization is the act of creating versions of that application for those specific locales.

Working together, these practices help tailor software to different languages and the local conventions and visual expectations of the intended audience.

The Ivy approach to localization

Angular Ivy's localization mechanism builds on tagged templates. With tagged templates, you can process a template literal using a function. In this case, the tag is the global $localize identifier. Rather than translating strings directly, the Ivy template compiler turns any template text carrying an i18n attribute into a $localize tagged string.

Consider this markup:

<h1 i18n>Hello World!</h1>
Enter fullscreen mode Exit fullscreen mode

The above gets compiled into $localize calls, and somewhere in the resulting code you'll find:

$localize`Hello World!`
Enter fullscreen mode Exit fullscreen mode

With a tagged template, the function you want to apply to the string is placed right before the template literal. Instead of writing function(), you write function``, or in this instance, $localize``.

Once this transformation is complete, you have two paths forward:

  • compile-time inlining: the $localize tag is processed at build time by a transpiler, which strips the tag and swaps in the translated string.

  • run-time evaluation: the $localize tag operates as a runtime function, replacing the template string with translations that are loaded dynamically.

Throughout this article, we rely on compile-time inlining. At the tail end of the build, we pass a flag to handle the translation files, producing a localized build for each target language. Because translations are baked in at compile time, we end up with one application bundle per locale.

Later, we'll explore run-time evaluation in more depth.

Since the app no longer needs a separate rebuild for each locale, the build pipeline is considerably faster than it was prior to Angular 9.

You can read more about this in [Angular localization with Ivy](https://cdn.hashnode.com/res/hashnode/image/upload/v1618075767519/BbSs51O0K.html) from where this picture is.For more details, check out Angular localization with Ivy, where this image is sourced.

With a clearer picture of the build process, we can now look at what this involves more concretely.

Weighing the pros and cons

The built-in Angular i18n and localization pipeline generates a separate compiled bundle for each language. This leads to optimal performance because there's no runtime overhead from fetching or parsing translation data. However, the flip side is that you must deploy each language version to its own distinct URL:

www.mydomain.com/en
www.mydomain.com/nb
www.mydomain.com/fi
Enter fullscreen mode Exit fullscreen mode

Consequently, some extra webserver configuration is required. One limitation is that ng serve supports only a single language at a time, and running multiple languages locally also demands additional setup. To test everything on your own machine, you'll need a local webserver—we'll cover how to handle that here.

Angular's i18n relies on XLIFF and XMB, both XML-based formats that are more verbose than JSON. Since these files are processed during compilation, verbosity isn't a real concern. JSON becomes the more sensible choice when loading translations at runtime to minimize file size. The built-in i18n formats, however, are compatible with translation management tools—something we'll take advantage of later.

The most frequently cited downside of this built-in approach is that switching languages forces a full page reload. But is that actually a dealbreaker for you? In practice, users change languages rarely, if ever. The few seconds spent reloading is hardly a nuisance.

For a web SPA, having a separate bundle per language is mostly just a matter of server configuration. But for standalone or native-style apps, it means users would need to download every translated bundle, or you'd have to ship a distinct app per language.

Before settling on a strategy, it's crucial to understand what your project actually requires.

Considering Transloco

If the stock Angular i18n doesn't meet your needs, Transloco is currently the strongest alternative in my view. It's under active maintenance, boasts a lively community, and gets you up and running faster with more flexibility than the built-in solution. Because Transloco works at runtime, you only need a single www.mydoman.com and can swap localizations on the fly.

Given how fundamental this decision is, it's worth checking out Transloco to see if it aligns better with your goals.

Alright, enough theory—let's jump into the code!

Installing localize in your Angular project

The @angular/localize package arrived with Angular 9 and enables i18n for Ivy-based apps. This package relies on a global $localize symbol being available, which gets loaded by importing the @angular/localize/init module.

To bring Angular's localization capabilities into your project, you'll need to add the @angular/localize package:

ng add @angular/localize
Enter fullscreen mode Exit fullscreen mode

This command does two things:

  • Modifies package.json and installs the package.

  • Updates polyfills.ts to include an import for @angular/localize.

Attempting to use i18n without this package produces a straightforward error prompting you to run ng add @angular/localize.

Marking up templates for translation

Before anything can be translated, you need to flag the text in your templates using the i18n attribute.

The i18n attribute comes from the WebExtensions API but is understood by Angular's tooling and compiler. During compilation, it's stripped out, and the associated content is replaced with the translated version.

Here's how you'd mark text:

<span i18n>Welcome</span>
Enter fullscreen mode Exit fullscreen mode

That <span> element is now tagged and prepared for the following phase of the translation workflow.

Localizing Code in TypeScript Files

NB! You need Angular 10.1 or later to extract strings from source code (.ts) files.

Templates aren't the only place where text may need translation. Logic written in TypeScript files can also contain user-facing strings. To mark such strings for localization, wrap them with the $localize template literal:

title = $localize`My page`;
Enter fullscreen mode Exit fullscreen mode

Keep in mind that template literals are delimited by backticks, not by the usual double or single quote characters.

Pulling Out the Strings

Once your app has been annotated, the extract-i18n command scans the code for all marked texts and writes them into a source language file that defaults to messages.xlf.

Several CLI options let you adjust its behavior:

  • --output-path: Specifies a different folder for the resulting file.

  • --outFile: Allows you to give the file a custom name.

  • --format: Selects the output type. The supported options are XLIFF 1.2 (the default), XLIFF 2, and XML Message Bundle (XMB).

When you execute this command from the project's root folder:

ng extract-i18n
Enter fullscreen mode Exit fullscreen mode

The result is a messages.xlf document that looks like this:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en-US" datatype="plaintext" original="ng2.template">
    <body>
      <trans-unit id="3492007542396725315" datatype="html">
        <source>Welcome</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">7</context>
        </context-group>
      </trans-unit>
      <trans-unit id="5513198529962479337" datatype="html">
        <source>My page</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.ts</context>
          <context context-type="linenumber">9</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>
Enter fullscreen mode Exit fullscreen mode

You'll spot the strings "Welcome" and "My page" inside the file, but what exactly is being stored here?

  • trans-unit is the element that wraps one individual translation entry. The id attribute holds a unique identifier that extract-i18n assigns, so leave it untouched.

  • source holds the original text to be translated.

  • context-group describes the location where that particular translation is used.

  • context-type="sourcefile" points to the file containing the translation.

  • context-type="linenumber" identifies the line number of the original code.

With the source file created, the next step is figuring out how to obtain files for the languages you actually want to support.

Producing Translation Files

Once messages.xlf exists, adding a new language is as simple as duplicating it and renaming the copy so that the locale is part of the filename.

For Norwegian translations, you would create messages.nb.xlf. This copy would normally go to a professional translator, who would work on it using an XLIFF editor. However, before jumping to that workflow, it's worth doing a manual translation to gain a clearer picture of how these files are structured.

Doing Manual Translations

Open the file and look for the <trans-unit> element that corresponds to the <h1> greeting tag previously marked with the i18n attribute. Take the <source>...</source> element, copy it beneath itself, rename the copy as target, and then swap its content for the Norwegian text:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en-US" datatype="plaintext" original="ng2.template">
    <body>
      <trans-unit id="3492007542396725315" datatype="html">
        <source>Welcome</source>
        <target>Velkommen</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">7</context>
        </context-group>
      </trans-unit>
      <trans-unit id="5513198529962479337" datatype="html">
        <source>my page</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.ts</context>
          <context context-type="linenumber">9</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>
Enter fullscreen mode Exit fullscreen mode

And that's all there is to inserting translations by hand. Now let's explore what it's like to work with a dedicated tool instead.

Using a Translation Editor

Translation software needs to know which language it's working toward. That information is supplied via the target-language attribute on the root file element, which can be added as follows:

<file source-language="en-US" datatype="plaintext" original="ng2.template" target-language="nb">
Enter fullscreen mode Exit fullscreen mode

Let's load this file into a translation tool to see what the workflow looks like. This article uses the free tier of PoEdit:

Maintaining Multi-language Angular Applications with i18n — figure 2

This is noticeably more user-friendly than editing XML by hand. The tool even provides translation suggestions. Let's translate "my page" and save the file. Opening messages.nb.xlf afterwards should reveal that the tool inserted the translation into a target block, matching the structure we created manually:

<source>My page</source>
<target state="translated">Min side</target>
Enter fullscreen mode Exit fullscreen mode

Notice that the target tag now carries state="translated". This state attribute is optional and can also be set to needs-translation or final, helping editors quickly pinpoint strings that still require work.

So far, so good. But before testing these translations in the application, let's explore some additional metadata we can attach. The box labeled "Notes for translators" in the screenshot is the key to that.

Guidance for localizers

Localizers frequently benefit from additional context about the strings they are working with. A description of the text can be supplied as the value of the i18n attribute:

<span i18n="Welcome message">Welcome</span>
Enter fullscreen mode Exit fullscreen mode

Additional clarity can be passed along by including the meaning of the message. The meaning and description can be combined, separated by the | character: <meaning>|<description>. For instance, we might indicate that this particular welcome message appears within the toolbar:

<span i18n="toolbar header|Welcome message">Welcome</span>
Enter fullscreen mode Exit fullscreen mode

The final attribute that can be placed in the value of the i18n attribute is an ID, introduced with @@. Custom IDs must be unique. If you assign the same ID to multiple different text messages, only the first instance is extracted, and its translation will be substituted for all occurrences of the original text.

This example applies the ID toolbarHeader:

<span i18n="toolbar header|Welcome message@@toolbarHeader">Welcome</span>
Enter fullscreen mode Exit fullscreen mode

Without a manually supplied ID, Angular generates a random one, as shown previously. Running ng extract-i18n again reveals that this context information has been incorporated into the translation unit:

<trans-unit id="toolbarHeader" datatype="html">
  <source>Welcome</source>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">7</context>
  </context-group>
  <note priority="1" from="description">Welcome message</note>
  <note priority="1" from="meaning">toolbar header</note>
</trans-unit>
Enter fullscreen mode Exit fullscreen mode
  • The output now includes note tags that store the description and meaning, and the id is no longer a random string of characters.

Upon copying these entries into messages.ng.xlf and opening the file in PoEdit, all of this information appears in the “Notes for translators” section:

Maintaining Multi-language Angular Applications with i18n — figure 3

Adding context in TypeScript modules

In a manner consistent with Angular templates, developers can supply meaning, description, and id within TypeScript files to offer localizers additional context. The syntax mirrors the i18n markers used in templates. The various configuration choices are listed in the Angular Documentation:

$localize`:meaning|description@@id:source message text`;
$localize`:meaning|:source message text`;
$localize`:description:source message text`;
$localize`:@@id:source message text`;
Enter fullscreen mode Exit fullscreen mode

Attaching an id and a description to our title might be done as follows:

title = $localize`:Header on first page@@firstPageTitle:My page`;
Enter fullscreen mode Exit fullscreen mode

When the template literal string includes expressions, the placeholder name can be placed directly after the expression, wrapped in : characters:

$localize`Hello ${person.name}:name:`;
Enter fullscreen mode Exit fullscreen mode

Specialized translation scenarios

Certain translation use cases require extra attention beyond the standard text content. Attributes, in particular, are easy to miss during localization, yet they play a critical role — especially for accessibility purposes.

Languages vary significantly in how they handle plurals and grammatical structures, which can complicate the translation process. To manage this complexity, plural is used to define pluralized expressions, while select is applied when alternative text options need to be chosen.

Translating attributes

While translating the textual content of HTML elements is the most obvious task, we must also remember that HTML attributes themselves often require translation. This becomes especially important when building applications that are usable by everyone.

Consider an img tag as an example. A screen reader user will not see the image; instead, the content of the alt attribute is what will be announced to them. For this reason, providing a meaningful alt value is recommended whenever it is feasible.

<img [src]="logo" alt="Welcome logo" />
Enter fullscreen mode Exit fullscreen mode

To indicate that an attribute should be translated, prefix it with i18n- followed by the name of the attribute in question. For instance, to translate the alt attribute on an img tag, we would write i18n-alt:

<img [src]="logo" i18n-alt alt="Welcome logo" />
Enter fullscreen mode Exit fullscreen mode

In this example, the text “Welcome logo” becomes the string that is extracted for localization.

You can also assign a meaning, description, and custom ID to an attribute translation by using the
i18n-attribute="<meaning>|<description>@@<id>" syntax.

Handling plurals

The rules for forming plurals are not universal across languages, so all potential variations must be accounted for. The plural clause allows us to define expressions whose translation varies with the number of subjects.

For example, consider a search feature that needs to communicate the number of results. The message should differentiate between “nothing found,” a single result, and multiple results with the count included.

The expression below enables the translation of these distinct plural forms:

<p i18n>
{itemCount, plural, =0 {nothing found} =1 {one item found} other {{{itemCount}} items found}}
</p>
Enter fullscreen mode Exit fullscreen mode
  • itemCount represents the property holding the number of items found.

  • plural specifies the type of translation being used.

  • The third argument enumerates the possible cases (0, 1, other) with the corresponding text to display. The other case serves as the fallback for any unmatched quantity. Angular provides support for additional categories, which are documented here.

When a plural expression is translated, it produces two translation units: one for the text preceding the plural section, and another that contains the plural variants themselves.

Selecting between alternatives

When the rendered text is dependent on a variable's value, each possible output needs its own translation. The select clause functions similarly to plural, but is designed for choosing between different text options based on a given value:

<p i18n>Color: {color, select, red {red} blue {blue} green {green}}</p>
Enter fullscreen mode Exit fullscreen mode

Depending on the value contained in color, the output will be either “red”, “blue”, or “green”. Just as with plural expressions, this translates into two distinct translation units:

<trans-unit id="7195591759695550088" datatype="html">
  <source>Color: <x id="ICU" equiv-text="{color, select, red {red} blue {blue} green {green}}"/></source>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">12</context>
  </context-group>
</trans-unit>
<trans-unit id="3928679011634560837" datatype="html">
  <source>{VAR_SELECT, select, red {red} blue {blue} green {green}}</source>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">12</context>
  </context-group>
</trans-unit>
Enter fullscreen mode Exit fullscreen mode

Translation editors recognize these units and assist with the localized text:

Maintaining Multi-language Angular Applications with i18n — figure 4

Working with interpolation

Now let’s combine a welcome message with the title property:

<h1 i18n>Welcome to {{ title }}</h1>
Enter fullscreen mode Exit fullscreen mode

This inserts the value of the previously translated title variable into the text. When this string is extracted, we can see how the interpolation is represented:

<source>Welcome to <x id="INTERPOLATION" equiv-text="{{ title }}"/></source>
Enter fullscreen mode Exit fullscreen mode

During the translation process, the <x.../> placeholder remains unchanged in the target language:

<target>Velkommen til <x id="INTERPOLATION" equiv-text="{{ title }}"/></target>
Enter fullscreen mode Exit fullscreen mode

This concludes our look at the various translation types. Next, let’s explore how to build and run the application with our new language configured.

Setting up locales

To support multiple languages in an application, the locales need to be declared in the build configuration. Within the angular.json file, the i18n option and its locales property allow mapping locale identifiers to their corresponding translation files:

"projects": {
  "i18n-app": {
    "i18n": {
      "sourceLocale": "en-US",
      "locales": {
        "nb": "messages.nb.xlf"
      }
   }
}
Enter fullscreen mode Exit fullscreen mode

In this example, configuration for Norwegian is added. The translation file path is provided for the "nb" locale, which is still located in the root directory in this instance.

The sourceLocale setting defines the language used in the source code. By default, this is en-US; leaving it unset has the same effect as explicitly setting it. Changing this value affects which locale is used when the application is built alongside the defined locales.

The "localize" option in angular.json instructs the CLI on which locales to generate for a build configuration:

  • Setting it to true builds all locales previously defined in the build configuration.

  • Providing an array of specific locale identifiers from the defined set builds only those versions.

The development server can only handle one locale at a time. If "localize" is set to true and more than one locale is defined, using ng serve results in an error. However, setting it to a single locale, like "localize": ["nb"], allows development against that specific language.

To enable running ng serve with a single language, a custom configuration specifying just the one locale can be added to angular.json:

"build": {
  "configurations": {
    "nb": {
      "localize": ["nb"]
    }
  }
},
"serve": {
  "configurations": {
    "nb": {
      "browserTarget": "ng-i18n:build:nb"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With this setup, the Norwegian version can be served and the translations checked by passing nb to the configuration option:

ng serve --configuration=nb
Enter fullscreen mode Exit fullscreen mode

Similarly, a build can target a specific locale:

ng build --configuration=production,nb
Enter fullscreen mode Exit fullscreen mode

Or a build can generate all the locales at once:

ng build --prod --localize
Enter fullscreen mode Exit fullscreen mode

In essence, the approach of defining a custom configuration provides more control, although simply setting both localize and aot to true would suffice for a basic setup.

Serving multiple languages locally

For reasons of performance, ng serve handles only a single locale at a time. As mentioned, passing a locale to the configuration option serves that particular language. But what about running the application with all configured languages?

The challenge of multiple locales

To serve all languages simultaneously, the project must be built first. Using the localize option allows for building applications for the locales defined in the build configuration:

ng build --prod --localize
Enter fullscreen mode Exit fullscreen mode

With the localized build complete, a local web server is necessary to serve the applications. The complexity here comes from the fact that there is a separate application output for each language.

The Angular documentation offers several server-side code examples that can be adapted.

Using Nginx

The steps to get the application running with Nginx are as follows:

  1. Install Nginx.

  2. Add the configuration from the Angular Docs to conf/nginx.conf.

  3. Build the applications.

  4. Copy the built applications into the directory designated by the root directive in nginx.conf.

  5. Open localhost in a browser.

The port, typically 80, is configured in the listen directive. The language is switched by altering the URL. The Norwegian app, in this case, is accessible at localhost/nb.

Below is a sample nginx.conf file:

events{}
http {
  types {
    module;
  }
  include /etc/nginx/mime.types;

  # Expires map for caching resources
  map $sent_http_content_type $expires {
    default                    off;
    text/html                  epoch;
    text/css                   max;
    application/javascript     max;
    ~image/                    max;
  }

  # Browser preferred language detection
  map $http_accept_language $accept_language {
    ~*^en en;
    ~*^nb nb;
  }

  server {
      listen       80;
    root         /usr/share/nginx/html;

    # Set cache expires from the map we defined.
    expires $expires;

    # Security. Don't send nginx version in Server header.
    server_tokens off;

    # Fallback to default language if no preference defined by browser
    if ($accept_language ~ "^$") {
      set $accept_language "nb";
    }

    # Redirect "/" to Angular app in browser's preferred language
    rewrite ^/$ /$accept_language permanent;

    # Everything under the Angular app is always redirected to Angular in the correct language
    location ~ ^/(en|nb) {
      try_files $uri /$1/index.html?$args;

      # Add security headers from separate file
      include /etc/nginx/security-headers.conf;
    }

    # Proxy for APIs.
    location /api {
      proxy_pass https://api.address.here;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

When Nginx is used in production, it's prudent to replicate that environment locally for testing.

Preparing for production deployment

If Nginx is the production server, the language configuration is already in place. For other server setups, the necessary adjustments will need to be determined.

It's important to differentiate between local development and production. The isDevMode function can be leveraged to check if Angular is running in development mode:

isDevMode() ? '/' : `/${locale}/`;
Enter fullscreen mode Exit fullscreen mode

Consequently, when serving the app locally with ng serve, the locale is not a part of the URL, unlike the localized production build where it is required in the path.

Keeping the Project Healthy After Launch

Once the app is live, the work isn't over. There are a few operational concerns to think about. The most significant one revolves around managing our localization files. We have to ensure that every string we've marked in our templates is properly routed to translators, and then find its way back into the codebase before the next release. To streamline this, we need to set up an automated pipeline for generating these translation files and introduce a system that alerts us if any translations are missing.

Automating Translation File Creation

Manually merging translation files is not a viable long-term strategy; automation is essential. To achieve this, I'm leveraging a free library named Xliffmerge.

Be aware that this tool lists older Angular versions as peerDependencies. If you're using a recent version of NPM (v7 and above), you'll need to include the --legacy-peer-deps flag during installation to prevent conflicts.

While the Xliffmerge documentation focuses on outdated Angular versions, I discovered through testing that simply installing the @ngx-i18nsupport/tooling package is sufficient.

npm install -D @ngx-i18nsupport/tooling --legacy-peer-deps
Enter fullscreen mode Exit fullscreen mode

The -D flag saves the package to devDependencies. If you plan to use this in a CI/CD pipeline, you should omit this flag so it's installed as part of the main dependencies.

Next, you need to register the new languages in your project's configuration. This is done within the angular.json file, under the projects -> projectName -> architect -> xliffmerge section.

"xliffmerge": {
  "builder": "@ngx-i18nsupport/tooling:xliffmerge",
  "options": {
    "xliffmergeOptions": {
      "defaultLanguage": "en-US",
      "languages": ["nb"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Once the languages are configured, you can extract all the newly marked strings and merge them into your translation files with a single script execution.

ng extract-i18n && ng run projectName:xliffmerge
Enter fullscreen mode Exit fullscreen mode

When you run this script, you'll see several warnings. This is a good sign, as it indicates the process is functioning correctly.

WARNING: merged 1 trans-units from master to "nb"
WARNING: please translate file "messages.nb.xlf" to target-language="nb"
Enter fullscreen mode Exit fullscreen mode

With the files updated, they can be sent to your translation team. Once the translated files are returned, they can be merged back into your main repository.

A word of caution: this library was not actively maintained at the time of writing. You may want to explore alternatives. There is a long-standing Angular feature request regarding the merging of translated files. If this is a feature you need, a quick upvote on the issue could help prioritize it.

Detecting Untranslated Strings

To proactively manage translation quality, you can configure the build process to alert you when strings are missing. By default, The Angular compiler only issues a warning for these missing translations. However, you can adjust the severity of this notification:

  • error: The build process halts and an error is generated.

  • warning (default): A warning is printed to the console or shell.

  • ignore: The compiler takes no action.

This level is set within the options of the build target in your angular.json configuration file. The example below demonstrates how to change the build action to hard-fail with an error.

"options": {
  "i18nMissingTranslation": "error"
}
Enter fullscreen mode Exit fullscreen mode

If a translation is missing, the application will render the text from the original source language. You'll need to assess how critical this is. For applications where accurate localization is non-negotiable, configuring the build to fail is the most reliable way to guarantee all translations are in place.

Localizing Data Formats

Beyond translating words, consider cultural conventions for representing data. Date and number formats differ significantly across regions, so you must account for these when serving international customers.

Angular uses the LOCALE_ID token to identify the active locale, and you can enrich it by registering locale data via registerLocaleData(). The Angular CLI simplifies this by automatically setting the LOCALE_ID and including the necessary locale data when you use the --localize option during ng build or a specific configuration with ng serve.

Once the correct locale is set, you can utilize Angular's built-in pipes to format data appropriately. The framework offers these pipes for different data types:

  • DatePipe: Handles the formatting of date values.

  • CurrencyPipe: Transforms a numeric value into a currency string.

  • DecimalPipe: Formats a number with decimal points.

  • PercentPipe: Converts a number into a percent-formatted string.

For instance, the template expression {{myDate | date}} applies DatePipe to display a date according to the locale. These same pipes are also injectable and usable in your TypeScript classes when they are provided in your module.

Translating on the Fly

The standard compilation process, via ng serve or ng build --localize, performs translations ahead of time. However, Angular provides a mechanism for runtime translation. If you skip the localization build step, the $localize tags remain embedded in the code. This allows you to load the necessary translations after the app has been delivered to the browser.

loadTranslations, which is part of @angular/localize, is the function you'll use. It accepts a set of key/value pairs to inject the translations into the application before it boots up.

Since this must run before any application module is loaded, the most common spot is within polyfills.ts. Alternatively, you could use it in main.ts by wrapping your bootstrap logic in a dynamic import(...).

Here's a sample of how to integrate loadTranslations in polyfills.ts:

import '@angular/localize/init';
import { loadTranslations } from '@angular/localize';

loadTranslations({
  'welcome': 'Velkommen'
});
Enter fullscreen mode Exit fullscreen mode

This approach produces a result similar to build-time translation but with more operational flexibility. Just know that translations are processed a single time. To switch to a different language, the entire application needs a full restart. Because $localize processes messages only once, dynamic language switching without a browser refresh isn't supported.

The primary advantage is the capability to ship a single build output while managing numerous translation files outside of it. Official Angular documentation for this workflow is still in progress; there's an open request for related docs. In the meantime, third-party libraries like Soluling are attempting to fill that gap.

If you require a dynamically switchable and runtime-oriented internationalization setup, take a look at Transloco.

Final Thoughts

The discussion opened by examining how the Ivy engine transformed the way Angular handles i18n and application localization. We weighed the advantages and limitations of this shift, and considered scenarios where adopting third-party tools might be preferable.

Next, we integrated the standard package into a project and marked the text that required translation. We configured the app for localization and added utilities to handle translation files efficiently. Working with a translation editor revealed the importance of attaching context to each translation entry.

Once the configuration and translation steps were complete, we deployed a web server to run the application both in a development environment and in production.

Localization touches many moving parts, and the goal of this guide was to clarify the process. By the end, you should feel more confident about building and maintaining Angular applications that serve multiple languages.

Further Reading