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), Dutch (nl), 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.
packages/i18n/src/messages/<locale>/<area>.ts holds one area. The area is the surface where a
reader looks for the text: settings, chat, mobile/settings, error/team.
commonholds 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.formatholds units and list words thatcreateFormatuses.languageholds language names, such as the dictation languages.error/*andstatus/*hold English that the main process, the backend, a host or the team client sends. See Text from another process.mobile/*holds text that only the mobile app shows. The mobile bundle containscommon,format,language,error/*,status/*andmobile/*, 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 the key to the English module of its area:
export const messages = defineMessages("settings", { "settings.language.title": "Language", "settings.agents.count": { one: "{count} agent", other: "{count} agents" }, });
defineMessagesmakes 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. -
Render it. Write the key as a literal, so
bun run i18n:checkand a search can find it. -
Add translations to the same module in
de/,es/,fr/,it/,ja/,ko/,nl/,pl/,pt/,ru/,tr/andzh/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 --fixdoes this for you. -
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:
const STATUS_LABEL = {
running: "agent.status.running",
stopped: "agent.status.stopped",
} as const satisfies Record<AgentStatus, AppTextKey>;| 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, calluseText(). Do not add atprop. 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)isuserErrorMessagein the reader's language. Give it a translated fallback:errorMessage(err, t("skill.saveFailed")).- The renderer's
I18nProvidersupplies the text context in every window. The web client and Storybook useStaticI18nProviderwith a fixed locale.
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:
// 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.
- 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.tsfile.
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.
tools/ui-foundation/no-hardcoded-ui-text.gritfails on literal text insrc/renderer/src,packages/ui/srcandapps/mobile/src: JSX text, a literalaria-label,title,placeholder,alt,label,description,accessibilityLabeloraccessibilityHint, a literal toast or alert, and a literalerrorMessagefallback. Tests and stories are not checked.bun run i18n:checkfails 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 --fixputs 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 --jsonwrites.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=<url>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,pland--stories=<part of an id>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.jsonand one screenshot for each failure that English does not have.- With a dev app open,
bun scripts/i18n-e2e.ts --allow-mutationsswitches 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 the BCP 47 tag to
APP_LANGUAGESinpackages/contracts/src/app-language.ts. Never rename a tag that shipped: a saved preference would read back as an unknown language. - Add it to
TRANSLATED_LOCALESinpackages/i18n/src/locale.ts. - Copy
packages/i18n/src/messages/fr/tomessages/<tag>/. Empty each module to{} as const satisfies PartialTranslation<typeof source>, and translate what you can. - Add the new catalogs to the
catalogsmaps inpackages/i18n/src/app-catalogs.ts,source-catalogs.ts,format-catalogs.tsandmobile-catalogs.ts, to theswitchinmobile-catalogs.native.ts, and to theloadersrecord inlazy-catalogs.ts. The public web build useslazy-catalogs.tsin place of the first three. The compiler shows each place that needs one. - Add the CLDR plural rule to
BUILT_IN_RULESinpackages/i18n/src/plural.ts. - Add the language's own name, and its
langtag, topackages/i18n/src/languages.ts. - Run
bun run typecheck:i18nandbun run i18n:check. With a dev app open, runbun 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.
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. Dutch uses the informal je 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 | 방향 지시 |
Dutch terms:
| English | Dutch |
|---|---|
| agent | agent |
| chat | chat |
| conversation | gesprek |
| channel | kanaal |
| server | server |
| host | host |
| skill | vaardigheid |
| routine | routine |
| workspace | werkruimte |
| provider | aanbieder |
| permissions | machtigingen |
| Settings | Instellingen |
| Computer Use | Computerbesturing |
| Steer | Bijsturen |
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 此.