Vaadin + Localization/Internationalization (l10n/i18n)
The I18nProvider
class handles the duty of performing localization in Vaadin apps.
You can invoke it either directly via VaadinService.getCurrent().getInstantiator().getI18NProvider().getTranslation("key", UI.getCurrent().getLocale());
or via Component.getTranslation()
. The latter is much shorter, but you may not have a component
always around to perform the translation. Therefore, I recommend to create a simple class
public class Tr {
public static String tr(String key, Object... params) {
return VaadinService.getCurrent().getInstantiator().getI18NProvider().getTranslation(key, UI.getCurrent().getLocale(), params);
}
}
Then you can static-import the tr()
function everywhere and use it from anywhere:
import static com.example.Tr.tr;
public class MyForm extends FormLayout {
public MyForm() {
TextField nameField = new TextField(tr("person.name"));
// ...
}
}
Default I18nProvider?
There is no default I18nProvider
implementation. You can check out
Vaadin documentation in Localization
on how to create a simple ResourceBundle
-backed I18nProvider, or simply read on.
For non-Spring projects, Vaadin recommends to register your I18nProvider via a system property or web init param.
It looks funny but it works; I recommend this approach of having ApplicationServlet
as shown in the Vaadin tutorial above.
Alternatively you can define your own Instantiator,
override Instantiator.getI18nProvider()
and provide a singleton instance of your I18nProvider.
With Spring, you can annotate your I18nProvider
-implementing class with @Service
and Vaadin-Spring integration should pick it up
automatically.
Resource Bundle
The best way to localize in Java is to use the mechanism of Resource Bundles. It is widely supported by IDEs, used by default in Java world, has no significant shortcomings, and therefore I recommended this way.
In order to use Resource Bundles with Vaadin, you’ll need to define the following i18n provider:
public class TranslationProvider implements I18NProvider {
private static final String BUNDLE_PREFIX = "translate";
private static final List<Locale> locales = Collections
.unmodifiableList(Arrays.asList(new Locale("en", "GB"), new Locale("fi", "FI")));
@Override
public List<Locale> getProvidedLocales() {
return locales;
}
@Override
public String getTranslation(String key, Locale locale, Object... params) {
Objects.requireNonNull(key);
Objects.requireNonNull(locale);
final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PREFIX, locale);
String value;
try {
value = bundle.getString(key);
} catch (final MissingResourceException e) {
log.warn("Missing resource", e);
return "!" + locale.getLanguage() + ": " + key;
}
if (params.length > 0) {
value = MessageFormat.format(value, params);
}
return value;
}
private static final Logger log = LoggerFactory.getLogger(TranslationProvider.class);
}
In the getProvidedLocales()
you should return all locales for which you define resource bundles.
Vaadin will then match the language list coming from the browser in the http request to this list,
it will pick a match and will set it to the VaadinSession and all UIs. If no match
is found, first locale is selected. Therefore:
- Pick one locale that will be the primary one, usually it’s English. This will be a fallback locale: if a match can not be found, the default is returned.
- Return the primary locale as first item in the
getProvidedLocales()
list - Create the default resource bundle (the bundle with no language/country selectors) and populate it with strings for this language.
In this example there are two locales: English and Finnish. If the browser requests Finnish, then Finnish resource bundle will be selected, otherwise English resource bundle will be selected.
You should then create two resource bundles in src/main/resources/
:
translate.properties
for English locale, with the contentHi=Hello, {0}!
translate_fi.properties
for Finnish locale, with the contentHi=Moi, {0}!
Note the {0}
string - that’s what MessageFormat
class uses to format String with parameters. See
the MessageFormat
class javadoc for more info on how to use additional formatting.
Now you are able to call tr("Hi", "Martin")
from anywhere in your code, to obtain the translation e.g. Hello, Martin!
.
Changing Language On-The-Fly
Say that you want to have a ComboBox language selector with the languages placed somewhere within the navigation bar of
your application. If the user changed the language,
then you’d like to immediately change all texts in all Vaadin components to the new language.
You may be tempted to have all
components implement the LocaleChangeObserver
interface to change their labels,
then simply call VaadinSession.getCurrent().setLocale(locale)
.
Don’t.
There is no good solution for this. Let’s explore two solutions.
Say you want to have a Button which changes its text automatically based on locale change.
You would create a class I18nButton
as follows:
public class I18nButton extends Button implements LocaleChangeObserver {
private final String i18nkey;
public I18nButton(String i18nkey) {
this(i18nkey, null);
}
public I18nButton(String i18nkey, Component icon) {
this.i18nkey = Objects.requireNonNull(i18nkey);
setIcon(icon);
updateText();
}
private void updateText() {
setText(getTranslation(i18nkey));
}
@Override
public void localeChange(LocaleChangeEvent event) {
updateText();
}
}
There are the following shortcomings:
- What if someone calls
setText()
on this button - should it interpret the String literally as untranslated text (and overwriting it on next locale change), or should it set the String as key toi18nkey
, thus changing the semantics ofsetText()
? Neither is perfect. - What if we need parameters? We can modify
Tr
to carry key+parameters and rewrite I18nButton to useTr
instead ofString i18nkey
.
public class Tr implements Serializable {
private final String key;
private final Object[] params;
public Tr(String key, Object... params) {
this.key = key;
this.params = params;
}
public Tr(String key) { this(key, (Object[]) null); }
public String get() { return tr(key, params); }
public static String tr(String key, Object... params) {
return VaadinService.getCurrent().getInstantiator().getI18NProvider().getTranslation(key, UI.getCurrent().getLocale(), params);
}
}
The usage would then be new I18nButton(new Tr("Hi", "Martin"))
. That could work.
What if we also need to localize placeholders for fields, LabelProviders for ComboBoxes etc?
We would have to introduce I18nTextField
which remembers Tr
not only for label, but also
for placeholder.
Furthermore, shall we
also define I18nTextField
, I18nComboBox
and I18n*
class for every Vaadin component and use those
instead of vanilla Vaadin component? That is certainly a lot of work, not to mention that
this solution feels weird to any new developer joining the project -
you have to remember to always use a non-standard I18nTextField
component instead of the
original counterpart TextField
. Any extending classes will need to have the I18n*
counterparts too:
say you have MyTextField
which extends TextField
then you need to define I18nMyTextField
and repeat the i18n
machinery there.
You may be tempted to solve the repetition problem by introducing a set of mixin interfaces,
say HasI18nText
which would extend LocaleChangeObserver
and would be implemented by all components, but that solution is just horrible:
- It’s very complex to understand;
- You’ll need to implement get/set for
tr
in every component - Mixin interface won’t add constructor parameters
- You’ll still need to define the
I18n*
class hierarchy anyway
Alternative: Traverse Components Yourself
Alternatively you could remember the i18nkey for component’s placeholder/label/texts in component data (ComponentUtil.getData()
),
then have one locale observer which would traverse all components on locale change and would apply the new placeholders/labels/texts
to the components. This certainly removes the need of having a parallel set of I18n*
classes.
Technical problem: UI doesn’t allow you to add a locale change listener to it, but that could be solved
by having a custom implementation of the UI, or making the root layout component LocaleChangeObserver
,
or the LanguageComboBox
can do it itself.
How would such an API look like? You could have a function, say Tr.setLabel(textField, "key.name")
which
would handle the abovementioned stuff. The problem is that IDE would not auto-complete this function in a TextField
since Java lacks the extension function language feature. You would also have to remember to not to use textField.setLabel()
itself but use some funny API Tr.setLabel()
, which is just weird and hard for any newcomers.
The Best Solution
The best & simplest solution is to reload the page on language change, then re-create all components and
set their labels via textField.setLabel(tr("key.name"))
. You only need to remember one rule: to always
call the tr()
function, and that’s really easy to remember.
What if you’re using @PreserveOnRefresh
or cached routes? You can
make LanguageComboBox
clear the view cache on language change then reload the page, which is perfectly acceptable -
the language doesn’t change often after all. In case of @PreserveOnRefresh
there’s no simple solution -
even if you navigate to other view, the root layout may not be changed. You may need a combination
of navigation + page reload; or you can clear the session and log out the user.
The simplest solution is to have the user to pick the language on login, then disallow any language change. Again - the language is frequently only changed once on login to the user’s native language, then not changed afterwards.