Internationalization
This guide assumes familiarity with the Spring Framework. If you are new to Spring, we recommend starting with their official guides to get up to speed.
Singularity's core includes a robust, native Internationalization (i18n) system that allows any document
type to support multiple language versions directly within the main entity.
This is achieved through a set of core Kotlin interfaces and the central TranslateService.
Core Components
The i18n system is built upon three primary Kotlin models: Translatable, Translation, and a custom Translation Model (Type C).
The Translatable<C> Interface
Any domain model that supports multiple languages must implement the Translatable<C> interface. It acts as a contract, embedding all language versions directly within the entity document, avoiding external translation tables.
interface Translatable<C> {
// Key: Locale (e.g., Locale.ENGLISH), Value: Content structure (C)
val translations: Map<Locale, C>
}
The Translation Model (C)
The generic type C (Content) is a data class that you define. It holds all the fields of the document that are intended to be translated. All other fields (e.g., id, createdAt, foreign keys) remain in the main document.
Example: TagTranslation | Localization Status |
|---|---|
name: String | Translatable |
description: String | Translatable |
tagKey: String (on the main document) | Non-Translatable (Key identifier) |
The Translation<C> Model
The Translation<C> data class is the standard return type when a specific translation is successfully resolved. It contains both the translated content and the exact Locale that was matched.
data class Translation<C>(
val locale: Locale, // The resolved locale (e.g., Locale.GERMAN)
val translation: C // The translated content (e.g., TagTranslation)
)
The TranslateKey Model
The TranslateKey is a simple wrapper for a String key used specifically when translating static strings, typically from standard Java resource bundles (.properties files).
data class TranslateKey(val key: String)
The TranslateService
The TranslateService is the central service responsible for resolving the best possible translation for a given Translatable entity. It uses a built-in fallback strategy to maximize the chance of returning valid content.
Resolving Document Translations
The service uses the translate method to resolve content based on a preferred client Locale.
| Method | Description | Kotlin Signature (Simplified) |
|---|---|---|
translate | Resolves the best-matching translation for a document based on the requested locale and the system's fallback rules. | fun <C> translate(translatable: Translatable<C>, locale: Locale?): Result<Translation<C>, TranslateException> |
Fallback Strategy
The TranslateService attempts to find a match in the following order:
- Exact Match: The translation for the exact
Localerequested (e.g.,fr_CA). - Language Match: The translation for the language component only (e.g.,
friffr_CAfailed). - Default Locale: The translation corresponding to the system's default locale (configured in
AppProperties). - First Available: The first translation available in the map, regardless of locale.
If all steps fail and the translations map is empty, an error is returned.
Resolving Static Resource Keys
The service also provides a method to translate static strings (like error messages) from resource bundles.
| Method | Description | Kotlin Signature (Simplified) |
|---|---|---|
translateResourceKey | Translates a static key from a resource bundle using the provided locale. | suspend fun translateResourceKey(key: TranslateKey, locale: Locale?, resourceBundle: String): Result<String, TranslateException> |
The TranslatableCrudService
The TranslatableCrudService<C, T> extends the standard database CrudService specifically for Translatable entities. Its primary function is to enable Localized Sorting during paginated queries.
Localized Sorting
When a client requests a sorted list of documents, the TranslatableCrudService automatically adjusts the MongoDB sort query to target the translations for the requested Locale.
| Method | Description | Kotlin Signature (Simplified) |
|---|---|---|
findAllPaginated | Retrieves a paginated list of entities with support for localized sorting. | suspend fun findAllPaginated(pageable: Pageable, criteria: Criteria?, locale: Locale?): Result<Page<T>, FindAllDocumentsPaginatedException> |
How Localized Sorting Works
- A client requests a sort on a translatable field, e.g.,
sort=name:ASC. - They pass a
localeheader, e.g.,Accept-Language: de-DE. - The service intercepts the sort request and transforms the sort property from
nametotranslations.de_DE.name. - This ensures that the database sorts the documents based on the German translation of the name, rather than the raw field name which doesn't exist.
The service automatically falls back to sorting by the field in the application's default locale if the requested locale is not found in the translation map.
Here is a usage example demonstrating how to integrate the Internationalization (i18n) components into a typical service layer for a Tag domain model.
Since you already have Article models, here is an alternative usage example using a Product domain model. This is a common scenario in e-commerce or inventory systems where product names and descriptions need to be localized.
Example
This example demonstrates how to define a translatable Product entity, store its multiple language versions,
and retrieve the best-matching translation using the TranslateService.
1. Define the Translation Model (C)
First, define the ProductTranslation data class. This class holds the fields of the ProductDocument that are meant to be translated.
data class ProductTranslation(
val name: String,
val description: String,
val features: List<String>,
)
2. Implement the Translatable<C> Interface
Next, ensure your main document, ProductDocument, implements the Translatable<ProductTranslation> interface and stores a map of translations.
// Example Product Document structure
data class ProductDocument(
val sku: String, // Non-translatable Stock Keeping Unit
val price: BigDecimal, // Non-translatable financial data
val availableInventory: Int, // Non-translatable inventory count
// The required field from the Translatable interface
override val translations: Map<Locale, ProductTranslation>
) : Translatable<ProductTranslation>
// Example of how a document might be stored in the database:
val productDocument = ProductDocument(
sku = "SMART_WATCH_V1",
price = BigDecimal("199.99"),
availableInventory = 500,
translations = mapOf(
Locale.US to ProductTranslation(
name = "SmartWatch Pro V1",
description = "The ultimate fitness companion with a 14-day battery life.",
features = listOf("GPS Tracking", "Heart Rate Monitor")
),
Locale.JAPANESE to ProductTranslation(
name = "スマートウォッチ Pro V1",
description = "14日間バッテリー駆動の究極のフィットネスコンパニオン。",
features = listOf("GPSトラッキング", "心拍数モニター")
),
)
)
3. Implement the Translation Logic in a Service
The ProductService would use the injected TranslateService to resolve the correct version of the document based on the client's requested locale.
@Service
class ProductService(
private val translateService: TranslateService,
private val productRepository: ProductRepository // Assume this handles fetching the ProductDocument
) {
/**
* Retrieves a product by its SKU and resolves the best matching translation.
*/
suspend fun getLocalizedProduct(sku: String, locale: Locale?): Result<Translation<ProductTranslation>, TranslateException> {
// 1. Fetch the multilingual document from the repository
val productDocument = productRepository.findBySku(sku)
.getOrThrow { /* Handle database/not found exception */ }
// 2. Resolve the best matching translation using the TranslateService
return translateService.translate(productDocument, locale)
}
}
4. The Result
If the service is called with: getLocalizedProduct("SMART_WATCH_V1", Locale.JAPANESE)
The result will be a Translation<ProductTranslation> object:
Translation(
locale = Locale.JAPANESE, // The specific locale that was matched
translation = ProductTranslation(
name = "スマートウォッチ Pro V1",
description = "14日間バッテリー駆動の究極のフィットネスコンパニオン。",
features = listOf("GPSトラッキング", "心拍数モニター")
)
)