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
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
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:
@vaadin/hilla-react-i18n
dependency in your package.json
.vaadin-i18n
inside src/main/resources
.translation.properties
file. This is your fallback language, oftentimes English.translation_de.properties
for German.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.@index.tsx
file (the one where the React app is configured), put in this line of code await i18n.configure();
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.translate
on these keys. The pages will dynamically update when changing the language signal.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);
}
}
@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)} />
</>
);
}
computed(() => ...)
signals.There are some limitations. For example, Hilla’s Auto components (Grid, Form, CRUD) don’t support i18n, at least I haven’t found it.
Good summary above
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:
Binder
from @vaadin/hilla-lit-form
- somewhat confusing but this is what the React useForm
hook uses under the hoodBinder.interpolateMessageCallback
to a function that translates messages using the Hilla I18N API