Internationalization
Internationalization
Norbital localizes every surface — the website, the Colony platform, and every tenant workspace — into English and Simplified Chinese . The platform ships both catalogs; a tenant workspace ships its own copy for both locales and can override platform chrome.
The model
A catalog is a flat map of message keys to strings — no nesting, so catalogs merge by spread with no shape surprises. Every catalog exists as an en / zh pair, and defineMessages enforces at compile time that both locales carry exactly the same keys. English is the source of truth; the runtime falls back
, so a future third language can be partial without breaking the type contract. Placeholders use {name} syntax and are passed as a second argument:
const { t } = useI18n<TenantI18nKeys>();
t('app.quotes.title'); // "Quotes" / "报价"
t('component.doc_no', { no: 'Q-2026-014' });
Supported locales
The supported set is closed: 'en' and 'zh' . Locale resolution on the client follows the persisted choice ( norbital.locale in local storage), then the browser languages, then English. Server code resolves the same way from an Accept-Language header (or an explicit ?lang= ). Formatting follows the same locale: 'en' maps to en-US and 'zh' to zh-CN for Intl.* date, number, and currency formatting.
Platform catalogs
The bolt ships two catalogs that cover the platform chrome — the shell, settings, notifications, agent surfaces, identity pages, and transactional email:
@norbital-ai/ui/i18n— the component library catalog (table.*,form.*,dataRenderer.*, …).@norbital-ai/bolt/i18n— the bolt chrome catalog (bolt.*,server.*,email.*).
The build-time catalog merge is platform catalogs ← tenant overrides , so a tenant can restyle platform text per locale by reusing the same keys.
Tenant copy
A workspace authors its copy under src/i18n/ :
src/
└── i18n/
├── messages.en.json # your English copy
└── messages.zh.json # your Chinese copy — same keys
Both files must carry exactly the same keys; the bolt compiler rejects a mismatch. App files translate with useI18n<TenantI18nKeys>() from @norbital-ai/ui/i18n , where TenantI18nKeys is a generated type union derived from your messages.en.json ( $bolt/i18n-keys ):
// src/apps/+quotes.svelte
import { useI18n } from '@norbital-ai/ui/i18n';
import type { TenantI18nKeys } from '$bolt/i18n-keys';
import { PageHeader } from '@norbital-ai/ui/page-header';
const { t } = useI18n<TenantI18nKeys>();
App metadata in <svelte:head> stays static English — the compiler reads it for app identity. The sidebar label is localized per locale through the app.<appId>.title key in your catalog instead.
Switching language
The website switches by URL: