Skip to content

i18n

import { ... } from '@system-core/core/cms'

The core/cms/i18n sub-module is the single translation authority for the platform. It handles both CMS content translations and plugin-seeded UI strings through a unified namespace model.

Row Model

Translation rows are keyed by namespace + entityType + entityId + locale.

fields stores the translated key/value pairs as Record<string, string>.

NamespaceentityTypeentityIdUse
contentContent model name (e.g. post)Record IDCMS content translations
uiui-stringsnamespaceShared runtime UI strings
ui:plugin-<id>ui-stringsnamespaceCapability-owned UI strings

API

ts
// List translation rows
listTranslations(filters?: TranslationFilters, deps: TranslationDeps): Promise<Translation[]>

// Upsert many keys in one call — used by plugin seed and bulk admin edit
bulkUpsertTranslations(input: BulkUpsertTranslationsInput, deps: TranslationDeps): Promise<Translation>

// Flatten all matching UI rows into one Record<key, string>
getUiStrings(input: GetUiStringsInput, deps: TranslationDeps): Promise<Record<string, string>>

// Single content translation helpers
getTranslation(entityType, entityId, locale, deps, namespace?): Promise<Translation | null>
getTranslatedFields(entityType, entityId, locale, deps): Promise<Record<string, string>>
deleteTranslation(entityType, entityId, locale, deps, namespace?): Promise<void>

TranslationFilters

ts
interface TranslationFilters {
  locale?: Locale
  namespace?: string         // exact match
  namespacePrefix?: string   // prefix match — e.g. 'ui' matches 'ui' and 'ui:plugin-blog'
  entityType?: string
  entityId?: string
}

BulkUpsertTranslationsInput

ts
interface BulkUpsertTranslationsInput {
  locale: Locale
  namespace: string
  entries: Record<string, string>
  entityType?: string     // defaults to 'ui-strings' for UI namespaces
  entityId?: string       // defaults to namespace for UI namespaces
  status?: TranslationStatus
  translatedBy?: string | null
  reviewedBy?: string | null
}

TranslationDeps vs TranslationStore

TranslationDeps is the minimal adapter contract that createPrismaDeps() satisfies.

TranslationStore extends it with the higher-level ops:

ts
interface TranslationStore extends TranslationDeps {
  count(filters?: TranslationFilters): Promise<number>
  bulkUpsert(input: BulkUpsertTranslationsInput): Promise<Translation>
  getUiStrings(input: GetUiStringsInput): Promise<Record<string, string>>
}

Plugin Seed Translations

Capability plugins seed UI strings through installCapabilityPlugin:

ts
export const backend = installCapabilityPlugin(capability, {
  seedTranslations: {
    en: {
      'blog.list.title': 'Posts',
      'blog.list.empty': 'No posts yet',
      'blog.action.new': 'New Post'
    },
    fr: {
      'blog.list.title': 'Articles',
      'blog.list.empty': 'Aucun article',
      'blog.action.new': 'Nouvel article'
    }
  }
})

Conventional layout inside each plugin package:

src/
  i18n/
    en.json
    fr.json

Seeds are written once per locale per capability namespace. If a locale already has rows for a namespace, the seed is skipped — admin edits are never overwritten.

Public Runtime

createSystem() exposes resolved UI strings through:

  • system.uiStrings — flattened Record<string, string> for the system locale
  • publicConfig.uiStrings — same, available on the GET /api/_system/public-config endpoint

The @maxnate-ui/system-core integration reads strings in this order:

  1. options.uiStrings (inline override)
  2. GET /api/_system/public-config response
  3. GET /api/public/i18n/ui-strings (live refresh)

Admin Surface

The translation management page is at /admin/cms/translations. It supports:

  • Locale filtering
  • Namespace and namespace-prefix filtering
  • Inline key/value editing
  • Mark-for-review actions

Translation Status

ts
type TranslationStatus = 'untranslated' | 'in_progress' | 'translated' | 'reviewed'

system-core documentation for maintainers, integrators, and AI build agents.