Разделы документации
ОбзорБыстрый стартРедакции и возможностиМодели и поляРедактор контентаМедиатекаЛокализацияПубликация и работа командыAPI, SDK и генерация типовРасширения и инструментыРабочие проектыЗадания, вебхуки и наблюдаемостьАудит и управление даннымиСоветники и доверие к плагинамКорпоративный входПространства, квоты и масштабированиеCommerce и PortalПрава и безопасностьРазвёртывание и обновленияЛицензии и установка пакетовТекущие ограниченияПомощь и диагностикаДанные в кабинетеCore CMSDeveloper PlatformProduction UseWorkflowOperationsComplianceAI AssistantsPlugin TrustEnterprise IdentityEnterprise ScaleEnterprise DeploymentCommerce BundlePortal BundleNotty CMS DocumentationAuth & SecurityContent ModelingDeploymentEcosystem & Packaging ConventionsEditions and First-party ModulesExtensibilityGetting StartedMedia ManagementModule Extraction PathDraft & PublishUpgrade GuideWebhooks & IntegrationsCookbook: Blog with Next.jsCookbook: Custom PluginCookbook: Multilingual SiteOperations DocsBackup AutomationDeployment BlueprintsRunbook — Восстановление БД из бэкапаRunbook — Плановый деплойRunbook — Реакция на инцидентRunbook — Откат релизаRunbook — Горизонтальное масштабированиеRunbook — Ротация секретовRunbook — Major upgradeSecrets ManagementNotty CMS — Capability MapNotty Configuration ModelGenerated App ContractComponents and Dynamic ZonesMiddleware SystemPerformance & Scaling ToolkitDisaster Recovery PlaybookDistribution Model
Документация / Технический справочник

Cookbook: Multilingual Site

Техническое руководство из исходного проекта Notty. Примеры, параметры и эксплуатационные ограничения.

Все редакцииОбновлено 2026-09-30

Set up content localization in Notty CMS with multiple languages, per-field translation, and fallback strategies.

What You'll Build

  • Content type with localized fields (title, body) and non-localized fields (slug, cover)
  • Content in English and Russian
  • API queries with locale filtering
  • Translation status tracking

Step 1: Enable i18n on a Schema

// schemas/article.json
{
  "kind": "collectionType",
  "info": {
    "singularName": "article",
    "pluralName": "articles",
    "displayName": "Article"
  },
  "options": {
    "draftAndPublish": true,
    "timestamps": true,
    "i18n": {
      "enabled": true,
      "locales": ["en", "ru"],
      "defaultLocale": "en"
    }
  },
  "indexes": [{ "fields": ["slug", "locale"], "type": "unique" }],
  "attributes": {
    "title": {
      "type": "string",
      "required": true,
      "pluginOptions": { "i18n": { "localized": true } }
    },
    "slug": {
      "type": "string",
      "required": true
    },
    "body": {
      "type": "richtext",
      "pluginOptions": { "i18n": { "localized": true } }
    },
    "cover": {
      "type": "media",
      "allowedTypes": ["images"]
    }
  }
}

Key decisions:

  • title and body are localized — each locale has its own value
  • slug and cover are not localized — shared across all locales
  • Composite unique index on [slug, locale] prevents slug collisions within a locale

Step 2: Create Content in the Default Locale

curl -X POST http://localhost:2102/api/content/article \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Getting Started with Notty",
    "slug": "getting-started",
    "body": "<p>Welcome to Notty CMS — a modern headless content management system.</p>",
    "locale": "en"
  }'

Step 3: Add a Translation

Create a localization for the same entry:

curl -X POST http://localhost:2102/api/content/article/1/localizations \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "locale": "ru",
    "title": "Начало работы с Notty",
    "body": "<p>Добро пожаловать в Notty CMS — современную headless систему управления контентом.</p>"
  }'

Non-localized fields (slug, cover) are inherited from the original entry.

Save the returned localization ID from the response. In the examples below, assume the Russian localization was created as entry 2.

Step 4: Query by Locale

Get English Content

curl "http://localhost:2102/api/content/article?locale=en&published=true" \
  -H "Authorization: Bearer $TOKEN"

Get Russian Content

curl "http://localhost:2102/api/content/article?locale=ru&published=true" \
  -H "Authorization: Bearer $TOKEN"

Get All Localizations for an Entry

curl http://localhost:2102/api/content/article/1/localizations \
  -H "Authorization: Bearer $TOKEN"

Returns all locale versions of the entry.

Step 5: Translation Status

Track which translations are up to date:

# Inspect localizations and translation metadata for the document
curl http://localhost:2102/api/content/article/1/localizations \
  -H "Authorization: Bearer $TOKEN"

The response includes data.translationMeta, which stores the workflow/status metadata for each locale in the document.

Translation Statuses

Status Meaning
draft Translation in progress
in_review Pending review
approved Approved for publication
published Live
outdated Source locale changed — needs update

Update Translation Status

Use the localization record ID in the path. For example, to update the Russian entry created above:

curl -X PUT http://localhost:2102/api/content/article/2/localizations/status \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "approved",
    "notes": "Ready for editorial review"
  }'

Step 6: Compare Translations

See what changed between locales:

curl "http://localhost:2102/api/content/article/2/localizations/diff" \
  -H "Authorization: Bearer $TOKEN"

The diff endpoint uses the localization entry in the URL and compares it against its source locale automatically.

Frontend Integration

Language Switcher Pattern

// Fetch articles for the current locale
async function getArticles(locale: string) {
  const res = await fetch(
    `${NOTTY_URL}/api/content/article?locale=${locale}&published=true&populate=cover`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  return res.json();
}

// In your Next.js page:
// app/[locale]/page.tsx
export default async function Home({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params;
  const { data: articles } = await getArticles(locale);
  // Render articles...
}

SEO with hreflang

// Generate alternate links for SEO
async function getAlternateLinks(slug: string) {
  const locales = ['en', 'ru'];
  return locales.map((locale) => ({
    rel: 'alternate',
    hrefLang: locale,
    href: `https://example.com/${locale}/blog/${slug}`,
  }));
}

Best Practices

  1. Always set locale in queries — otherwise you get all locales mixed together
  2. Use composite unique indexes on [slug, locale] to allow the same slug in different locales
  3. Only localize user-facing text — keep slugs, IDs, and media references non-localized
  4. Track translation status — mark translations as outdated when the source locale changes
  5. Set up a translation workflow — use the admin panel to review and approve translations before publishing

Источник: docs/cookbook/multilingual-site.md. Снимок документации исходного проекта. Технический справочник сохраняет язык оригинала.