Translating the interface: I18NProvider
Internationalization (i18n: 18 letters between "i" and "n") means preparing an application to work in several languages. Interface texts are not written in the code directly; they go into translation files, one per language. The code keeps only keys such as nav.posts, and Vaadin finds the right string by the key and the current language. The lookup is done by an I18NProvider, an object that knows which languages exist and where the translations are.
Translation files
Create the folder src/main/resources/vaadin-i18n with two files in it. English:
# src/main/resources/vaadin-i18n/translations_en.properties
app.title=Blog admin
nav.posts=Posts
nav.new-post=New post
language=Language
welcome=Hello, {0}!And Russian:
# src/main/resources/vaadin-i18n/translations_ru.properties
app.title=Админка блога
nav.posts=Статьи
nav.new-post=Новая статья
language=Язык
welcome=Здравствуйте, {0}!These are resource bundles, a standard Java mechanism. The end of the name (_en, _ru) sets the language. Save the files in UTF-8. You do not need to write your own I18NProvider: when Vaadin finds the vaadin-i18n folder, it plugs in its built-in provider, which will offer the en and ru languages.
getTranslation
Every component has a getTranslation(key) method. Let us translate MainLayout:
// src/main/java/com/example/blog/views/MainLayout.java (part of the constructor)
H1 title = new H1(getTranslation("app.title"));
title.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE, LumoUtility.Flex.GROW);
SideNav nav = new SideNav();
nav.addItem(new SideNavItem(getTranslation("nav.posts"), PostListView.class, VaadinIcon.LIST.create()));
nav.addItem(new SideNavItem(getTranslation("nav.new-post"), PostFormView.class, VaadinIcon.PLUS.create()));In an English browser, http://localhost:8080/ shows "Blog admin" in the header and "Posts" and "New post" in the menu. A browser set to Russian shows "Админка блога", "Статьи" and "Новая статья". A browser language that has no file (German, say) gets the first language in the list, English.
Values can be inserted: getTranslation("welcome", "Anna") returns "Hello, Anna!". {0} is the place for the first parameter, {1} for the second.
A language switch
Let us add a drop-down list to the header:
// src/main/java/com/example/blog/views/MainLayout.java (part of the constructor)
Select<Locale> language = new Select<>();
language.setAriaLabel(getTranslation("language"));
language.setItems(Locale.ENGLISH, Locale.of("ru"));
language.setItemLabelGenerator(locale -> locale.getDisplayLanguage(locale));
language.setValue(Locale.of(getLocale().getLanguage()));
language.addValueChangeListener(event -> {
VaadinSession.getCurrent().setLocale(event.getValue());
UI.getCurrent().getPage().reload();
});
addToNavbar(new DrawerToggle(), title, language);A list with "English" and "русский" appears on the right of the header: getDisplayLanguage(locale) writes each language name in that language. Choose "русский" and the page reloads with the whole frame in Russian. The language is stored in the session (VaadinSession), so it stays the same when moving between screens. Imports: java.util.Locale, com.vaadin.flow.component.UI, com.vaadin.flow.component.select.Select, com.vaadin.flow.server.VaadinSession.
The reload is needed because already created components do not change their texts by themselves. Without it, the screen must implement LocaleChangeObserver and call setText(getTranslation(...)) again for each component in its localeChange method.
A custom I18NProvider
If the translations live elsewhere, in another folder or in a database, write the provider yourself. A Spring bean is enough:
// src/main/java/com/example/blog/TranslationProvider.java
package com.example.blog;
import com.vaadin.flow.i18n.I18NProvider;
import java.text.MessageFormat;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import org.springframework.stereotype.Component;
@Component
public class TranslationProvider implements I18NProvider {
@Override
public List<Locale> getProvidedLocales() {
return List.of(Locale.ENGLISH, Locale.of("ru"));
}
@Override
public String getTranslation(String key, Locale locale, Object... params) {
ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
if (!bundle.containsKey(key)) {
return "!" + key;
}
return MessageFormat.format(bundle.getString(key), params);
}
}This provider reads the files src/main/resources/messages_en.properties and messages_ru.properties, and returns !nav.postz for an unknown key, so a typo is visible on the screen. The built-in provider shows !en: nav.postz in that case.
Common mistake
The English file was named without a suffix, translations.properties, and the Russian one translations_ru.properties. There is no error, but the whole interface is in Russian for every user, even in an English browser. The built-in provider counts only files with a suffix as languages, so it sees a single language, ru, and picks it for everyone. A file without a suffix is only a fallback for keys missing from a language file. Give each language its own file: translations_en.properties and translations_ru.properties.