Skip to content

Latest commit

 

History

History
278 lines (234 loc) · 14 KB

i18n.md

File metadata and controls

278 lines (234 loc) · 14 KB

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), 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.

Where the text lives

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.

  • 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.
  • 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:

    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/, nl/, 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:

const STATUS_LABEL = {
  running: "agent.status.running",
  stopped: "agent.status.stopped",
} as const satisfies Record<AgentStatus, AppTextKey>;

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:

// 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=<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,pl and --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.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/<tag>/. Empty each module to {} as const satisfies PartialTranslation<typeof source>, 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-catalogs.ts, to the switch in mobile-catalogs.native.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. 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 此.