Specify UI language programmatically

I am developing an open source application for conferences to display the conference agenda and social media posts on a big screen. The language for the UI views should be configurable and not use the browser locale. Actually I have problems to implement that because I don’t fully understand how the selection of the resource bundle works.

In the directory src/main/resources/vaadin-i18n I placed two property files:

  • translations.properties
  • translations_de.properties

The first one contains all translations in English and should be the fallback, because it has no language code in the name. The second one contains all German translations.

When I start my application, I have the UI in German. That tells me that Vaadin has found the files and was able to read the translations from them (at least from the German one). Now I want to force the UI to be English. This is what I tried so far:

On the view (there is only one view) I added an BeforeEnterObserver to set the locale:

    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        UI.getCurrent().setLocale(Locale.ENGLISH);
    }

The UI was still in German. So I tried to use a VaadinServiceInitListener instead:

    @Override
    public void serviceInit(ServiceInitEvent event) {
        event.getSource().addUIInitListener(uiInitEvent -> {
            uiInitEvent.getUI().setLocale(Locale.ENGLISH);
        });
    }

Same, the UI was still in German. My last approach was to implement an own I18NProvider:

@SpringComponent
public class CustomI18NProvider implements I18NProvider {

    private static final List<Locale> SUPPORTED_LOCALES = List.of(Locale.ENGLISH);

    @Override
    public List<Locale> getProvidedLocales() {
        return SUPPORTED_LOCALES;
    }

    @Override
    public String getTranslation(String key, Locale locale, Object... params) {
        return ResourceBundle.getBundle("vaadin-i18n/translations", Locale.ENGLISH).getString(key);
    }
}

The UI is still in German… :confused:

Can please someone help me to find a working solution and hopefully, to understand why and how this works correctly?

PS: You can find the whole project with all sources on GitHub.

Before I’m typing a novel…

  • I’m rolling my own i18n impl without the default of Vaadin… so take it with a grain of salt
  • adding a _en property file should do the trick… “root fallback” might not work
  • Your custom impl does not work because Vaadin’s i18n files are in a folder - so your name resource bundle name is wrong

Edit: wild guess with German… you are developing on a German localized System… so your JVM is Locale::setDefault(GER) → which (if I remember correctly) is somehow related to how resourcebundle interpret it’s root bundle… but that’s a wild guess… I would have to check my custom impl I’ve written years ago that works :grimacing:

1 Like

Hi, thanks for your quick response. I recognized the wrong folder a minute ago. So I’ll try to add a translations_en.properties file and go without a root fallback. I am used to have english translations in a root fallback for decades with Swing applications. ;-)

1 Like

That’s kind of weird because this is how I’m doing it as well

My language switch sets the language on the session

1 Like

The problem is the DefaultI18NProviderFactory.

public static DefaultI18NProvider create(String locationPattern) {
        try {
            Resource[] translations = getTranslationResources(locationPattern);
            if (translations.length > 0) {
                List<Locale> locales = I18NUtil.collectLocalesFromFileNames((List)Arrays.stream(translations).map(Resource::getFilename).filter(Objects::nonNull).collect(Collectors.toList()));
                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                return new DefaultI18NProvider(locales, classLoader);
            }
        } catch (IOException e) {
            LoggerFactory.getLogger(DefaultI18NProviderFactory.class).error("Unable to create DefaultI18NProvider instance.", e);
        }

        return null;
    }

It discovers the locales based on the prefix, and as there is only de, it serves the translations initially in German.
IMO, this is a wrong implementation, and the DefaultI18NProvider should use the Session locale.

You could overwrite this behavior like this.

public class RegistrationI18NProvider extends DefaultI18NProvider {

    private final static List<Locale> PROVIDED_LOCALES = List.of(Locale.ENGLISH, Locale.GERMAN);

    public RegistrationI18NProvider(List<Locale> PROVIDED_LOCALES) {
        super(PROVIDED_LOCALES);
    }

    public RegistrationI18NProvider(List<Locale> PROVIDED_LOCALES, ClassLoader classLoader) {
        super(PROVIDED_LOCALES, classLoader);
    }

}
1 Like

To me this is a bug and I created a bug report:

1 Like

Thank you very much for your analysis and the bug report, @SimonMartinelli. I solved my problem in my project by implementing an own I18NProvider where I read the locale from the application configuration. In my app, a social media wall, it should use a configurable locale for all screens and not a browser locale, because that is often wrong. The social media wall is usually used on screens at conferences and the browsers of the “Smart TVs” are often not so smart regarding their locale. To solve this, I made it configurable.

@SpringComponent
public final class ApplicationI18NProvider implements I18NProvider {

    private static final @NotNull List<Locale> SUPPORTED_LOCALES = List.of(Locale.ENGLISH, Locale.GERMAN);

    private final transient @NotNull ResourceBundle resourceBundle;

    public ApplicationI18NProvider(@NotNull final AppConfig appConfig) {
        final var locale = switch (appConfig.custom().language()) {
            case "de" -> Locale.GERMAN;
            default -> Locale.ENGLISH;
        };
        resourceBundle = ResourceBundle.getBundle("i18n/translations", locale);
    }

    @Override
    public List<Locale> getProvidedLocales() {
        return SUPPORTED_LOCALES;
    }

    @Override
    public String getTranslation(@NotNull final String key, @Nullable final Locale ignoreLocale, @NotNull final Object... params) {
        final String message =  resourceBundle.getString(key);
        return params.length > 0 ? MessageFormat.format(message, params) : message;
    }
}

But for my other projects I’m looking forward for the bug fix in DefaultI18NProviderFactory.

2 Likes