Practical i18n in Vaadin: Resource Bundles, Locale Handling and UI Language Switching
Modern web applications are rarely used only by users with the same language. Even internal tools often reach international teams or are used in different countries. A multilingual user interface is therefore not a luxury feature, but an important part of the user experience.
The open-source project URL-Shortener also benefits from a clear internationalisation strategy. The application is intended for developers, administrators, and other users who want to manage, analyse, or distribute links. To ensure that the interface remains understandable regardless of the user’s origin, the application supports multiple languages.

The project can be found on GitHub at the URL: https://3g3.eu/url
This article shows how the URL shortener UI has been enhanced with simple, robust internationalisation. The implementation is based entirely on Vaadin Flow’s capabilities and deliberately uses only a few additional helper classes to keep the architecture clear.
The solution has several key objectives. On the one hand, the users’ language is to be automatically recognised based on the locale transmitted by the browser. In addition, it must be possible to manage multiple translations via resource bundles without creating additional administrative overhead in the application code. At the same time, the user should be able to change the language at any time during a running session. Another important goal is to keep the implementation lean and deliberate and to dispense with additional frameworks so that internationalisation can be implemented directly within the existing application without complex infrastructure.
Currently, the application supports three languages:
- English
- German
- Finnish
The choice of these languages is deliberately pragmatic. English serves as the standard language, while German and Finnish depict real-life usage scenarios from the project’s environment.
The focus of this article is not only on the technical implementation but also on how internationalisation can be integrated into a Java web application in a way that keeps the source code clearly structured and maintainable. To do this, we examine both the organisation of translation resources and the handling of locales within the application.
In the following course, it is explained step by step how Vaadin recognises and loads translation resources, how translation keys can be structured sensibly, and how the user’s language is determined based on the browser locale. In addition, it shows how simple language switching can be integrated directly into the user interface, allowing users to switch the desired language during a running session.
This creates an internationalisation solution that is deliberately kept simple but can be easily expanded if other languages are added later.
Internationalisation in Vaadin
Vaadin Flow already provides basic support for internationalisation. Applications can provide translations via so-called resource bundles, which are automatically recognised and loaded by the runtime environment. In this way, user interfaces can be operated in several languages without much additional effort.
The central mechanism relies on translation files stored in a special directory within the project. By default, Vaadin searches for translation resources in the vaadin-i18n folder within the classpath. All files stored there are automatically recognised and are available to the application at runtime.
The translation files follow the familiar structure of Java Resource Bundles. A base file contains the standard translations, while additional files provide language-specific variants. The language is defined by a language suffix in the file name.
A typical setup looks like this, for example:
vaadin-i18n/
translations.properties
translations_de.properties
translations_fi.properties
The file without a language suffix serves as the default resource. In many projects, it contains the English texts. As soon as Vaadin determines an active locale, the framework automatically attempts to load the appropriate translation file. If a language-specific variant exists, it is used. Otherwise, Vaadin falls back on the default resource.
This behaviour corresponds to the classic fallback mechanism of Java Resource Bundles. This means that the application remains functional even if individual translations are not yet complete.
For the user interface, this means that text is no longer defined directly in the source code but is referenced via unique translation keys. These keys are then resolved at runtime via the active locale.
Vaadin provides the getTranslation() method for this purpose, which can be used directly in UI components. This method is used to pass a translation key to the framework, after which Vaadin determines the appropriate text from the loaded resources.
Thus, the resource bundle system forms the basis for all other internationalisation mechanisms within the application. On this basis, additional concepts such as language switching, session-based locale management or structured translation keys can then be developed.
Structuring Translation Resources
After looking at Vaadin’s basic internationalisation support in the previous chapter, an important question quickly arises in practice: How should translation resources be organised within a project to remain maintainable in the long term?
In the URL shortener project, all translations are stored in so-called resource bundles. These are located in the directory src/main/resources/vaadin-i18n/. Vaadin automatically detects this folder and loads the translation files it contains at runtime.
The structure of the files follows the conventions of Java Resource Bundles. A base file contains the standard translations, while additional files provide language-specific variants.
src/main/resources/vaadin-i18n/
translations.properties
translations_de.properties
translations_fi.properties
The translations.properties file serves as the default resource. This project contains the English texts. For other languages, additional files with a language suffix are created. The language is defined by the ISO language code in the file name.
For example, the German version contains the file translations_de.properties, while the Finnish version contains translations_fi.properties.
Within these files, the actual translations are defined via key-value pairs. Each key corresponds to a specific text in the user interface.
main.appTitle=URL Shortener
main.logout=Logout
nav.overview=Overview
nav.create=Create
nav.youtube=Youtube
nav.about=About
The keys themselves follow a simple naming convention. They are often built according to the pattern bereich.element. This allows texts that belong together to be logically grouped.
This structure makes it easier to keep track of a large number of translations. At the same time, it prevents name conflicts between different areas of the application.
Another advantage of this structure is that translations remain completely separate from the application code. The source code only references the keys, while the actual texts are defined in the resource bundles.
This allows translations to be expanded or corrected later without changes to the application code.
This clear separation between code and translation resources provides an important basis for further internationalisation of the application. The next step will therefore look at how these translation keys can be used comfortably within the user interface.
Simplified access to translations with I18nSupport
Once the structure of the translation resources has been determined, another question arises in practical development: How can these translations be used as comfortably as possible within the user interface?
Vaadin basically provides the getTranslation() method, which can be used to resolve translation keys at runtime. This method is available, for example, within UI components and allows you to use a key to determine the appropriate text from the loaded resource bundles.
In practice, however, direct use of this method often results in recurring boilerplate code. Especially in larger user interfaces, translations have to be resolved in many different places. This quickly results in redundant calls and less readable code.
To make this access easier, the URL shortener project uses a small helper interface called I18nSupport. This interface encapsulates access to the translation function and provides a compact method for querying translations within the UI components.
A simplified example of this interface looks like this:
public interface I18nSupport { default String tr(String key, String fallback) { return UI.getCurrent().getTranslation(key); }}
This auxiliary method allows translations to be used much more compactly within the application. Instead of working directly with the Vaadin API, a single method call suffices.
An example from the application’s MainLayout illustrates this:
H1 appTitle = new H1(tr(“main.appTitle”, “URL Shortener”));
This keeps the source code clear and the actual text completely separate from the user interface implementation.
Another advantage of this solution is that access to translations can be adjusted centrally. If the implementation changes later – for example, due to additional fallback mechanisms or logging – this can be done within the helper interface without having to adapt all UI components.
I18nSupport thus forms a small but effective abstraction layer on top of the Vaadin i18n API. It ensures that translations can be used consistently throughout the application with as little boilerplate code as possible.
Determining the user locale
Once translation resources have been structured and convenient access has been provided via I18nSupport, the next step is to ask a central question: What language should the application use when it is first launched?
In web applications, this decision is usually made based on the browser’s language preferences. Modern browsers send a so-called Accept-Language header with every HTTP request that contains a prioritised list of preferred languages. Based on this information, the application can decide which locale to use initially.
Vaadin Flow already takes this information into account during user interface initialisation. When building a new UI, the browser’s locale is automatically detected and set as the current locale. This allows an application to select the appropriate language when the interface is first rendered.
In practice, however, it rarely makes sense to support every language reported by the browser fully. Many applications only offer a limited number of translations. Therefore, the browser-reported locale must first be compared with the application’s actually supported languages.
In the URL shortener project, this matching is implemented via a small helper class called LocaleSelection. This class defines the application’s supported languages and provides methods to map a requested locale to an available variant.
A simplified example looks like this:
public static Locale match(Locale requested) { if (requested == null) { return EN; } String language = requested.getLanguage(); return SUPPORTED.stream() .filter(l -> l.getLanguage().equals(language)) .findFirst() .orElse(EN);}
The method first checks which language the browser requested. It then checks whether this language is among the application’s supported languages. If this is the case, the corresponding locale is adopted. Otherwise, the application’s default language is used.
This strategy ensures that the application remains consistent even if a browser reports a language for which there are no complete translations. At the same time, the implementation remains clear and easily expandable.
The next step is how to keep the chosen language stable during a session. Therefore, in the following chapter, we will look at how a user’s locale is stored and managed within the Vaadin session.
Session-based voice management
After describing in the previous chapter how the initial language of an application can be determined based on the browser locale, another important question arises in practice: How does this decision remain stable throughout the application’s use?
In a typical web application, a user session consists of multiple HTTP requests. During this time, the user expects the interface to be consistently displayed in the same language. If the language were to be determined exclusively by the browser locale for each request, this could lead to unexpected changes – especially if a user manually switches languages within the application.
For this reason, the URL shortener project stores the selected language within the Vaadin session. The session represents a server-side context that is retained across multiple requests and is therefore well-suited to storing user-specific settings, such as the current locale.
Vaadin itself already offers basic support for this. Each VaadinSession has an associated locale that can be set and queried by the application. If this locale is changed, it directly affects the resolution of translation keys in the user interface.
In the project, the session locale is also managed using the LocaleSelection helper class. In addition to the previously shown method for selecting a supported language, this class also provides methods to store or read the locale in the session.
A simplified example for storing the locale looks like this:
public static void setToSession(VaadinSession session, Locale locale) { session.setAttribute(SESSION_KEY, locale);}
When changing the language, the selected locale is saved in the session and set directly on the current UI. This ensures that the new language becomes active immediately and is retained for all subsequent interactions within the same session.
This approach has several advantages. For one, a user’s language remains consistent throughout the session. On the other hand, the language can be changed at any time by a user action without the need for additional persistence mechanisms such as cookies or database entries.
In addition, the implementation remains deliberately simple. The application uses only the existing Vaadin mechanisms and adds only a small helper class for central management of the supported locales.
The next chapter shows how this session-based locale is eventually linked to the user interface and how users can switch languages directly within the application.
Language switching in the user interface
After describing in the previous chapter how to store the selected language in the Vaadin session, the practical question now arises: how can users change this language in the first place? Internationalisation is only useful if users can actively switch the interface language.
In the URL shortener project, language switching has been integrated directly into the MainLayout. This layout forms the central structure of the user interface and includes, among other things, the navigation bar and various status and control elements. This makes it ideal for placing a compact language switch.
Instead of a classic drop-down menu, the application uses a small group of buttons, each representing a language. The buttons are displayed as flags, making the desired language recognisable at a glance.
The user interface shows three options:
- German
- English
- Finnish

