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:

  1. URL prefix (/en/, /nl/)
  2. User preference (?lang=en)
  3. Domain TLD (.en, .nl)
  4. Browser language header
  5. 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 .meta has translations, not languages
  • Each record has a scalar lang field
  • Each language is a separate record
  • Records are linked through a translations object in the record data

Core's field-level i18n is different:

  • Resource .meta has languages
  • 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 &lt; and &gt;.",
      "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 languages in both site config and resource .meta
  • Always use 2-letter language codes, consistently everywhere
  • Add "i18n": true only 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; use i18n fields for structured content
  • Always include HTML safety instructions in ai_prompts._all for html fields

View this page as Markdown