Concepts
Field-level i18n
Languages as part of the system: language routes, translated fields, static texts and templates per language.
Nimbly supports multi-language systems out of the box. Languages are handled at three levels: site configuration, routing, and data.
Step 1: Define site languages
ext/data/.config/site:
{
"languages": ["en", "nl"]
}
Rules:
- Required for all multi-language setups
- Only 2-letter language codes
- First language is the fallback
Step 2: Set up language routing
Each language gets its own URL prefix: /en/, /nl/
Redirect root to the detected language in ext/uri/index.tpl:
[#redirect [#detect-language#]#]
Language detection order:
- URL prefix (
/en/,/nl/) - User preference (
?lang=en) - Domain TLD (
.en,.nl) - Browser language header
- Fallback - first language in site config
Once a user is inside a language scope, it persists automatically across navigation.
Step 3: Configure resources for translation
Core uses field-level i18n. Add languages to every resource .meta
that stores translated values in the same record:
"languages": ["en", "nl"]
Mark translatable fields with i18n: true:
"title": {
"type": "text",
"required": true,
"i18n": true
},
"intro": {
"type": "html",
"buttons": "bold,italic",
"admin_col": false,
"i18n": true
}
Fields without i18n: true are shared across all languages. Most technical
fields should stay shared, but i18n: true is valid for any field whose value
intentionally differs per language, such as a language-specific image,
visibility flag, sort order, or date.
Old record-level i18n vs new field-level i18n
Older Nimbly projects may still use record-level i18n:
- Resource
.metahastranslations, notlanguages - Each record has a scalar
langfield - Each language is a separate record
- Records are linked through a
translationsobject in the record data
Core's field-level i18n is different:
- Resource
.metahaslanguages - Individual fields opt in with
"i18n": true - One record stores all language values for that field
- Translated field values are objects such as
{ "nl": "Titel", "en": "Title" }
Both models can use /nl/ and /en/ routes. The difference is the data shape
and admin behavior, not the public URL shape.
Do not add .meta.languages to an old record-level resource just because the
site has multiple languages. That opts the admin into the 1.1 field-level data
shape and can corrupt editing workflows unless the existing records are being
intentionally migrated to {nl,en,...} field objects.
Step 4: Add static text translations
Use [#text Key#] for UI labels in templates. Keys use underscores for spaces.
Create translation files per language:
ext/data/.i18n/text.en.po:
msgid "Search"
msgstr "Search"
msgid "Read_more"
msgstr "Read more"
ext/data/.i18n/text.nl.po:
msgid "Search"
msgstr "Zoeken"
msgid "Read_more"
msgstr "Lees meer"
Build the merged base file after changes:
./nimbly build
Step 5: Templates per language
Languages are not just translations of the same site - each language can have its own templates, routing, and structure. They are effectively separate sites under /en/, /nl/, etc.
ext/uri/en/index.tpl → /en/
ext/uri/en/about/index.tpl → /en/about/
ext/uri/nl/index.tpl → /nl/
ext/uri/nl/about/index.tpl → /nl/about/
When structure is shared across languages, extract it into a reusable template:
ext/uri/nl/about/main.tpl:
[#about-main#]
ext/tpl/about-main/index.tpl:
<h1>[#text About#]</h1>
<p>[#text About_intro#]</p>
AI-assisted translation
Fields can define translation instructions using ai_prompts. The admin uses these to guide AI translation.
Structure:
"ai_prompts": {
"_all": [...], → shared context for all languages
"en": [...], → English-specific instructions
"nl": [...] → Dutch-specific instructions
}
For text fields:
"title": {
"type": "text",
"i18n": true,
"ai_prompts": {
"_all": ["Your style is accurate and professional."],
"en": ["You translate to English."],
"nl": ["You translate to Dutch."]
}
}
For HTML fields - strict rules apply:
HTML fields require explicit instructions to preserve structure. Always include all of the following in _all:
"main_text": {
"type": "html",
"i18n": true,
"ai_prompts": {
"_all": [
"The input and output are in HTML, properly escaped for safe use in an HTML editor.",
"You preserve existing HTML structures including images, lists, links and other tags as much as possible.",
"You do not introduce raw < or > characters in your output, but use < and >.",
"Your style is accurate and professional."
],
"en": ["You translate to English."],
"nl": ["You translate to Dutch."]
}
}
Rules for HTML translation:
- Always include the HTML safety instructions in
_all - Always preserve images, links, lists, and formatting tags
- Never allow the model to simplify or restructure the HTML
i18n rules for AI agents
- Always define
languagesin both site config and resource.meta - Always use 2-letter language codes, consistently everywhere
- Add
"i18n": trueonly when the field value should intentionally differ per language - Never hand-translate slug fields or UUIDs. I18n slug fields are valid when they are generated from i18n source fields.
- Keep resource structure identical across languages
- Use
[#text#]for UI labels; usei18nfields for structured content - Always include HTML safety instructions in
ai_prompts._allfor html fields