Notes on Dynamic Translation
中文Dynamic translation is useful when the same API response needs to keep one stable shape while presenting text in the language requested by the client.
The naive solution is to add separate fields everywhere: nameCn, nameEn, descriptionCn, descriptionEn, and then let every frontend decide what to display. That works for a small object, but it ages badly. The response becomes noisy, and every client has to repeat the same language-selection logic.
The design I prefer is different: store translations as structured metadata, then choose the visible value at serialization time.
1. Keep the Domain Field Stable
The field the client reads should remain ordinary:
{ |
The client should not need to know whether name came from the original field, a Chinese translation, an English translation, or a fallback.
Internally, the object can carry a language source:
{ |
The public response can still hide language and only render the selected values.
2. Use Request Context for Selection
Language selection should usually come from request context, not from every service method.
The basic rule is:
requestedLanguage = request.headers["Accept-Language"] |
Then serialization can decide:
if requestedLanguage is blank: |
This lets the business method return the same object no matter who calls it. The language decision is a presentation concern, so it belongs close to output formatting.
3. Mark Source and Rendered Fields Explicitly
Two annotations are enough for the shape I like:
class ResultDTO { |
The rendered field says, “when language output is requested, look up this key.” The source field says, “this object contains the translations.”
That makes the serializer generic. It does not need to know about warehouses, contracts, categories, articles, or any other domain model.
4. Serialization Is the Right Hook
A contextual serializer can inspect the field annotation, find the parent object, read the language source, and write the translated value.
Pseudocode:
function serialize(value, fieldAnnotation, generator, provider): |
This is dynamic without making every endpoint dynamic. The same DTO can be returned from many methods, and the language behavior follows the response object.
5. Generate Translations When Text Enters the System
There are two places to translate:
- when the response is read
- when the text is created or updated
For user-facing business data, I prefer translating when text enters the system. That means a create or update flow can build a language map once:
function buildLanguageInfo(text, sourceLanguage = auto): |
This keeps reads fast and predictable. It also avoids calling an external translation service during every list or detail request.
The tradeoff is that updated source text needs to refresh its language map. That is a reasonable tradeoff for most admin or content-style data.
6. Fallbacks Matter More Than Perfect Translation
A translation system should fail soft.
If translation generation fails, store the original text as fallback. If the requested language value is blank, return the original field. If the request does not include a language header, do nothing.
The fallback chain can be simple:
function chooseText(original, languageInfo, requestedLanguage): |
This is not glamorous, but it prevents i18n from becoming a source of broken responses.
7. Allow Fixed-Language Fields When Needed
Sometimes a field should always return one language regardless of request header. For example, an external integration may require an English address, or a document export may need a fixed language.
That should be explicit:
|
The key idea is that fixed language should be local and visible. It should not be hidden in a service method where future callers will miss it.
Current Rule
For dynamic translation, my rule is:
store all language variants once, choose the visible value late |
This keeps API shapes stable, keeps clients simple, and keeps language behavior close to serialization instead of spreading it through business code.