Skip to content

Internationalization (i18n)

Mateu passes every user-visible string — field labels, page titles, validation messages, button texts, alerts — through a bean implementing the Translator interface. This centralizes translation in a single extension point.


public interface Translator {
String translate(String text, HttpRequest httpRequest);
}
ParameterDescription
textThe original string (as it appears in code or in annotations)
httpRequestRequest context — lets you read the Accept-Language header, session cookies, or anything else needed to resolve the locale

The returned value is the string sent to the frontend.


The core module ships an implementation based on standard Java i18n (ResourceBundle). To use it, just add a messages file to the classpath:

src/main/resources/
messages.properties ← default locale
messages_es.properties ← Spanish
messages_fr.properties ← French
...

Example messages_es.properties:

Name=Nombre
Save=Guardar
Order\ #=Número de pedido
Field\ is\ required=El campo es obligatorio

The locale is resolved from the Accept-Language header of the HTTP request.


To replace the default implementation, register a bean implementing Translator with higher priority. In Spring Boot:

@Component
@Primary
public class DatabaseTranslator implements Translator {
final TranslationRepository repo;
@Override
public String translate(String text, HttpRequest httpRequest) {
String locale = httpRequest.getHeaderValue("Accept-Language");
return repo.findTranslation(text, locale)
.orElse(text); // fall back to the original text
}
}

In Quarkus or Micronaut use the equivalent annotation (@Alternative + @Priority, @Primary, etc.).


ScenarioSuggested implementation
Translations in .properties filesDefault implementation from core
Translations in a database (hot-editable)@Primary bean querying the DB
Per-tenant / white-label labelsRead the tenant from the request and resolve the matching bundle
External translation serviceAPI call with a local cache to keep latency low
Development mode without translationsReturn text directly (pass-through)

Your Translator implementation is invoked for every string of every response. If resolution involves DB or network access, add a cache layer keyed by (text, locale):

@Component
@Primary
public class CachedDatabaseTranslator implements Translator {
final TranslationRepository repo;
final Cache<String, String> cache = Caffeine.newBuilder().maximumSize(10_000).build();
@Override
public String translate(String text, HttpRequest httpRequest) {
String locale = resolveLocale(httpRequest);
return cache.get(text + "|" + locale,
k -> repo.findTranslation(text, locale).orElse(text));
}
}

  • Security — authentication and role-based authorization
  • Key interfaces — full reference of public interfaces