Localization & Translations
Step-by-step guide to building multi-language content models with localized fields, translation tables, independent workflows, and localized public APIs.
Content Engine supports localized fields, storing translatable text in dedicated per-language tables ({tableName}_translations) while keeping shared fields in the main table.
Prerequisites & Context
Multi-language support is configured in your content type definition file:
When createContentModel compiles a localized model, it automatically generates a separate secondary database table ({tableName}_translations) to hold per-language strings without altering the schema of the main table.
Step-by-Step Multi-Language Setup
Step 1: Mark Localized Fields in Definition
Set localization configuration and add localized: true to translatable fields in your content definition:
import { defineContentType, field } from "@vitnode/core/content"
export const articleContentType = defineContentType({
id: "example.article",
tableName: "example_articles",
localization: {
enabled: true,
defaultLocale: "en",
fallback: "default", // "default" | "none"
},
fields: {
code: field.text({ required: true }), // Shared field on main table
title: field.text({ required: true, localized: true }), // Localized
content: field.textarea({ localized: true }), // Localized
},
})localized: true is supported on text, textarea, slug, and group fields. Shared fields (such as number, boolean, select, relation, file, user) always reside on the base table.
Step 2: Understand the Database Tables
Compiling this model automatically builds two PostgreSQL tables:
- Main Table (
example_articles): Storesid,code, system timestamps (createdAt,updatedAt), and shared non-localized columns. - Translation Table (
example_articles_translations): Stores composite primary key(itemId, languageId),version, timestamps, publication columns (if enabled), and all localized field values.
example_articles example_articles_translations
┌─────┬────────┬──────────┐ ┌────────┬────────────┬─────────┬────────┬─────────┐
│ id │ code │ authorId │ ◄──┐ │ itemId │ languageId │ version │ title │ content │
├─────┼────────┼──────────┤ │ ├────────┼────────────┼─────────┼────────┼─────────┤
│ 1 │ ART-01 │ 5 │ └─────│ 1 │ 1 │ 1 │ Hello │ World │
└─────┴────────┴──────────┘ (PK) │ 1 │ 2 │ 1 │ Bonjour│ Monde │
└────────┴────────────┴─────────┴────────┴─────────┘
▲
│ (references core_languages.id)languageId references the system core_languages.id table, and itemId cascades upon deletion of the parent record.
Step 3: Export the Translation Table for Drizzle Kit
When exporting database tables from your plugin for migrations, export both the base table and translationTable:
import { createContentModel } from "@vitnode/core/content/server"
import { articleContentType } from "../content/article"
export const articleContent = createContentModel(articleContentType)
export const exampleArticlesTable = articleContent.table
export const exampleArticlesTranslationsTable = articleContent.translationTableStep 4: Render Localized Content
When querying content via the public API or resolveContentDelivery, translatable fields (title, content) automatically resolve in the requested locale, with fallback to defaultLocale when fallback: "default" is configured.
For UI labels and chrome text, use useTranslations from use-intl:
import { useTranslations } from "use-intl"
interface Props {
article: {
title: string // Resolved to active locale by Content Engine
content: string
}
}
export const ArticleCard = ({ article }: Props) => {
const t = useTranslations("example")
return (
<div className="card">
<span className="badge">{t("content.article.badge_label")}</span>
<h3>{article.title}</h3>
<p>{article.content}</p>
</div>
)
}AdminCP Translation Features
- Language Selector Tabs: Automatically rendered in AdminCP form dialogs for each enabled system language.
- Independent Status: Each translation language manages its own draft/published lifecycle when publication is enabled.
- Independent Revisions: When editorial is enabled, revision histories and restores operate per locale (
GET /{id}/translations/{locale}/revisionsandPOST /{id}/translations/{locale}/revisions/{revisionId}/restore). - Composite Key Integrity: Translations are uniquely identified by
(itemId, languageId), guaranteeing exactly one translation per locale per item.
Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
enabled | true | — | Enables multi-language localization. |
defaultLocale | string | — | Default locale code (e.g. "en"). Required when enabled: true. |
fallback | "default" | "none" | "default" | Fallback strategy when a translation in the requested locale is missing. |