In this article, we will explore how React and Angular applications compare when it comes to Internationalization (i18n). We will create a sample application in English and then convert the content into a different locale. This article does not go into the details of either of these frameworks but rather focuses on their ability to facilitate i18n and their ease of use.

Table of Contents

  1. Setting a Goal
  2. Understanding Terminology
  3. Implementing Internationalization in Angular applications
  4. Implementing Internationalization in React applications
  5. Caveats and Comparison

Setting a Goal

Before we start with coding and deep diving into any of the details, it is important to create a goal for this article. By the end of the article, we will achieve the following:

  1. Create an Angular and React application with no Internationalization and then converting them into one with the ability to handle multiple languages.
  2. Determine ways to serve the application with minimal overhead.
  3. Compare and contrast the i18n capabilities and the ease of use of each framework.

Understanding Terminology

First, we need to define the terminologies that we would be using throughout this article:

Locale: A geographic region or a group of settings that the user has configured or is preconfigured for them eg. en-US refers to American English with the US locale, while en-GB refers to British English with Great Britain locale.

Internationalization: Ability of an application to support multiple locales.

Localization: Translating the (internationalized) application into a different locale.

Implementing Internationalization in Angular applications

We need to create a bare application with some basic routing and content before we add internationalization. We will use the Angular CLI to create a new application:

ng new ng-internationalization

and add some components:

ng g component home
ng g component settings
ng g component about

Then, create a file in the src/app folder called app.routing.module.ts and set up the basic routing necessary to reach these newly created components as shown below:

Finally, include the newly created AppRoutingModule in the main application module found in app.module.ts under the imports section after the BrowserModule. Also, add links in our app.component.html to navigate to each of these components based on the route definition.

With these changes in, our basic application is ready to be started. This would also be a good time to add some content to our home page which we will be localizing later on:

and we initialize those variables in the component:

Run the command npm start from the root of the project and navigate to http://localhost:4200 to see your application running.

Adding Internationalization

Now that we have the base application in place, the changes needed to enable i18n are fairly straightforward thanks to the out-of-the-box i18n support provided by Angular.

The entire process can be broken down into 3 simple steps:

  1. Mark the translatable content and generate translations
  2. Generate locale-specific bundles
  3. Create a server to identify and serve the right content based on the requested locale

Marking Translatable Content

For localizing the application, we need to mark all the text that needs to be translated. In the past version of Angular (i.e. 1.x version a.k.a AngularJS), we were forced to create a json file with our various translations text in English and then provide them to translators who would convert them to whatever language we needed. We then had the ability to dynamically switch the language within our application which was resolved using a translationdirective.

Things are a little different in the latest version of Angular. We can still use the old way of translations using a library such as ngx-translate. However, regardless of internationalization, we would want to enable AOT builds i.e. Ahead of Time compiled code so that we can remove any unnecessary payload /code from the final distribution bundle (such as the compiler, additional translation files etc). The downside of this is that we end up with one distribution folder per locale. For this article, we are moving forward with the assumption that our application in production in running in AOT mode.

We first need to mark the text for translation using the i18n directive. Using the directive is very easy, we only need to provide the value in the following format and the entire thing is optional.

i18n="Meaning | Description @@UniqueIDToIdentifyThisElement"

Once we have marked the entire text that needs to be translated, we can run the xi18n command provided by Angular CLI to generate the translations file in any of the 3 supported formats (XLF, XLF2 or XMB) which can be passed to the translators. The generated file also persists the context of the translation such as the location of the translation id, its meaning and description.

As long as the @@id is not removed or changed, the translation can be moved around and reused throughout the application. In our case, our base locale is English i.e. when we code, we write the text in our template in English so when we generate the translations file, we want to denote the same. This can be easily done by marking the file with the right locale extension:

ng xi18n --output-path locale --out-file messages.en.xlf

Running this command creates the XML file messages.en.xlf under the src/localefolder.

One caveat to using this translation file as default is that Angular expects a base messages.xlf file which we have renamed to messages.en.xlf instead. So to offset that, we simply need to add the translations to the en locale file which would be same as the source, for example, if we have a source node labeled Home, we need to create a target node with the same label in the generated XML file.

Notice that all the sources and the target nodes are the same text in this case.

If we now wish to add an additional language, such as French, we only need to get the new xlf file for the same. In our example application, since we do not have access to real translators, we can simply copy paste the English xlf file into a new file called messages.fr.xlf and then append FR- to denote the French translation.

Note that the target node is the same as the source node with FR- prefix.

Generating locale-specific bundles

To build the code, we typically use the ng build command which accepts optional parameters. In this case, let us make an assumption that during development, we only wish to work with the English version of the application. We make this assumption to avoid changing the angular.jsonfile.

The angular.json file can be updated to create multiple profiles for building and running the application with different locales as shown here.

We are going to create a small script which will create builds for all the locales we support by passing in optional locale-specific params to the ng buildcommand.

We first add the supported locales list as allLocales to the package.json file which will act as the single source of truth for our application moving forward.

We can now consume this within our custom build script:

The most important line in the above script is where we are running the build command with all our custom, locale-specific parameters:

ng build 
    --aot 
    --base-href=/${locale}/ 
    ${args} 
    --i18n-file src/locale/messages.${locale}.xlf 
    --i18n-format xlf 
    --i18n-locale ${locale} 
    --output-path dist/${locale}/

We are creating the AOT builds with the base href of the application set to the locale name, that ensures every request originating from the locale-specific template has the locale prefixed to the request.

To generate the locale-specific builds, run the below command with the optional prod flag:

npm run build -- --prod

which logs the following when the build is successfully run:

We can also verify the result of the build by opening the dist folder and verifying the base href path for any locale:

Creating a Server

Now that we have locale-specific bundles ready to go, we need a server to check the request that is originating from the browser and then serve the content based on the requested locale or default the user to a pre-determined locale (which is assumed to be en).

To achieve this, we are going to use an Express server which has a middleware to implement our locale logic:

we simply break down the incoming request to determine whether the request has an existing locale, an unknown locale or if it is a base request to load the index page and then handle it accordingly. We also generate a static path for each of the supported locales and ensure that they point to the right folder under dist.

To test the changes, run node server.js from the root of the project and we can see the application start at port 3000.

Open http://localhost:3000 and we can see it load the content from the dist/en/ which is the default in case the locale is not specified, the URL now has /en/ in it as the base path is set to /en/:

We can change the path in the URL to load from the French locale as well

We can see that it not only loads the translations as expected, we also get the localized values for the date, number and currency pipe that we have used thanks to the--i18n-locale flag that we passed in while generating the build.

Implementing Internationalization in React applications

As in the case of the Angular application, we need to first set up the base project which has 3 routes and some basic localizable content on the home page.

We will be using create-react-app to set up the project:

create-react-app react-internationalization
cd react-internationalization

Then, create the 3 components Home, Settings and About which, for now, look the same.

In the Home component, we are going to add some nuances as we did in case of the Angular example above:

We can now invoke these components from the template as routes by using react-router and react-router-dom so let us install those:

npm i -S react-router react-router-dom

and update our App.js file:

We have exported the routes into their own file for modularity:

Finally, wrap the main application into the BrowserRouter and we are ready to go:

Adding Internationalization

Our base application is now ready to be internationalized. As is the case with most of the React projects, we first need to pick a library of our choice which allows us to perform internationalization. This is not a negative in any way since this is how React was meant to be. In this article, we will be using react-intl which provides the components needed for the localization of messages and react-intl-translations-manager which helps with the generation of the translation files.

yarn add react-intl && yarn add --dev react-intl-translations-manager

The process with React application is less convoluted as compared to Angular as it needs only two things from us:

  1. Marking translatable content from the application
  2. Generate translations for locale

Marking Translatable Content

To mark the content as translatable, we will be using the translation-manager that we installed earlier. First, we need to mark the content which requires us to convert all the text into a JSON object which has an id associated with it. So, for the content that we have, we would need to transform it into something that looks as below in a file under src/translations/extractedMessages:

And on the templates, we only need to refer to the id that we have defined with each message using the FormattedMessage component:

To combine these two, we need to use the react-intl babel plugin which requires the path to all the extractedMessages i.e. path to the App.json file which we created earlier. To invoke the plugin, update the package.json with the new babel configuration:

The enforceDescription flag prevents warning messages in case description is not provided with each of the elements in the App.json file.

With that, we have made the necessary changes for marking the content to be available for translations.

Generate translations for locale

We are now ready to generate the translations per locale. Similar to the Angular application, let us update our package.json file with a property called allLocales which lists all possible locales that we wish to support. Which will be en and fr for now.

To generate the translations, we could use a small script created at the root of our project which invokes the translation manager as shown below with the locals of our choosing, our input extracted translation messages and our output destination folder:

to invoke it, simply run node translationRunner.js and that would generate our en.json and fr.json files which look as follows:

Similarly, even the fr.json file has the same content which we need to get translated, but, we will instead append FR- to indicate the difference between the two:

To consume these translations now, we are ready to modify the root of our application and provide the translations as necessary based on the user selection.

Before we do that, we still have a couple of tasks to do:

  1. Create an index.js file under locale folder which can export all the available translation messages (en.json and fr.json in this case).
  2. Update Home.js to consume FormattedDate and FormattedNumbercomponents along with the FormattedMessage component to show the correct format based on the selected locale.

For the currency field, we have used a custom format called USD to indicate that the currency is USD, this is not something that is built into the react-intl library but they provide us the capability to create and manage our own formats which are pretty much JavaScript objects which can be passed into the react-intl provider:

Which we can then embed into our index.js file to combine everything:

We can now run the application, and we can dynamically switch the locale and rerender the entire application:

Caveats And Comparison

Now that we have both our application running as expected, let us discuss some caveats within each of the approaches:

  1. Angular kind of corners you to generate a new distribution per locale which in my opinion is not really the worst idea since there is no overhead at runtime. React, on the other hand, still has to evaluate and re-evaluate all the extra components which are provided by react-intl (which I’m sure to have some form of memoization under the hood).
  2. Angular i18n requires a full page refresh when trying to switch the user locale, unlike React. Which could be either an advantage or a disadvantage based on personal preference.
  3. In the case of Angular, we need to add a server which can handle and serve the right distribution files based on the selected locale. This means added code, testing etc and your application is no longer a static bundle which is much easier to deploy.
  4. Angular provides some common pipes such as number, currency, and date which handle all the locale-specific changes internally unlike react-intlwhere sometimes we need to provide custom formats based on our use case and locale.
  5. In the Angular app, if we change the default output file name we need to manually add the targets to the XLF file which is far from ideal and a pain in case there are many translatable fields.
  6. In the case of Angular, we need to depend on libraries such as ngx-translate to provide missing functionality such as translating strings which are not already on the template. Making it a favorable alternative over the out of the box i18n. Follow this open GitHub issue to learn more and check out some possible workarounds.
  7. Both Angular and React application templates are polluted with translation specific code either as the i18n directive or the use of an additional component.
  8. An approximate bundle size (of the entire bundle) comparison for final distribution is as follows (No Source Maps. Styling, and functionality as similar as possible):

Angular version of the sample was completed before the new Ivy compiler was launched, the payload size with Ivy enabled might differ as it now comes with Tree Shaking out of the box.

The code base for both of these example projects is available here: AngularReact.