Developer Docs

Internationalisation (i18n) with Angular and Java Spring in SpringBot

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:

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:

    Server-side i18n resource bundle screenshot

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

  2. Create three new files inside of this directory. The first called , the second called and the third This will give us three languages that we can use.

     ├── i18n
     │   ├──
     │   ├──
     │   └──

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

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

     invalid_credentials_error_description=The username/password combination is invalid.

     invalid_credentials_error_description=La combinaison nom d'utilisateur / mot de passe n'est pas valide.
  4. 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 file found at serverside\src\main\resources\

     # % protected region % [Add any additional properties here] on begin
     # % 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.

    Message files tree structure

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 file found at serverside/src/main/java/lmsspring/configs/security/handlers/

  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;
  2. Inject the MessageSource bean using the protected region called Add any additional class fields here:

     private MessageSource messageSource;
  3. 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.

         messageSource.getMessage("invalid_credentials_error_description", null, LocaleContextHolder.getLocale())
    Diff view of adding our message source

    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.

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

    Screenshot of headers being added to request
  3. 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:

  1. Update our model with a locale entity. This will supply the first two files for us.

    Our entity should appear as follows:

    Locale Entity

    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.

  2. Add a new method to our ApplicationLocaleRepository class found at repositories/ 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.

  3. Create a new package called i18n under the configs package. Inside of this package create a new class called MessageSource.
  4. 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.stereotype.Component;
     import java.text.MessageFormat;
     import java.util.Locale;
     public class MessageSource extends AbstractMessageSource {
       private final ApplicationLocaleRepository localeRepository;
       public MessageSource(ApplicationLocaleRepository localeRepository) {
         this.localeRepository = localeRepository;
       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.

    Client-side message files
  2. Populate these files with our messages within a nested JSON structure. For example

    Diff view French vs English message files

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

  3. 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-translate you need two key packages:

* Core - [npx-translate/core](
* A loader - for example, [npx-translate/http-loader](


  1. Install both packages:
    1. yarn add @ngx-translate/core@12
    2. yarn add @ngx-translate/http-loader@5

    Please 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);
  3. In the same file, add the following into the protected region called Add any additional module imports here:

       loader: {
         provide: TranslateLoader,
         useFactory: HttpLoaderFactory,
         deps: [HttpClient]


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';
  3. Inject it using our constructor by adding the following into the protected region labelled Add any additional constructor parameters here here:

     public translate: TranslateService,
  4. 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']);
     const browserLang = translate.getBrowserLang();
     translate.use(browserLang.match(/en|fr/) ? browserLang : 'en');
  5. 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:

       <h2>{{ 'page.login.title' | translate }}</h2>
         {{ '' | translate }}
         <select #langSelect (change)="translate.use(langSelect.value)">
           <option *ngFor="let lang of translate.getLangs()" [value]="lang"
             [selected]="lang === translate.currentLang">{{ lang }}</option>

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

    Example of language dropdown in action

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.


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.

Was this article helpful?

Thanks for your feedback!

If you would like to tell us more, please click on the link below to send us a message with more details.







On this page

New to Codebots?

We know our software can be complicated, so we are always happy to have a chat if you have any questions.