# Interface text and languages All text that a user reads comes from the catalogs in `packages/i18n` (`@openbot/i18n`). Desktop, shared UI, the web client and mobile use the same catalogs. English is the source. Brazilian Portuguese uses the `pt` catalog. German (`de`), Spanish (`es`), French (`fr`), Italian (`it`), Japanese (`ja`), Korean (`ko`), Polish (`pl`), Brazilian Portuguese (`pt`), Russian (`ru`), Turkish (`tr`) and Simplified Chinese (`zh`) translate all keys, on desktop, the web client and mobile. A missing translation shows its English source text. ## Where the text lives `packages/i18n/src/messages//.ts` holds one area. The area is the surface where a reader looks for the text: `settings`, `chat`, `mobile/settings`, `error/team`. - `common` holds short words with no context of their own: Cancel, Save, Retry. If the translation can change with the screen, put the key in that screen's area, also when the English is the same. - `format` holds units and list words that `createFormat` uses. - `language` holds language names, such as the dictation languages. - `error/*` and `status/*` hold English that the main process, the backend, a host or the team client sends. See [Text from another process](#text-from-another-process). - `mobile/*` holds text that only the mobile app shows. The mobile bundle contains `common`, `format`, `language`, `error/*`, `status/*` and `mobile/*`, and no desktop areas. `en/index.ts`, `en/mobile.ts`, `en/source.ts` and `en/shared.ts` only join the area modules. Do not put keys in them. ## Add a key 1. Add the key to the English module of its area: ```ts export const messages = defineMessages("settings", { "settings.language.title": "Language", "settings.agents.count": { one: "{count} agent", other: "{count} agents" }, }); ``` `defineMessages` makes the build fail when a key does not start with the module prefix. A message with `{count}` has plural forms. Other values are `{name}` placeholders. 2. Render it. Write the key as a literal, so `bun run i18n:check` and a search can find it. 3. Add translations to the same module in `de/`, `es/`, `fr/`, `it/`, `ja/`, `ko/`, `pl/`, `pt/`, `ru/`, `tr/` and `zh/` if you can. If you cannot, leave the key out: the screen shows English, and the check reports the key as not translated. Put each translated key in the English order. `bun run i18n:check --fix` does this for you. 4. Keep the label of a button, tab, menu item or badge short. A button does not wrap or shrink, so a long label pushes the row; a badge or menu item can cut the label off. Find the shortest natural word, not a literal translation: German "Wiederholen", not "Erneut versuchen", for "Retry". If the translation is more than about 1.5 times the English, look for a shorter word. For a map from a value to text, map to keys, not to text: ```ts const STATUS_LABEL = { running: "agent.status.running", stopped: "agent.status.stopped", } as const satisfies Record; ``` ## Use the text | Surface | API | | --- | --- | | Desktop renderer and `packages/ui` | `const { t, format, errorMessage } = useText()` from `@openbot/ui/text` | | Mobile screens | `useText()` from `@/shared/lib/text` | | Mobile code outside React (stores, `Alert.alert` callbacks) | `currentText()` from `@/shared/lib/text` | | Main process, native UI (dialogs, menus, window titles) | `language.translate` from `LanguageService` | | Main process, backend, team client: errors and status text | `sourceText(key, params)` from `@openbot/i18n/source` | - In `packages/ui`, call `useText()`. Do not add a `t` prop. The text context is the one application value that shared UI reads from a context. Outside a provider it is English, so a test or a story works without setup. - `errorMessage(error, fallback)` is `userErrorMessage` in the reader's language. Give it a translated fallback: `errorMessage(err, t("skill.saveFailed"))`. - The renderer's `I18nProvider` supplies the text context in every window. The web client and Storybook use `StaticI18nProvider` with a fixed locale. ## Text from another process An error crosses IPC as its message only, and the Team API sends `{ error: message }` to clients of every released version. So the sender writes English, and the screen translates it: ```ts // main, backend, team client throw new Error(sourceText("error.skill.nameTaken", { name })); ``` The English text of a source key must be the same, byte for byte, as the text the code sent before. Old clients and code that compares error text depend on it. To change the English of an `error.*` or `status.*` key, treat it as a wire change: find every comparison and every older client that reads it. The screen calls `errorMessage` or `sourceText(text)`. These find the key whose English template matches the text and render the key in the reader's language. `{count}` matches a number, and the other placeholders match any text. `{reason}` and `{detail}` are translated again. Text that no template matches, such as text from an older host or a provider, shows as it is. A remote client reads its own language, not the host's. `bun run i18n:check` fails when two source templates can match the same text. ## Do not translate - Protocol values, IPC channels, identifiers and saved values. Map a saved value to a key when it renders; never save translated text. - Analytics event names and properties, and log lines. - Agent prompts and the text that the app sends to a provider. - User content: messages, file names, agent names. - Product and brand names: OpenBot, provider names, ZIP, JSON. - Errors that only a malformed payload, a programming error or a development tool can cause, such as a failed shape check in an `*-inputs.ts` file. ## Numbers, dates and sizes Do not call `toLocaleString()` without a locale, or with a fixed `"en-US"`: both ignore the language that the user selected. Use `format` from `useText()`: | Method | Output | | --- | --- | | `format.number(value, options)` | 1,234.5 | | `format.compact(value)` | 1.2K | | `format.percent(ratio)` | 25% for 0.25 | | `format.currencyUsd(value)` | $1.50 | | `format.date(value, options)` | `Intl.DateTimeFormat` options | | `format.list(items)` | a, b, and c | | `format.fileSize(bytes)` | 512 B, 12 KB, 3.4 MB | The mobile app runs on Hermes, which may not have `Intl.PluralRules`, `Intl.ListFormat` or compact numbers (not confirmed on a device). `packages/i18n` has a fallback for each one. ## Checks - `tools/ui-foundation/no-hardcoded-ui-text.grit` fails on literal text in `src/renderer/src`, `packages/ui/src` and `apps/mobile/src`: JSX text, a literal `aria-label`, `title`, `placeholder`, `alt`, `label`, `description`, `accessibilityLabel` or `accessibilityHint`, a literal toast or alert, and a literal `errorMessage` fallback. Tests and stories are not checked. - `bun run i18n:check` fails on a key with the wrong prefix, a key in two modules, a translated key that English does not have, a placeholder that English does not have, a plural shape that is not the same as English, an ambiguous source template, and an English key that no code names. It reports the keys that are not translated, and the translated modules whose keys are not in the English order. `bun run i18n:check --fix` puts those keys in the English order. Each key moves with the comments and blank lines above it, and no text changes. `bun run i18n:check --json` writes `.openbot-build/i18n-report.json`. - Storybook has a **Locale** menu in the toolbar. Use it to see a story in each language. - `bun scripts/i18n-fit-e2e.ts --storybook=` opens each story in each language, opens its menus, and finds control text that is cut off, runs past its box, or wraps. `--locales=de,pl` and `--stories=` make the run shorter. It uses one browser; all stories in all languages take about one hour with `--workers=2`. It writes `.openbot-build/i18n-fit/report.json` and one screenshot for each failure that English does not have. - With a dev app open, `bun scripts/i18n-e2e.ts --allow-mutations` switches the app through each language. It fails on a catalog key or a placeholder on the screen, and on control text that does not fit when the English fits. ## Add a language 1. Add the BCP 47 tag to `APP_LANGUAGES` in `packages/contracts/src/app-language.ts`. Never rename a tag that shipped: a saved preference would read back as an unknown language. 2. Add it to `TRANSLATED_LOCALES` in `packages/i18n/src/locale.ts`. 3. Copy `packages/i18n/src/messages/fr/` to `messages//`. Empty each module to `{} as const satisfies PartialTranslation`, and translate what you can. 4. Add the new catalogs to the `catalogs` maps in `packages/i18n/src/app-catalogs.ts`, `source-catalogs.ts`, `format-catalogs.ts` and `mobile.ts`, and to the `loaders` record in `lazy-catalogs.ts`. The public web build uses `lazy-catalogs.ts` in place of the first three. The compiler shows each place that needs one. 5. Add the CLDR plural rule to `BUILT_IN_RULES` in `packages/i18n/src/plural.ts`. 6. Add the language's own name, and its `lang` tag, to `packages/i18n/src/languages.ts`. 7. Run `bun run typecheck:i18n` and `bun run i18n:check`. With a dev app open, run `bun scripts/i18n-e2e.ts --allow-mutations`; it writes `.openbot-build/i18n-e2e/report.json`. Both Settings screens show the new language when the tag is in `APP_LANGUAGES`. ## Terms Keep these terms the same in each catalog. German uses *du*. Spanish uses *tú* and avoids region-specific wording. French uses *vous*. Japanese uses です・ます. Korean uses 해요체 for sentences and short plain nouns for buttons, tabs and labels. Simplified Chinese uses 你 and keeps the plain verb form for actions, without 请 where a button or menu item already states the action. Brazilian Portuguese uses *você*. Russian uses *вы*. Polish uses the informal *ty* imperative. Italian uses the informal *tu* and the imperative for actions. Both `pt-BR` and `pt-PT` system settings select the `pt` catalog; numbers and dates keep the system's regional conventions. Every `zh` system setting selects the Simplified Chinese catalog, so a computer set to `zh-TW` or `zh-HK` reads it too. | English | French | Japanese | Portuguese (Brazil) | Russian | Turkish | | --- | --- | --- | --- | --- | --- | | agent | agent | エージェント | agente | агент | ajan | | chat | discussion | チャット | chat | чат | sohbet | | conversation | conversation | 会話 | conversa | диалог | konuşma | | channel | canal | チャンネル | canal | канал | kanal | | server | serveur | サーバー | servidor | сервер | sunucu | | host | hôte | ホスト | computador anfitrião | хост | ana makine | | skill | compétence | スキル | habilidade | навык | beceri | | routine | routine | ルーティン | rotina | регулярная задача | rutin | | workspace | espace de travail | ワークスペース | espaço de trabalho | рабочее пространство | çalışma alanı | | provider | fournisseur | プロバイダー | provedor | провайдер | sağlayıcı | | Settings | Réglages | 設定 | Configurações | Настройки | Ayarlar | German, Spanish, Polish and Italian terms: | English | German | Spanish | Polish | Italian | | --- | --- | --- | --- | --- | | agent | Agent | agente | agent | agente | | chat | Chat | chat | czat | chat | | conversation | Unterhaltung | conversación | rozmowa | conversazione | | channel | Kanal | canal | kanał | canale | | server | Server | servidor | serwer | server | | host | Host | host | host | host | | skill | Fähigkeit | habilidad | umiejętność | skill | | routine | Routine | rutina | rutyna | routine | | workspace | Arbeitsbereich | espacio de trabajo | obszar roboczy | workspace | | provider | Anbieter | proveedor | dostawca | provider | | Settings | Einstellungen | Ajustes | Ustawienia | Impostazioni | | Computer Use | Computersteuerung | Uso del equipo | Sterowanie komputerem | Controllo del computer | | Steer | Steuern | Orientar | Pokieruj | Indirizza | Korean terms: | English | Korean | | --- | --- | | agent | 에이전트 | | chat | 채팅 | | conversation | 대화 | | channel | 채널 | | server | 서버 | | host | 호스트 | | skill | 스킬 | | routine | 루틴 | | workspace | 워크스페이스 | | provider | 제공자 | | Settings | 설정 | | Computer Use | 컴퓨터 제어 | | Steer | 방향 지시 | A settings tab or a settings row has little space, so it can use a short form of a term. The page title keeps the full term. Computer Use tab: German *PC-Steuerung*, Italian *Controllo PC*, Polish *Sterowanie PC*, Russian *Управление ПК*. Routines tab or row: Russian *Автозадачи*. Simplified Chinese terms: | English | 简体中文 | | --- | --- | | agent | 助手 | | chat, of an agent | 对话 | | chat, of a messaging connection | 聊天 | | conversation | 对话 | | channel | 频道 | | server | 服务器 | | host | 主机 | | skill | 技能 | | routine | 自动化任务 | | workspace | 工作区 | | provider | 服务商 | | Settings | 设置 | | Computer Use | Computer Use | | Steer | 引导 | | session | 会话 | Chinese has one plural form, so every count takes the `other` text and `plural.ts` has `zh` return `other` for every number. An agent chat and a conversation are 对话; a chat on a messaging connection, such as a Telegram chat, is 聊天; a session is 会话. The catalog uses the mainland quote marks “ ” and prefers 该 for a definite item (该服务器, 该文件) to the literary 此.