Hilla i18n

Hello, I’m trying to add translations to my app, but I can’t find any documentation on how to do it.

I found the following issues on Github:
https://github.com/vaadin/hilla/issues/2101
https://github.com/vaadin/docs/issues/1230

You have to wait a little bit longer for an official solution.

Unfortunately, i18n is currently an experimental feature.

However, through digging the internet, I was able to come up with a semi decent solution that should be good enough until official support is given!

To enable the experimental feature, create the file vaadin-featureflags.properties inside src/main/resources. Inside this file, set com.vaadin.experimental.hillaI18n=true.

Now that the feature is enabled, follow these instructions:

  • First, import the @vaadin/hilla-react-i18n dependency in your package.json.
  • Then, create the folder vaadin-i18n inside src/main/resources.
  • Create a root translation.properties file. This is your fallback language, oftentimes English.
  • Then, create language specific translation files, e.g. translation_de.properties for German.
  • Create key-value pairs inside these files, e.g. greeting=Hello in your root translation file and greeting=Servus in your German translation file. The values of the key-value pairs can even have parameters, such as greetingWithName=Hello, {name}. {name}'s value can then be provided.
  • In your @index.tsx file (the one where the React app is configured), put in this line of code await i18n.configure();
  • Now, inside your TSX files, you can call the translate() function. translate('greeting') will either output Hello or Servus, depending on the value you have configured in await i18n.configure() or depending on the value of i18n.language, which is a global signal provided by Hilla.
  • To translate page/menu titles, set the corresponding value of ViewConfig to a key you have defined in your translation files. When rendering the menu/pages, use translate on these keys. The pages will dynamically update when changing the language signal.
  • Now comes the tricky part: Providing a translate function for the Java backend, e.g. when you want to send back an error based on the clients language (which is sent in a cookie). You can use following utility class that I have created. It is not perfect, but this was the only way I could make it work. Maybe you can come up with something better:
public class TranslationUtil {

  public static String translate(String key, Object... params) {
    var i18nProvider = VaadinService.getCurrent().getInstantiator().getI18NProvider();
    return i18nProvider.getTranslation(key, getCurrentLocale(), params);
  }

  /**
   * Unfortunately, to get the current locale sent from the client inside the cookie, one needs to
   * manually parse the cookie. Using I18nProvider of Vaadin requires specifying each locale, which
   * is not practical. Using VaadinService.getCurrentRequest().getLocale() returns the server's
   * locale, not the one inside the current request's cookie. This can be bypassed by implementing a
   * custom I18nProvider, which, as already mentioned, is not practical.
   */
  private static Locale getCurrentLocale() {
    var localeString = "en";
    var cookie =
        Arrays.stream(VaadinService.getCurrentRequest().getCookies())
            .filter(c -> c != null && Objects.equals("vaadinLanguageSettings", c.getName()))
            .findFirst()
            .orElse(null);

    if (cookie == null) {
      return Locale.of(localeString);
    }

    var jp = JsonParserFactory.getJsonParser();
    try {
      var json = jp.parseMap(UriUtils.decode(cookie.getValue(), "UTF-8"));
      localeString = (String) json.get("language");
    } catch (Exception ignored) {
    }

    return Locale.of(localeString);
  }
}
  • To have i18n conform error messages in the bean validation framework (e.g. in DTOs or entities), you can do the following:
@Data
public class EmployeeDetailDto {

  @NotBlank(message = "validation.required")
  private String firstName;
}

In your translation files, define a key-value pair validation.required=This field is required.
Now, you will provide this key to your frontend so that it will use the right value in forms. A neat little trick to do this is as follows, (e.g. in your @index.tsx file):

BinderRoot.interpolateMessageCallback = (message, validator, binderNode) => {
  return translate(message);
};

Now, if a user types in an incorrect value for a form field, an error message will be displayed. Example:

export default function HomeView() {
  const { model, field } = useForm(EmployeeDetailDtoModel);

  return (
    <>
      <TextField label={translate('greeting')} {...field(model.firstName)} />
    </>
  );
}
  • Many Vaadin/Hilla components support i18n. For example, to i18n the datepicker, you can take inspiration from this code. Similarly, the LoginOverlay component has also some i18n properties you can configure. Simply create the key-value pairs in your translation files and create the corresponding computed(() => ...) signals.

Limitations

There are some limitations. For example, Hilla’s Auto components (Grid, Form, CRUD) don’t support i18n, at least I haven’t found it.

2 Likes

Good summary above :+1:

The Hilla codebase also includes an integration test project with a basic setup: hilla/packages/java/tests/spring/react-i18n at main · vaadin/hilla · GitHub

One thing to check is using await i18n.configure();, which would be a top-level await. I’m not sure if that’s supported yet in production builds, I remember there was some issue with esbuild. The test project above uses a different approach where it uses the promise returned by configure and chains mounting the React app, which has the same effect without using a top-level await.

Do you by any chance know how to work with parameters when using DTO annotations?

Suppose I have a DTO with a field that’s annotated with @Size(min=1, max=255, message="validation.size")

With a key-value pair in the translation resource bundle as follows:
validation.size=Must be between {min} and {max}

Since only the string “validation.size” is sent to the frontend and the frontend has to display the value of the key-value pair by itself, it seems quite inefficient to pass in “min” and “max” manually as opposed to having it automatically (just like Hilla already does if i18n is disabled).

It’s not mentioned in the React docs unfortunately, but there is an example in the Lit docs: https://vaadin.com/docs/latest/hilla/lit/guides/forms/binder-validation#message-interpolation-example

So basically:

  • Import Binder from @vaadin/hilla-lit-form - somewhat confusing but this is what the React useForm hook uses under the hood
  • Set Binder.interpolateMessageCallback to a function that translates messages using the Hilla I18N API
  • The function receives the respective validator instance, so you should be able to get the min/max parameters from that