Creating a multi lingual web application is a common task in modern web development. Most (old-school) java web frameworks offer out-of-the-box solutions for this task, but for single-page-application frameworks like Vaadin, you need to come up with some fresh ideas for localization.

The business requirements for a Vaadin i18n application

I would like to collect the business requirements I want to satisfy with my i18n approach.

  • readable urls like /webshop/category/product
  • bookmarkable urls containing the language like /webshop/en/payment
  • fast and lightweight design (i18n code should not block or interfere with business logic)

Vaadin does not support all these business tasks out of the box, but the Vaadin eco-systems offers us some plugins, that can be a good help to solve this task.

  • Vaadin CDI (JEE CDI usage)
  • use @com.vaadin.navigator.PushStateNavigation for readable urls
  • and we need some custom code to customize it for our needs and combine it all
  • use java.util.ResourceBundle to store translated labels and messages

I created a small show-case application where these business requirements are solved. You can clone or download the code from Bitbucket at:
https://bitbucket.org/mekaso/vaadin-cdi-mvp/branch/i18n

The application

The application UI has a navigation on top of the page where the user can click all registered views and choose the current language out of the 2 supported languages (english and german). For the good-looking navigation I used the Hybrid-Menu addon (https://vaadin.com/directory/component/hybridmenu).

The application start screen

The application supports english and german languages. When calling the application at their context root /vaadin-cdi-mvp the application should detect the current browsers locale. If the language of this locale is one of the supported languages, the UI should be displayed in this language. If the browser’s locale language is not supported by our application it should be displayed in english (as it is the default language). This (common) behaviour is handled in our UI class:

@CDIUI("")
@Theme("mekaso")
@PushStateNavigation
public class ApplicationUI extends UI {
  private static final long serialVersionUID = 1L;
  @Inject
  private CDINavigator navigator;
  @Inject
  private AppLayout layout;
  @Inject
  private I18nHandler i18nHandler;
  @Inject
  private ViewProvider viewProvider;
  @Inject
  private javax.enterprise.event.Event<Locale> localeChangeEventSender;
  
  @Override
  public void init(VaadinRequest request) {
    this.navigator.addProvider(this.viewProvider);
    super.setSizeFull();
    super.setContent(this.layout);
    //this.navigator.setErrorView(ErrorView.class);
    this.navigator.init(this, layout.getContent());
    Locale requestLocale = request.getLocale();
    if (!this.navigator.getState().isEmpty()) {
      int slashIndex = this.navigator.getState().indexOf("/");
      String loc = this.navigator.getState().substring(0, slashIndex);
      requestLocale = new Locale(loc);
    } 
    this.layout.addMenu();
    this.i18nHandler.init(requestLocale);
    this.localeChangeEventSender.fire(this.i18nHandler.getLocale());
    if (this.navigator.getState().isEmpty()) {
      // forward to start view person
      this.navigator.navigateTo(this.i18nHandler.getLocale().toString() + "/person");
    }
  }
}

After detecting the language, the I18NHandler class (where the resource bundles are handled) is inited with the detected language and the application redirects to the main view (PersonView).

All labels and messages are stored in .properties files for the 2 supported languages english and german. We can access these files with a ResourceBundle. As we need the internationlized labels in all of our view classes and the internationalized messages in all our presenter classes, I encapsulated the access of the resource bundle in an injectable class.

The I18nHandler class

import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.annotation.PostConstruct;
import com.vaadin.cdi.UIScoped;

@UIScoped
public class I18nHandler {
  public final List<Locale> supportedLocales = Collections.unmodifiableList(Arrays.asList(Locale.ENGLISH, Locale.GERMAN));
  private final Locale defaultLocale = Locale.ENGLISH;
  private ResourceBundle resourceBundle;
  private Locale locale;
  
  @PostConstruct
  private void init() {
    init(defaultLocale);
  }
  
  public void init(Locale locale) {
    Locale languageLocale = Locale.forLanguageTag(locale.getLanguage());
    if (this.supportedLocales.contains(languageLocale)) {
      this.locale = languageLocale;
    } else {
      this.locale = defaultLocale;
    }
    this.resourceBundle = ResourceBundle.getBundle("messages", this.locale);
  }
  
  public String getMessage(String key, Object...params) {
    String message = key;
    try {
      message = MessageFormat.format(this.resourceBundle.getString(key), params);
    } catch (MissingResourceException e) {
      message = String.format("** %s **", key);
    }
    return message;
  }

  /**
   * @return the locale
   */
  public Locale getLocale() {
    return locale;
  }
}

The resource bundle is initially loaded and populated with all translations, all classes can @Inject the I18nHandler and call the getMessage(String key, Object ... params) method to access the translated labels and messages. An example for the usage of the I18nHandler class can be found in the PersonView class:

@Inject
private PersonLayout layout;
@Inject
private I18nHandler i18nHandler;

@Override
public void enter(ViewChangeEvent event) {
  super.setCompositionRoot(this.layout);
  this.layout.getNameField().setCaption(this.i18nHandler.getMessage("person.name"));
  this.layout.getFirstnameField().setCaption(this.i18nHandler.getMessage("person.firstname"));
}

The I18nHandler is injected in lines 3-4 and when entering the view the labels for all UI fields are set in lines 9 and 10.

AddressView in english

When clicking a navigation button (like Address) the corresponding view class is called by the CDINavigator and displays its content in the selected language. As you can see the URI is nice and readable and contains the selected language and the view route. This is declared at the top of the AddressDataView class:

@CDIView(I18nView.LOCALE_PATTERN + "/address")
public class AddressDataView extends CustomComponent implements I18nView

...

public interface I18nView extends View {
  public static final String LOCALE_PATTERN = "${locale}";
}

A custom ViewProvider to handle I18N

Vaadin (the CDINavigator to be precise) does not support the handling of a locale in the uri. So I came up with creating a customĀ I18nViewProvider that can handle this behaviour. The code is an copy of the CdiViewProvider class from the Vaadin CDI addon, but enhanced with the handling of the locale. To use it in an application it has to be declared as a CDI alternative:

@Alternative
public class I18nViewProvider implements ViewProvider

To make our application use this class and not the default CdiViewProvider we need to declare this alternative in the beans.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<beans 
  xmlns="http://java.sun.com/xml/ns/javaee" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
  <alternatives>
    <class>de.mekaso.vaadin.i18n.I18nViewProvider</class>
  </alternatives>
</beans>

This is all we need to make our I18N application work. To switch to another language I added a sub-menu with all supported languages.

Change to another language

After clicking the german language button, the navigator redirects to the selected language and updates the current view:

switched to german

As you can see the URI is updated with the current language, what makes our application perfectly bookmarkable and support the browser history. If you like this lightweight approach to create a I18N Vaadin application, feel free to clone or download this small show-case project and leave a comment on this page.

p.s.: as there is a bug affecting the @PushStateNavigation in version 3.0.0 of the Vaadin CDI addon, I had to include the code (including the bug-fix) of the addon in my project. After this bug is corrected and a new version is released, you will not need the code of this addon in your projects.

By Meik Kaufmann

I am a certified oracle enterprise architect and like to share my knowledge and experience that I gain in the projects I am working on.

Leave a Reply

Your email address will not be published. Required fields are marked *