Start modelling your app today.

Get started for free

What's this?

Internationalisation (i18n) with Angular and Java Spring in SpringBot

i18n as defined in RFC-6365 is an abbreviation for “internationalisation”; the term used to describe the means of adapting software to support different locales, with changes in language and time-zone being achieved without significant software changes.

For applications that support multiple regions, it is often important to also support the different languages found within these regions and as such most large web frameworks support i18n to some degree out of the box.


The problem

This article will be exploring the process of adding i18n to a java Spring Boot and Angular application, as such, we will be making use of an application built with SpringBot.

The example application we will be using is the Learning Management System (LMS) - Example project.

What we want to achieve is the ability to support multiple languages application wide.

Some of the questions that we need to answer are:

  • How is a language selected?
  • Where are our translations stored and retrieved?
  • How do we manage incomplete translations?
  • How do we supply new translations?

Spring Boot server-side

Spring Boot provides some i18n support as part of the core framework namely through the use of messages. More details can be found in the Spring Boot documentation. We will leverage this support in our own implementation.

How is the language selected?

One of the key advantages of making use of the inbuilt Spring Boot i18n support is that the solution to this question is already solved for us. Spring Boot will set the current locale of the application based upon the value of the Accept-Language header in any given request.

The value of this header is often set by the users browser language value and, as such, is updated automatically. Spring Boot will attempt to match the value of this header with one of the known locales.

If you wish to change how this works, a custom locale resolver can be implemented. This will not be covered in this article but a good starting point can be found in the Spring documentation regarding Using locales.

Where are our translations stored and retrieved from?

To start with, we will be sourcing our translations from a resource bundle. A resource bundle is a collection of property files that follow a specific naming convention. These property files will present our various translations through the keys of the each property being used to reference the translated values.

For the purpose of this example, we will ensure that all our files have the following message defined:

invalid_credentials_error_description=The username/password combination is invalid.

In our case, we will be creating a resource bundle called messages.

Creating a resource bundle
  1. Create a directory under serverside/src/main/resources/ called i18n. Your file structure should now look like the following:

This directory will be the home to the contents of our bundle.

  1. Create three new files inside of this directory. The first calledmessages.properties , the second called messages_en_AU.properties and the third messages_fr_FR.properties. This will give us three languages that we can use.
├── i18n
│   ├── messages_en_AU.properties
│   ├── messages_fr_FR.properties
│   └── messages.properties

These message files follow the naming convention of messages_<locale_code>.properties. In our example, the file messages_en_AU.properties represents an Australian English locale.

  1. Add our first message to all of these files, feel free to offer up your own translations for each, for example:

messages.properties

invalid_credentials_error_description=The username/password combination is invalid.

_messages_frFR.properties

invalid_credentials_error_description=La combinaison nom d'utilisateur / mot de passe n'est pas valide.
  1. Now that we have created our resource bundle files, we now need to map them to our bundle name. We achieve this by updating our properties file. To ensure our bundle is available in all application profiles, we will add the following lines into the application.properties file found at serverside\src\main\resources\application.properties.
# % protected region % [Add any additional properties here] on begin
spring.messages.basename=i18n/messages
# % protected region % [Add any additional properties here] end

The value of this option is the combination of the folder name (i18n) and the bundle name (messages). Once set, using some IDE’s our messages will now be considered as a resources bundle.

Loading our resource bundle into the application context

Now that we have created our resource bundle, the next step is to tell the application that it exists. There are a few small configuration changes that need to be made to our application to achieve this.

For this example we will be updating one of our custom error handlers to demonstrate how you would make use of our translations.

We will be using the CustomAuthenticationFailureHandler.java file found at serverside/src/main/java/lmsspring/configs/security/handlers/CustomAuthenticationFailureHandler.java.

  1. Open up the file and activate the protected region called Add any additional imports here before adding the following imports:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
  1. Inject the MessageSource bean using the protected region called Add any additional class fields here:
@Autowired
private MessageSource messageSource;
  1. Now that we have access to our message source, we can override our default error messages. Replace our error_description contents with our locale resolved message using the following snippet.
rootNode.put(
        "error_description",
        messageSource.getMessage("invalid_credentials_error_description", null, LocaleContextHolder.getLocale())
);

We are using our message key to reference our language which will automatically be resolved for us.

PLEASE NOTE: This example is not inside of a protected region and as such is used for demonstration purposes only. See Protected Regions for details.

Testing our locale resolver

To test our local resolver we are going to use a REST API tool called Insomnia to allow us to send HTTP requests to our server-side application. Feel free to use your own HTTP tool of choice.

  1. Create a new POST request to our login endpoint.

  1. Add the Accept-Language header, we will be turning this on and off to demonstrate the default messages vs our French message resolution.

  1. Trigger the request

We can see that our message response is being replaced with our translation.

Sourcing our messages from a database

So far we have been sourcing our messages from a resource bundle. This works well for most scenarios but has the drawback of having to redeploy our application every time we want to make a change to an existing translation or to add a new one.

One solution to this problem is sourcing our messages from a database.

To achieve this we will be creating three new files:

  • Message entity,
  • Message repository, and
  • Custom message source
  1. Update our model with a locale entity. This will supply the first two files for us.

Our entity should appear as follows:

NOTE: Make sure you allow the appropriate level of access to this entity in the security diagram. i.e Visitors should be able to read.

  1. Add a new method to our ApplicationLocaleRepository class found at repositories/ApplicationLocaleRepository.java. We will add this to the protected region Add any additional class methods here. The method will appear as follows:
