OpenBitFun has one language contract across product surfaces, but each surface owns and loads only its own translation resources.
- Locale ids, aliases, fallback chains, and surface defaults are centralized.
- Web UI, mobile-web, installer, backend, and static pages do not import another surface's full locale catalog.
- Shared terms are only for stable cross-surface concepts: product names, feature names, modes, agent names, tool names, connection methods, and status labels.
- Page, workflow, toast, validation, and feature copy stays in the nearest surface or feature namespace.
- Audits distinguish structural safety, review inventory, and actionable l10n gates. Do not delete keys from quality, inventory, or dynamic-access findings unless usage is ruled out.
The canonical source is src/shared/i18n/contract/locales.json.
Run this after editing the contract or shared terms:
pnpm run i18n:generateGenerated outputs:
src/web-ui/src/infrastructure/i18n/presets/generatedLocaleContract.ts- The marked startup i18n block in
src/web-ui/index.html src/mobile-web/src/i18n/generatedLocaleContract.tsOpenBitFun-Installer/src/i18n/generatedLocaleContract.tssrc/crates/assembly/core/src/service/i18n/generated_locale_contract.rsOpenBitFun-Installer/src-tauri/src/installer/generated_locale_contract.rssrc/apps/relay-server/static/homepage/i18n.shared.json
Do not edit generated files manually.
The pre-React desktop splash reads a small, synchronous projection of Web UI
common.loading and common.window strings from the generated block in
index.html. The desktop host injects the saved locale; locale aliases and
default selection come from the shared contract. Run pnpm run i18n:generate
after changing these startup strings. The projection avoids loading full
catalogs or making an extra request before first paint.
| Surface | Resource owner | Loading rule |
|---|---|---|
| Shared terms | src/shared/i18n/resources/shared/<locale>/terms.json |
Generated into a small shared.* contract for each surface |
| Web UI | src/web-ui/src/locales/<locale>/**/*.json |
Eagerly loads bootstrap namespaces only; route and feature copy loads lazily |
| Mobile Web | src/mobile-web/src/i18n/messages.ts |
Owns mobile-only messages; no Web UI catalog imports |
| HarmonyOS phone | src/apps/mobile/harmonyos/entry/src/main/ets/i18n |
Owns ArkTS catalogs for zh-CN and en-US; no Web UI or mobile-web catalog imports |
| Installer | OpenBitFun-Installer/src/i18n/locales/*.json |
Owns installer-only messages; no app runtime catalog imports |
| Backend | src/crates/assembly/core/locales/*.ftl |
Uses generated locale helpers and Fluent resources |
| Relay static homepage | src/apps/relay-server/static/homepage |
Self-contained static resources plus generated page-scoped shared terms |
Shared contract does not mean shared bundle. Smaller product shapes must not pay for Web UI resource size.
The backend follows the same split at compile time. LocaleId, locale aliases,
fallback facts, metadata, and model-facing language copy are feature-free Core
contracts. Fluent parsing, backend bundles, I18nService, and its global mutable
state are enabled only by the Core i18n-runtime owner feature. A host that
initializes or updates the backend service must select that feature explicitly;
product-full and App Server do so, while contract-only Core consumers do not
compile Fluent.
The public agent label is shared.agents.Claw (Assistant / 助理 / 助理).
Claw, assistant IDs, workspace paths, command names, and existing translation
keys remain internal compatibility identifiers.
Core owns assistant instance names. An explicit IDENTITY.md name takes priority;
otherwise a proven generated name uses the shared label, with a stable number
from the second instance onward. The workspace catalog records
assistantNameSource and assistantNameNumber. A host-local, locked
personal_assistant/name-registry.json ledger allocates numbers across Desktop
and CLI processes and retains them across catalog rediscovery. The catalog's
optional assistant_name_sequence is a compatible high-water mark. Peer clients
receive optional defaultNameNumber for localization; older clients and remote
mobile/bot surfaces can keep displaying the host's resolved name.
Migration accepts only an exact legacy generated name (Claw or Claw <id>)
without an explicit name source or identity name. Arbitrary stored names and
explicit names containing Claw remain verbatim. Missing or unreadable identity
files retain the last known identity; a readable document with its name cleared
can restore the default.
All normal session modes use shared.sessions.new (New Session / 新会话 / 新會話).
Blank runtime creation names ask the owning host to choose this default and
record its i18n provenance. CLI sends the same default descriptor with a readable
fallback title for older Shared hosts. Mobile clients send a blank/absent name; an older host
may continue using its historical default until upgraded. User-provided names
remain text. Manual rename records titleUserEdited, including a rename to the
same visible default, so a delayed automatic title cannot overwrite it.
src/shared/session-naming/legacy-default-titles.json is the compatibility
allowlist shared by storage and Web UI. Only empty sessions with a known i18n
source/key and exactly matching text migrate. Untagged strings, edited titles,
and sessions containing turns remain unchanged. Storage reads project the
migration and the next normal save persists it; no bulk rewrite or record
removal is required. Web UI applies the same projection to older peer payloads.
Resource intent has three layers:
shared.*: stable concept labels.- Surface common namespaces: short UI atoms owned by that surface.
- Feature or route namespaces: workflow and scene-specific copy.
Specific copy wins over common copy; common copy wins over shared terms. Do not
configure shared as an implicit global fallback namespace. Use a shared key
directly for standalone labels, or pass its value into composed sentences as an
interpolation argument.
Web UI direct i18nService.t('namespace:key') calls are only for bootstrap
namespaces. Route and feature UI should use useI18n(namespace) or an explicit
namespace load at the render boundary.
JSON resources use dot-separated semantic keys with camelCase segments. Existing backend Fluent keys remain kebab-case until that owner is deliberately migrated.
Use feature ownership in the key path. Avoid abbreviated, positional, or
English-copy-shaped keys such as f1t, button1, or
areYouSureYouWantToDelete.
Allowed exceptions:
- i18next plural suffixes such as
_oneand_other. - Runtime enum or protocol mirrors reached by documented dynamic mappings.
- Existing backend Fluent keys.
pnpm run i18n:audit -- --report-json <path> emits the governance snapshot.
The categories have different meanings:
| Category | Meaning | Allowed action |
|---|---|---|
| Confirmed unused keys | No shipped surface can reach the key through known static or dynamic paths | Safe to delete after reviewing the evidence |
| Dynamic key candidates | Keys reached through metadata, enum/error-code mappings, fallback arrays, or factories | Keep source-proof allowlist entries current |
| Shared-term duplicates | Surface values matching shared.* terms |
Migrate only when the local value is the same stable concept |
| Same-text locale inventory | Broad zh-CN/zh-TW same-text Han pairs, including entries without a script or terminology signal | Review for copy quality; this inventory is non-blocking and is not deletion evidence |
| L10n quality candidates | Same-text locale pairs with script or terminology signals that need localization review | Translate or narrowly allowlist reviewed same-writing terms |
| Literal fallback candidates | Static UI copy in component fallback arguments or composed defaultValue expressions |
Add or repair locale resources instead |
| Locale-format candidates | User-visible formatting bypassing surface i18n helpers | Move formatting behind the surface i18n API |
Baselines:
scripts/i18n-governance-baseline.jsonis a no-growth guard for structural governance candidates and must be lowered when debt is removed.scripts/i18n-dynamic-key-allowlist.jsondocuments source-proven dynamic key access. Stale keys or stale source references fail audit.scripts/i18n-l10n-identical-allowlist.jsonis only for reviewed values that are intentionally identical across locale pairs.scripts/i18n-literal-fallback-baseline.jsonandscripts/i18n-locale-format-baseline.jsonshould stay empty.
The same-text inventory preserves broad review visibility. The l10n candidate check is intentionally a narrower signal, not a translation-quality oracle. Key parity and a clean audit do not replace human copy review.
All surfaces exchange canonical app locale ids:
zh-CNzh-TWen-US
Aliases such as zh, zh-Hans, zh-Hant, and en are accepted only at input
boundaries. Persisted config, API payloads, QR links, and backend state should
store canonical ids.
- Update
src/shared/i18n/contract/locales.json. - Add shared terms for every supported locale.
- Add resources in each surface that supports the locale.
- Run
pnpm run i18n:generateandpnpm run i18n:audit. - Run focused checks for touched runtime code.
- If a surface intentionally does not support the locale, document that decision in the contract-facing PR description.
Duration presentation uses src/shared/i18n/formatDuration.ts with each surface's number formatter. The policy carries rounded seconds into minutes and hours, while live timers truncate elapsed seconds. Web tools, model rounds and usage reports use the Web UI formatting wrapper; mobile uses its own i18n provider without importing Web UI catalogs. Protocol durations remain milliseconds.