Skip to content

Internationalization (i18n) Conventions

This document outlines the internationalization (i18n) conventions for the Hatchgrid project. All contributors are expected to follow these guidelines to ensure that the application can be easily translated into different languages.

Internationalization (i18n) is the process of designing and developing an application in a way that it can be easily adapted to different languages and regions without any engineering changes.

The Spring Boot backend is configured for internationalization in application.yml:

spring:
messages:
basename: i18n/messages
encoding: UTF-8
fallback-to-system-locale: false
web:
locale: en
locale-resolver: accept_header

Configuration Details:

  • basename: Points to message bundle files in src/main/resources/i18n/
  • encoding: UTF-8 encoding for proper character support
  • fallback-to-system-locale: Disabled to ensure consistent behavior
  • locale: Default locale set to English (en)
  • locale-resolver: Uses HTTP Accept-Language header for locale detection

The Vue.js frontend uses vue-i18n for internationalization. See Frontend Translation Integration for detailed configuration.

The locale of the user is determined by the Accept-Language header of the HTTP request. The default locale is en (English).

We use resource bundles to store the translated strings. The resource bundles are located in the src/main/resources/i18n directory.

Current Resource Bundles:

  • messages.properties: The default resource bundle (English)
  • messages_es.properties: The Spanish resource bundle

Naming Convention:

Resource bundles follow the pattern messages_{locale}.properties:

  • messages.properties: Default (English)
  • messages_es.properties: Spanish
  • messages_de.properties: German (future)
  • messages_fr.properties: French (future)

Message Categories:

The message bundles are organized into logical categories:

  • Application: General app information (app.*)
  • Common: Shared messages and validations (common.*)
  • Authentication: Login/logout messages (auth.*)
  • User: User management messages (user.*)
  • Newsletter: Newsletter-specific messages (newsletter.*)
  • Workspace: Workspace management (workspace.*)
  • Email: Email-related messages (email.*)
  • Error: Error messages (error.*)
  • Health: Health check messages (health.*)

The message keys should be descriptive and should follow a consistent naming convention.

Good:

  • user.profile.title
  • user.profile.firstName
  • user.profile.lastName

Bad:

  • title
  • firstName
  • lastName

We use the java.text.ChoiceFormat class to handle pluralization.

double[] limits = {0,1,2};
String[] parts = {"There are no files.","There is one file.","There are {2} files."};
ChoiceFormat form = new ChoiceFormat(limits, parts);

We use the java.text.DateFormat class to format dates and times.

DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, locale);
String formattedDate = df.format(new Date());

We use the java.text.NumberFormat class to format numbers.

NumberFormat nf = NumberFormat.getNumberInstance(locale);
String formattedNumber = nf.format(123456.789);

We use the java.text.NumberFormat class to format currencies.

NumberFormat cf = NumberFormat.getCurrencyInstance(locale);
String formattedCurrency = cf.format(123456.789);

Inject MessageSource into your Spring components to access localized messages:

@Service
class UserService(
private val messageSource: MessageSource
) {
fun createUser(user: User, locale: Locale): String {
// Business logic here...
return messageSource.getMessage(
"user.created",
null,
locale
)
}
fun validateEmail(email: String, locale: Locale): String? {
return if (!isValidEmail(email)) {
messageSource.getMessage(
"common.validation.email",
null,
locale
)
} else null
}
}
@RestController
class UserController(
private val userService: UserService,
private val messageSource: MessageSource
) {
@PostMapping("/users")
fun createUser(
@RequestBody user: User,
@RequestHeader("Accept-Language") acceptLanguage: String?
): ResponseEntity<ApiResponse> {
val locale = parseLocale(acceptLanguage) ?: Locale.ENGLISH
val result = userService.createUser(user, locale)
val message = messageSource.getMessage("user.created", null, locale)
return ResponseEntity.ok(ApiResponse(message = message))
}
}

The application automatically resolves the user’s locale from the Accept-Language header. You can also manually parse and use specific locales as needed.

  • All new features should be tested to ensure that they are properly internationalized.
  • The tests should cover all the supported locales.
  • Test message resolution with different locales:
@Test
fun `should return localized message for Spanish locale`() {
val locale = Locale.forLanguageTag("es")
val message = messageSource.getMessage("user.created", null, locale)
assertThat(message).isEqualTo("Usuario creado exitosamente")
}