Content Engine

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:

plugins/example/src/content/article.ts
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:

  1. Main Table (example_articles): Stores id, code, system timestamps (createdAt, updatedAt), and shared non-localized columns.
  2. 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:

plugins/example/src/database/articles.ts
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.translationTable

Step 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:

plugins/example/src/components/article-card.tsx
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}/revisions and POST /{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

OptionTypeDefaultDescription
enabledtrueEnables multi-language localization.
defaultLocalestringDefault locale code (e.g. "en"). Required when enabled: true.
fallback"default" | "none""default"Fallback strategy when a translation in the requested locale is missing.