# Multilingual content

Language routes, translated fields, static text and translation workflows.

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`:
```json
{
  "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:

```json
"languages": ["en", "nl"]
```

Mark translatable fields with `i18n: true`:

```json
"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`:
```po
msgid "Search"
msgstr "Search"

msgid "Read_more"
msgstr "Read more"
```

`ext/data/.i18n/text.nl.po`:
```po
msgid "Search"
msgstr "Zoeken"

msgid "Read_more"
msgstr "Lees meer"
```

Build the merged base file after changes:
```bash
./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`:
```html
<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:
```json
"ai_prompts": {
  "_all": [...],   → shared context for all languages
  "en": [...],     → English-specific instructions
  "nl": [...]      → Dutch-specific instructions
}
```

**For text fields:**
```json
"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`:

```json
"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

---