Optional<ApplicationLocaleEntity> findByKeyAndLocale(String key, String locale);

This method will allow us to query a message per locale.

  1. Create a new package called i18n under the configs package. Inside of this package create a new class called MessageSource.
  2. Populate this message source with the following contents:
package lmsspring.configs.i18n;
import lmsspring.repositories.ApplicationLocaleRepository;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.stereotype.Component;

import java.text.MessageFormat;
import java.util.Locale;

@Component("messageSource")
public class MessageSource extends AbstractMessageSource {

    private final ApplicationLocaleRepository localeRepository;

    @Autowired
    public MessageSource(ApplicationLocaleRepository localeRepository) {
        this.localeRepository = localeRepository;
    }

    @Override
    protected MessageFormat resolveCode(@NonNull  String key, @NonNull Locale locale) {
        var messageOpt = localeRepository.findByKeyAndLocale(key, locale.toLangageTag());

        // The message may be missing from the database, we will want to handle this better for a production system
        var message = messageOpt.orElseThrow();

        assert message.getValue() != null;
        return new MessageFormat(message.getValue(), locale);
    }
}

This will allow us to fetch our messages from the database. We can pre-load default values by seeding data. See SpringBot Seeding Data for details.

Recommendation: Given that these messages will be queried often it is recommended to implement caching.

Angular client-side

Now that we have our messages being fetched on the server, we want translate our client-side application. Given that the client-side is our presentation layer, the internationalisation of our Angular app is arguable more important than our server.

Built in i18n

Angular comes with i18n support baked in that allows for message substitution at compile time. This functionality works in a similar way to how the server-side does with a set of messages for each translation desired.

To make use of the default i18n support we would complete the following steps:

  1. Create two new files two files under a new directory called i18n within clientside/src/assets, once for English and one for French.

  1. Populate these files with our messages within a nested JSON structure. For example

The trick here, same as on the server-side, is to ensure that the keys match.

  1. Now we can make use of these translations within our components.

For details please see the official Angular docs for i18n support as while this is a powerful tool that unlocks the power to translate our site into many different languages, it comes with one big drawback, it cannot be switched on the fly requiring us to produce version of the client-side available for each language we wish to support.

Dynamic i18n

What we want to achieve with our i18n as shown with our server-side implementation is flexibility, we want to be able to switch languages on the fly based upon input such as a users selection. To achieve this we need to expand beyond our baked-in support. To achieve this level of flexibility, we will be using a third party library called npx-translate which provides us the ability to support many different locales at the same time.

To make use of npx-translat need two key packages:

  1. Core - npx-translate/core
  2. A loader - for example, npx-translate/http-loader
Setup
  1. Install both packages:
    a) yarn add @ngx-translate/core@12
    b) yarn add @ngx-translate/http-loader@5

    NOTE: These versions are specific to Angular 9

  2. Update the app.module.ts found at clientside\src\app\app.module.ts. Add the following into the protected region Add any additional imports here:

import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';

export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http);
}
  1. In the same file, add the following into the protected region called Add any additional module imports here:
TranslateModule.forRoot({
    loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient]
    }
})
Usage

For this example we will be customising the login component to have a language selector that allows us to switch between our two languages, French and English.

  1. Import our TranslateModule into our login.tile.module.ts file found at clientside/src/app/lib/tiles/login/login.tile.module.ts by adding TranslateModule to the protected region labelled Add any additional module imports here and adding the import import { TranslateModule } from '@ngx-translate/core'; into the protected region labelled Add any additional imports here.
  2. Open clientside\src\app\lib\tiles\login\login\login.component.ts and add the following import into the protected region labelled Add any additional imports here:
import { TranslateService } from '@ngx-translate/core';
  1. Inject it using our constructor by adding the following into the protected region labelled Add any additional constructor parameters here here:
public translate: TranslateService,
  1. Within our constructor body add the following into the protected region labelled Add any additional construct logic before the main body here:

NOTE: This could be added somewhere globally.

translate.addLangs(['en', 'fr']);
translate.setDefaultLang('en');

const browserLang = translate.getBrowserLang();
translate.use(browserLang.match(/en|fr/) ? browserLang : 'en');
  1. Finally, make use of the directive in our template by adding the following to the protected region labelled Add additional content here above the login form:
<div>
    <h2>{{ 'page.login.title' | translate }}</h2>
    <label>
        {{ 'page.login.select' | translate }}
        <select #langSelect (change)="translate.use(langSelect.value)">
            <option *ngFor="let lang of translate.getLangs()" [value]="lang"
                [selected]="lang === translate.currentLang">{{ lang }}</option>
        </select>
    </label>
</div>

This will add a language selector and a second title at the top of our login page that looks like the following:

sdsds

Database backed

Given that we now have two sets of translations, if we wished to make use of the server-side messages we could implement a custom translation loader to replace our http-loader that we are currently using.

This translation loader is a class that implements the TranslateLoader interface and our implementation could query our API for each message.

Conclusion

Internationalisation requires both server-side and client-side implementations to properly support a language. Key considerations when adding internationalisation are, maintainability, performance, and error handling. This the examples in this article only briefly touch on performance and error handling in favour of focusing on the maintainability of the locale sets.

While adding i18n support to your applications can be complex, hopefully this article has shown that it does not have to be.


Start modelling your app today.