Skip to content

Internationalization

Internationalization

A workspace’s text lives in message catalogs under src/i18n/. The base catalog is the source of truth, and every other locale mirrors its keys.

// src/i18n/+messages.ts (templates/field-operations, excerpt)
import { messages } from '@norbital-ai/bolt';

export default messages({
  'app.field_ops_contractor.title': 'Contractor Workspace',
  'app.field_ops_contractor.header_title': 'Contractor jobs'
  // …
});

// src/i18n/+zh.messages.ts: the same keys or fewer, never more
export default messages({
  'app.field_ops_contractor.title': '承包商工作区',
  'app.field_ops_contractor.header_title': '承包商任务'
  // …
});

Authored files

src/i18n/+messages.ts is messages({ … }), a flat map of dot-namespaced keys to strings, and there is exactly one when src/i18n/ exists. src/i18n/+<locale>.messages.ts holds another locale; its keys must be a subset of the base, or it is a type error. Keep {placeholders} identical in every locale.

Messages in pages

bolt.t(key, vars?) is typed from the base catalog, so an unknown key is a type error. bolt.locale is the viewer’s locale — their stored choice, then their browser languages, then the workspace locale — and the shell owns the language toggle.

Messages in declarations

A Msg in a declaration — an app or page title, a field label or help, a notification title — is a catalog key. Outside catalog mode, in a workspace with no src/i18n/, it may be a literal string.

The raw-text rule

With a catalog, every user-facing string in a page or record view comes from bolt.t(…): a raw text node, or a literal passed to a prop that renders, is a missing catalog entry and fails bolt check. Identifiers, URLs, seed data and model descriptions are not user-facing copy.

Key conventions

  • app.<app>.title, app.<app>.description and app.<app>.tab_<name> for app copy
  • component.<name> for labels shared across pages
  • plurals as {count}, with separate keys where the languages differ