Skip to content

Latest commit

 

History

History
193 lines (149 loc) · 10 KB

i18n.md

File metadata and controls

193 lines (149 loc) · 10 KB

I18n Architecture

OpenBitFun has one language contract across product surfaces, but each surface owns and loads only its own translation resources.

Core Invariants

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

Locale Contract

The canonical source is src/shared/i18n/contract/locales.json.

Run this after editing the contract or shared terms:

pnpm run i18n:generate

Generated 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.ts
  • OpenBitFun-Installer/src/i18n/generatedLocaleContract.ts
  • src/crates/assembly/core/src/service/i18n/generated_locale_contract.rs
  • OpenBitFun-Installer/src-tauri/src/installer/generated_locale_contract.rs
  • src/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.

Resource Ownership

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.

Assistant Names And Session Titles

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.

Lookup And Override Order

Resource intent has three layers:

  1. shared.*: stable concept labels.
  2. Surface common namespaces: short UI atoms owned by that surface.
  3. 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.

Key Policy

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 _one and _other.
  • Runtime enum or protocol mirrors reached by documented dynamic mappings.
  • Existing backend Fluent keys.

Audit And Governance

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.json is a no-growth guard for structural governance candidates and must be lowered when debt is removed.
  • scripts/i18n-dynamic-key-allowlist.json documents source-proven dynamic key access. Stale keys or stale source references fail audit.
  • scripts/i18n-l10n-identical-allowlist.json is only for reviewed values that are intentionally identical across locale pairs.
  • scripts/i18n-literal-fallback-baseline.json and scripts/i18n-locale-format-baseline.json should 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.

Language Contract

All surfaces exchange canonical app locale ids:

  • zh-CN
  • zh-TW
  • en-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.

Adding A Locale Or Surface

  1. Update src/shared/i18n/contract/locales.json.
  2. Add shared terms for every supported locale.
  3. Add resources in each surface that supports the locale.
  4. Run pnpm run i18n:generate and pnpm run i18n:audit.
  5. Run focused checks for touched runtime code.
  6. 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.