Skip to main content
Version: 0.3.36

Translations & Localization

RaisinDB stores translations as a per-locale overlay on top of a base node. The base node holds the source-language content; each locale contributes an overlay containing only the translated fields. When you read with a locale, the server merges the overlay over the base, and anything not translated falls back to the base value.

There is one node per piece of content, not one per language, so non-translated fields stay in sync automatically and translations can be added or changed independently of the source.

Repository language settings​

Each repository has a default language, a list of supported languages, and optional fallback chains. Read and change them through the translation config endpoint:

curl -s http://localhost:8090/api/repositories/myrepo/translation-config \
-H "Authorization: Bearer $TOKEN"
{"default_language":"en","supported_languages":["en"],"locale_fallback_chains":{}}
curl -s -X PATCH http://localhost:8090/api/repositories/myrepo/translation-config \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"supported_languages":["en","fr","fr-CA","de"],"locale_fallback_chains":{"fr-CA":["fr","en"]}}'

The default language is always kept in supported_languages. Every locale named in a fallback chain must already be in supported_languages, so add the languages first if you send the two settings in separate requests.

Choose the default language at creation​

The default language is set when the repository is created, and the base node holds content in that language. A German site in a repository created with the server default en stores its German text as English base content, and a de overlay then sits on top of it. Pass the language when you create the repository:

raisindb repo create website --default-language de --languages de,fr,en
raisindb repo languages website # default + supported languages
raisindb repo languages website --add it # supported languages can be added later

Over HTTP, send default_language and supported_languages in the POST /api/repositories body.

To switch an existing repository to another default language, send default_language to the translation config endpoint. The switch is refused with 409 DEFAULT_LANGUAGE_CONFLICT while translation overlays in the new language exist, because they would collide with the base content; delete those translations first. A successful switch queues a full-text rebuild of every branch. Full-text search indexes base content under the default language, so after recreating a repository or changing its languages, run a full-text rebuild if one was not queued for you.

A locale with a region always falls back to its language on its own (fr-CA to fr) even without a configured chain. Configured chains extend that with any further steps you want.

Marking fields translatable​

Set is_translatable: true on a NodeType property, or use the TRANSLATABLE modifier in SQL:

CREATE NODETYPE 'site:Page' (
title String REQUIRED TRANSLATABLE,
body String TRANSLATABLE,
slug String
);

On element type fields the flag is translatable: true:

# elementtypes/article.yaml
fields:
- $type: TextField
name: title
translatable: true
- $type: RichTextField
name: body
translatable: true
- $type: TextField
name: slug

The flag drives the staleness report (below) and editor tooling, and it decides which repeatable items need a uuid. The translate command itself accepts any pointer, so keep translation writes to the fields you have marked.

Writing translations​

REST: raisin:cmd/translate​

Post a map of JSON pointers to values for one locale on the node's path:

curl -s -X POST "http://localhost:8090/api/repository/myrepo/main/head/site/home/raisin:cmd/translate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"locale":"fr","translations":{"/title":"Bienvenue"},"message":"French title"}'
{"node_id":"suQMLzS9o8nMVn3AQ53_F","locale":"fr","revision":"1788720173765-0","timestamp":"2026-09-06T18:42:53.765757+00:00"}

Pointers address the node's properties: /title is the title property. The optional message and actor are recorded with the revision.

Read the node back with the lang query parameter. Fields without a translation keep their base value:

curl -s "http://localhost:8090/api/repository/myrepo/main/head/site/home?lang=fr" \
-H "Authorization: Bearer $TOKEN"
{"properties":{"title":"Bienvenue","body":"Hello there","slug":"home","$mixins":[],"$supertypes":["site:Page"]}}

Without lang the base content is returned. lang also works on folder listings under the same route.

SQL: UPDATE ... FOR LOCALE​

UPDATE site FOR LOCALE 'fr'
SET title = 'Bienvenue',
body = 'Bienvenue sur notre site'
WHERE path = '/home';
{"columns":["affected_rows"],"rows":[{"affected_rows":1}],"row_count":1,"execution_time_ms":2}

The token after UPDATE is the workspace name, written as a bare identifier (no quotes). The WHERE clause accepts path = '...' or id = '...', optionally combined with AND node_type = '...'. Add IN BRANCH 'name' after the locale to target another branch. Both forms write the same overlay.

Removing and hiding​

  • POST .../raisin:cmd/delete-translation with {"locale":"fr"} removes the whole overlay for that locale (204).
  • POST .../raisin:cmd/hide-in-locale with {"locale":"de"} hides the node in that locale: a read with ?lang=de returns 404 and the node is left out of listings for that locale. unhide-in-locale reverses it.

Deleting a node ends all of its translations, including the translations of its blocks. A read of the node at an earlier revision still sees them.

Translations and history​

Translations are versioned with the node. A time-travel read (the rev/… routes, or __revision = … in SQL) returns each locale's translation as it was at that revision, not the current one. See Time-Travel Queries.

Translated node names​

The reserved field __node_name is the node's name in a locale. It drives localized URLs such as /fr/produits/chaise:

UPDATE site FOR LOCALE 'fr' SET __node_name = 'bienvenue' WHERE path = '/home';

The REST pointer is /__node_name, and a package overlay accepts a top-level __node_name key. See Localized Paths.

Repeatable content needs UUIDs​

When a CompositeField is multiple: true and has a translatable sub-field, each item must carry a uuid. The overlay addresses items by uuid so it can merge only the translated fields and keep the rest of the item. Writing a node whose items lack one fails validation with COMPOSITE_MISSING_UUID.

# Element type
fields:
- $type: CompositeField
name: features
multiple: true
fields:
- { $type: TextField, name: title, translatable: true }
- { $type: TextField, name: icon }
# Base content: a uuid on every item
features:
- uuid: feat-fast
icon: zap
title: Fast Development
- uuid: feat-scale
icon: trending-up
title: Scalable

The translation for one item names it by uuid:

UPDATE site FOR LOCALE 'fr'
SET features[uuid='feat-fast'].title = 'Developpement rapide'
WHERE path = '/home';

The REST equivalent is the pointer /features/feat-fast/title.

Nested composites​

The rule applies at every repeatable level. A pointer alternates field and uuid at each array level, and the resolver walks it to any depth:

/sections/<section-uuid>/features/<feature-uuid>/title
UPDATE site FOR LOCALE 'de'
SET sections[uuid='s1'].features[uuid='f1'].title = 'Schnelle Entwicklung'
WHERE path = '/home';

A repeatable composite needs item uuids when it has a translatable field anywhere in its subtree. Composites you never translate can stay without them.

Staleness​

The server keeps a hash of the source value for every translated pointer. When the source changes after a translation was written, that pointer is reported as stale:

curl -s -X POST "http://localhost:8090/api/repository/myrepo/main/head/site/home/raisin:cmd/translation-staleness" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"locale":"fr"}'
{"stale":["/title"],"missing":[{"pointer":"/body","current_original_hash":"cb5e5c03..."}],"fresh":[],"unknown":[]}

fresh pointers are translated and current, stale ones were translated before the source changed, and missing ones are translatable fields with no translation in this locale. Re-translating a field clears the stale flag, or accept the source change without retranslating:

curl -s -X POST ".../home/raisin:cmd/acknowledge-staleness" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"locale":"fr","pointer":"/title"}'
{"acknowledged":true,"pointer":"/title","locale":"fr"}