Each of these buttons changes the current locale when clicked. Technically, the first step is to check which language has been chosen. This language is then set in both the current UI and the Vaadin session.
The switch’s specific design is deliberately kept small and does not require any additional components or dependencies. Instead of a ComboBox, a horizontal button group is rendered, which acts like a “segmented control”: three round buttons in a slightly separated bar. The bar itself is merely a horizontal layout, with styling handled via Lumo CSS variables, so the look blends harmoniously with the rest of the theme.
When the switch is created, the currently effective locale is first determined. It is crucial that not only the browser locale is considered, but that a value already stored in the session takes precedence. This is exactly what LocaleSelection.resolveAndStore(…) is used for. The result is the “current” locale, which is used to mark the active button.
private Component createLanguageSwitch() { Locale current = LocaleSelection.resolveAndStore( VaadinSession.getCurrent(), UI.getCurrent().getLocale() ); Button de = flagButton(LocaleSelection.DE, current); Button en = flagButton(LocaleSelection.EN, current); Button fi = flagButton(LocaleSelection.FI, current); HorizontalLayout bar = new HorizontalLayout(de, en, fi); bar.setSpacing(false); bar.getStyle() .set("gap", "6px") .set("padding", "2px") .set("border-radius", "999px") .set("background", "var(--lumo-contrast-5pct)"); return bar;}
The actual logic lies in creating a single button. Each button only gets a chip with an emoji flag as content. This seems trivial, but it’s practical: it requires no assets, no additional files, and no build steps. Styling and interaction take place directly on the button.
Three points are important here: First, the buttons are shaped into a compact, round form so that they are perceived as a group that belongs together. Second, the active state is calculated based on the language (getLanguage()), because regional variants such as de-DE or de-AT should not affect the UI. Third, a click triggers the actual locale switch.
private Button flagButton(Locale locale, Locale current) { String flag = flagEmoji(locale); Button b = new Button(new Span(flag)); b.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); b.getStyle() .set("width", "38px") .set("height", "32px") .set("min-width", "38px") .set("border-radius", "999px") .set("padding", "0") .set("line-height", "1") .set("font-size", "18px"); boolean active = locale.getLanguage().equalsIgnoreCase(current.getLanguage()); applyActiveStyle(b, active); b.addClickListener(e -> switchLocale(locale)); Tooltip (optional) b.getElement().setProperty("title", locale.getLanguage().toUpperCase()); return b;}
For the active state, no additional theme variant is deliberately introduced; instead, a minimal style is applied: a slight background tint and an outline ring in the primary colour. Thus, the component remains visually restrained but unambiguous.
private void applyActiveStyle(Button b, boolean active) { if (active) { b.getStyle() .set("background", "var(--lumo-primary-color-10pct)") .set("outline", "2px solid var(--lumo-primary-color-50pct)"); } else { b.getStyle() .remove("background") .remove("outline"); }}
The locale change itself is deliberately implemented “robustly”. First, the selected locale is matched to a supported language. It is then set in both the session and the UI. The final reload() ensures that all views, dialogues, and components resolve their texts from the resource bundles again. This avoids the additional complexity that would otherwise be caused by LocaleChangeObserver implementations across many components.
private void switchLocale(Locale selected) { UI ui = UI.getCurrent(); VaadinSession session = VaadinSession.getCurrent(); Locale effective = LocaleSelection.match(selected); LocaleSelection.setToSession(session, effective); session.setLocale(effective); ui.setLocale(effective); ui.getPage().reload();}
Finally, the display of the flags is encapsulated via a small mapping method. This keeps the display in a central place and can be easily adapted later – for example, if US is to be used instead of EN or if other languages are added.
— <IMG DE> – this is a replacement of the emoji —
private string flagEmoji(Locale locale) { return switch (locale.getLanguage()) { case "de" -> "<IMG DE>"; case "fi" -> "<IMG FI>"; default -> "<IMG EN>"; EN };}
The process consists of several steps. First, the desired language is compared with the application’s supported languages. Then the locale is set in both the session and the current UI. As a result, the new language is immediately available.
The final page reload ensures that all UI components are re-rendered and that all translations are consistently resolved with the new locale. This approach is deliberately kept simple and avoids additional complexity within the component logic.
By integrating language switching into the MainLayout, this feature is available in every view of the application. Users can switch languages at any time without leaving the current page.
This means that the internationalisation of the user interface is fully integrated into the application. Translation resources, locale discovery, session management, and language switching are now intertwined, enabling a multilingual interface with comparatively low implementation effort.
Conclusion and outlook
With internationalisation, the URL shortener has reached a point where good user-friendliness and clean architecture converge. The application supports multiple languages without the source code becoming confusing or requiring additional dependencies. Instead, the existing Vaadin Flow mechanisms are relied on, and only added where they make sense for the project.
The essential building blocks mesh neatly. Translations are stored as resource bundles in the vaadin-i18n directory so that Vaadin recognises them automatically. Clearly structured keys are used to extract texts from the interface and manage them centrally. I18nSupport reduces access to translations to a compact, anywhere-to-use method, keeping UI classes readable.
The locale determination follows a pragmatic principle. By default, the browser’s locale is used, but limited to the languages that are actually supported. This keeps the system predictable and avoids incomplete translation states. At the same time, the selected language is stored on a per-session basis, so that users retain a consistent interface during a session and their selection does not have to be “renegotiated” with each request.
Finally, the whole thing becomes particularly visible in the user interface. The language switch in the MainLayout is deliberately minimalist, but on the UX side, it is much more pleasant than a nested menu. Three flag buttons are enough to quickly change the language, and the active state is immediately recognisable. The technical change to the locale is implemented robustly across the session and UI. The final reload is not a workaround, but a conscious design decision: It ensures that all views, dialogues, and components are consistently rendered in the new language without each component having to implement additional observer logic.
This creates a solid foundation that can be expanded if necessary. An obvious next step would be to add more languages or refine translations without changing the code. For more complex language cases – such as pluralisation, number and date formatting, or language-dependent sentence modules – ICU-based formatting could be added later. It would also be possible to save the language selection not only on a session basis, but also to link it to a user profile permanently.
For the current state of the URL shortener, however, the solution is deliberately kept to the essentials: It is small enough to remain maintainable, but complete enough to deliver real benefits in everyday life. This is precisely the practical value of good internationalisation – it is hardly noticeable in the code but immediately noticeable to users.



















