i18n Workflow
Contentrain handles internationalization through per-locale content files, dictionary models for UI strings, and a translation pipeline that works with your AI agent.
How Contentrain Handles i18n
Every piece of content in Contentrain can be localized. The system stores one file per locale per model, with the same keys across all locales.
Locale configuration
Locales are defined in .contentrain/config.json:
{
"locales": [
{ "code": "en", "label": "English", "default": true },
{ "code": "tr", "label": "Turkish" },
{ "code": "de", "label": "German" }
]
}The default locale is the source of truth. All other locales are translations of the default.
Locale strategies
Each model can use a different file organization strategy:
| Strategy | File pattern | Best for |
|---|---|---|
file (default) | {model-id}/{locale}.json | Most models |
suffix | {model-id}.{locale}.json | Single-directory layouts |
directory | {locale}/{model-id}.json | Locale-first organization |
none | {model-id}/data.json | Single-language projects |
For document models, the patterns use markdown files:
| Strategy | File pattern |
|---|---|
file | {slug}/{locale}.md |
suffix | {slug}.{locale}.md |
directory | {locale}/{slug}.md |
Dictionary Model for UI Strings
Dictionaries are the primary model for UI strings — button labels, form messages, navigation items, error messages.
Structure
A dictionary is a flat key-value JSON file where all values are strings:
{
"auth.login.button": "Sign in",
"auth.login.error": "Invalid email or password",
"auth.register.button": "Create account",
"nav.home": "Home",
"nav.blog": "Blog",
"nav.pricing": "Pricing",
"cta.get_started": "Get started free",
"cta.learn_more": "Learn more"
}TIP
Keys are semantic dot-separated addresses. Use a consistent naming convention: {section}.{subsection}.{element}. Keys are the identities — no id or slug field.
Creating a dictionary
"Create a dictionary model called ui-labels for all UI strings"
The agent creates the model and populates it with extracted strings. Each locale gets its own file:
.contentrain/content/ui/ui-labels/
en.json # Source locale
tr.json # Turkish translation
de.json # German translationQuerying dictionaries
<script setup lang="ts">
// In a Nuxt server route:
import { dictionary } from '#contentrain'
const labels = dictionary('ui-labels').locale('en').get()
// labels['auth.login.button'] → "Sign in"
// Single key:
const loginBtn = dictionary('ui-labels').locale('en').get('auth.login.button')
// loginBtn → "Sign in"
</script>import { dictionary } from '#contentrain'
// All labels:
const labels = dictionary('ui-labels').locale('en').get()
// Single key:
const loginBtn = dictionary('ui-labels').locale('en').get('auth.login.button')---
import { dictionary } from '#contentrain'
const locale = Astro.currentLocale ?? 'en'
const t = dictionary('ui-labels').locale(locale).get()
---
<button>{t['auth.login.button']}</button>Collection and Singleton Locale Files
Collections and singletons also support per-locale files.
Collection example
A testimonials collection with i18n enabled:
.contentrain/content/marketing/testimonials/
en.json
tr.jsonen.json:
{
"a1b2c3d4e5f6": {
"author": "Jane Smith",
"role": "CTO at Acme",
"quote": "Contentrain transformed our content workflow"
},
"f6e5d4c3b2a1": {
"author": "John Doe",
"role": "Lead Developer",
"quote": "Finally, content that developers can manage"
}
}tr.json:
{
"a1b2c3d4e5f6": {
"author": "Jane Smith",
"role": "Acme CTO",
"quote": "Contentrain içerik iş akışımızı dönüştürdü"
},
"f6e5d4c3b2a1": {
"author": "John Doe",
"role": "Baş Geliştirici",
"quote": "Sonunda, geliştiricilerin yönetebileceği içerik"
}
}INFO
Entry IDs are identical across locales. Only field values change. The author field may or may not be translated depending on context.
Singleton example
A hero-section singleton:
.contentrain/content/marketing/hero-section/
en.json
tr.jsonen.json:
{
"title": "Build faster with AI-powered content",
"subtitle": "Ship your next project in days, not weeks",
"cta_text": "Get started free"
}tr.json:
{
"title": "Yapay zeka destekli içerikle daha hızlı geliştirin",
"subtitle": "Projenizi haftalar değil günler içinde gönderin",
"cta_text": "Ücretsiz başlayın"
}Translation Workflow
The complete i18n pipeline follows four steps: extract, copy locale, translate, generate.
Step 1. Extract content (Phase 1 of normalize)
First, extract hardcoded strings into Contentrain content. See the Normalize Flow guide for the full process.
After extraction, you have content in the default locale (e.g., en.json).
Step 2. Copy locale
Create target locale files by copying the source:
"Copy all English content to Turkish"
The agent calls:
contentrain_bulk({
operation: "copy_locale",
model: "ui-labels",
source_locale: "en",
target_locale: "tr"
})This creates tr.json with the same structure as en.json, values still in English. Repeat for each model that needs translation.
TIP
Copy locale preserves all keys and structure. The agent translates values in the next step without risk of missing keys.
Step 3. Translate
Ask the agent to translate the copied content:
"Translate all Turkish content. Keep the tone professional and concise. Preserve all placeholders like {name} and {count}."
The agent reads each tr.json file, translates every value, and saves using contentrain_content_save.
For dictionaries:
{
"auth.login.button": "Giriş yap",
"auth.login.error": "Geçersiz e-posta veya şifre",
"auth.register.button": "Hesap oluştur",
"nav.home": "Ana Sayfa",
"nav.blog": "Blog",
"nav.pricing": "Fiyatlandırma"
}WARNING
The agent must preserve all placeholder tokens like {name}, {count}, {price} during translation. These are runtime-replaced values.
Step 4. Regenerate the SDK client
npx contentrain generateThe application now serves localized content from the new locale files.
Parameterized Templates
Contentrain supports {placeholder} syntax for dynamic values in strings:
{
"welcome.greeting": "Welcome back, {name}!",
"cart.item_count": "You have {count} items in your cart",
"pricing.monthly": "{price}/month"
}Placeholders are preserved across all locales:
{
"welcome.greeting": "Tekrar hoş geldin, {name}!",
"cart.item_count": "Sepetinizde {count} ürün var",
"pricing.monthly": "{price}/ay"
}Rendering parameterized strings
The SDK returns raw strings with placeholders. Your application replaces them at runtime:
<script setup lang="ts">
import { dictionary } from '#contentrain'
const labels = dictionary('ui-labels').locale('en').get()
function t(key: string, params: Record<string, string> = {}) {
let value = labels[key] ?? key
for (const [k, v] of Object.entries(params)) {
value = value.replace(`{${k}}`, v)
}
return value
}
</script>
<template>
<p>{{ t('welcome.greeting', { name: user.name }) }}</p>
</template>import { dictionary } from '#contentrain'
const labels = dictionary('ui-labels').locale('en').get()
function t(key: string, params: Record<string, string> = {}) {
let value = labels[key] ?? key
for (const [k, v] of Object.entries(params)) {
value = value.replace(`{${k}}`, v)
}
return value
}
export function Welcome({ name }: { name: string }) {
return <p>{t('welcome.greeting', { name })}</p>
}---
import { dictionary } from '#contentrain'
const labels = dictionary('ui-labels').locale(Astro.currentLocale ?? 'en').get()
function t(key: string, params: Record<string, string> = {}) {
let value = labels[key] ?? key
for (const [k, v] of Object.entries(params)) {
value = value.replace(`{${k}}`, v)
}
return value
}
---
<p>{t('welcome.greeting', { name: user.name })}</p>TIP
If you are already using a framework i18n library (vue-i18n, next-intl, paraglide), use the library's interpolation instead. Contentrain dictionaries can serve as the message source for these libraries.
Agent Prompt Examples
Here are practical prompts for each stage of the i18n workflow:
Setting up locales
"Add Turkish and German as supported locales in my Contentrain config"
Creating dictionaries
"Create a dictionary model for UI labels. Extract all button texts, form labels, and navigation items from my components."
Translating content
"Translate all content in the ui-labels dictionary to Turkish. Use formal tone. Preserve all {placeholder} tokens."
Adding a new locale later
"Add French as a new locale. Copy all existing content from English and translate it."
Reviewing translations
"Check all Turkish translations for missing keys or untranslated values"
The agent calls contentrain_validate which checks for i18n completeness — missing keys, mismatched placeholders, and untranslated values.
SDK Locale Selection
Every SDK query method accepts a .locale() call:
import { query, singleton, dictionary, document } from '#contentrain'
// Collections
const posts = query('blog-post').locale('tr').all()
// Singletons
const hero = singleton('hero').locale('tr').get()
// Dictionaries
const labels = dictionary('ui-labels').locale('tr').get()
// Documents
const article = document('blog-article').locale('tr').bySlug('getting-started')Dynamic locale from request
// server/api/posts.get.ts
import { query } from '#contentrain'
import { getQuery } from 'h3'
export default defineEventHandler((event) => {
const locale = getQuery(event).locale?.toString() ?? 'en'
return query('blog-post').locale(locale).all()
})// app/[locale]/blog/page.tsx
import { query } from '#contentrain'
export default async function BlogPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const posts = query('blog-post').locale(locale).all()
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}---
import { query } from '#contentrain'
const locale = Astro.currentLocale ?? 'en'
const posts = query('blog-post').locale(locale).all()
---// src/routes/[locale]/blog/+page.server.ts
import { query } from '#contentrain'
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = ({ params }) => {
const posts = query('blog-post').locale(params.locale).all()
return { posts }
}Content Update Workflow
When dictionary content is added or updated:
- Edit content via MCP (
contentrain_content_save) - Re-run
npx contentrain generateto update the static data modules - Rebuild the application
For development, use watch mode:
npx contentrain generate --watchChanges to .contentrain/ content trigger automatic client regeneration, which Vite/webpack hot-reloads into the running dev server.
Team Translation Workflow
For teams with dedicated translators or multilingual reviewers, Contentrain Studio provides a visual interface for reviewing translations side-by-side, with approval workflows and version tracking.