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 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.
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.
After clicking the german language button, the navigator redirects to the selected language and updates the current view:
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.