# i18n guide vibe_erp is built to be sold worldwide. There are no hard-coded user-facing strings, currencies, date formats, time zones, number formats, address shapes, or tax models in the framework — everything user-facing flows through the i18n and formatting layers from day one. This is guardrail #6 in `CLAUDE.md`, and it applies to the core, every PBC, and every plug-in. > **Status (current build).** The api.v1 contract for i18n (`Translator`, > `LocaleProvider`, `MessageKey`) is locked in and present. The host > implementation backed by ICU4J is **P1.6** in the implementation plan > and is **not yet wired** — `context.translator` currently throws > `UnsupportedOperationException`. This guide describes the design that > P1.6 will deliver. See [`../../PROGRESS.md`](../../PROGRESS.md) for > the live status of every feature. For the architectural placement of i18n, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). ## ICU MessageFormat is the backbone vibe_erp uses **ICU MessageFormat** (via ICU4J) on top of Spring's `MessageSource`. ICU MessageFormat handles the things naive `printf`-style formatting cannot: - Plurals (`{count, plural, one {# item} other {# items}}`) - Gender selection - Locale-aware number, date, and currency formatting - Nested arguments and select clauses A message in a bundle file looks like this: ```properties orders_sales.cart.summary = {count, plural, =0 {Your cart is empty.} one {# item, total {total, number, currency}} other {# items, total {total, number, currency}}} ``` The same key produces correct output in English, German, Japanese, Chinese, and Spanish without changing a line of calling code. ## Plug-ins ship message bundles Each plug-in (and each PBC) ships its message bundles inside its JAR under `i18n/messages_.properties`. The locale tag follows BCP 47: ``` src/main/resources/ └── i18n/ ├── messages_en-US.properties ├── messages_zh-CN.properties ├── messages_de-DE.properties ├── messages_ja-JP.properties └── messages_es-ES.properties ``` The `plugin.yml` manifest lists the bundle paths under its flat `metadata:` array so the loader knows to pick them up: ```yaml metadata: - i18n/messages_en-US.properties - i18n/messages_zh-CN.properties ``` ## How the host merges bundles On boot, and on every plug-in install, the host builds a merged `MessageSource` per locale. The merge order, from lowest precedence to highest: 1. **Core bundles** shipped inside the vibe_erp image. 2. **Plug-in bundles** picked up from each plug-in's JAR in plug-in load order. A plug-in may override a core key with the same key in its own bundle, but is not encouraged to. 3. **Operator overrides** stored as `metadata__translation` rows tagged `source = 'user'`. These will be entered through the Tier 1 customization UI (P3.x) and let an integrator rewrite any string for the instance without rebuilding anything. ``` operator overrides ← highest precedence plug-in bundles core bundles ← lowest precedence ``` The merged `MessageSource` is per-locale. Switching the user's locale picks the right merged bundle without restarting anything. (vibe_erp is single-tenant per instance, so there are no per-tenant bundles — every user of the same instance shares the same merge result for a given locale.) ## Shipping locales for v1.0 vibe_erp v1.0 ships with five locales: | Locale tag | Language | |---|---| | `en-US` | English (United States) | | `zh-CN` | Chinese (Simplified) | | `de-DE` | German | | `ja-JP` | Japanese | | `es-ES` | Spanish (Spain) | Adding a sixth locale (once P1.6 lands) is a Tier 1 operation: drop a `.properties` file into `i18n-overrides/` on the mounted volume, or add `metadata__translation` rows through the customization UI. No rebuild. The reference business documentation under `raw/业务流程设计文档/` is in Chinese. **That does not make Chinese the default.** Chinese is one supported locale among the five. ## Translation key naming Translation keys follow `..`: ``` identity.login.title identity.login.error.invalid_credentials catalog.item.field.sku.label orders_sales.order.status.confirmed plugin_printingshop.plate.field.thickness.label ``` Rules: - The first segment is the PBC table prefix (`identity`, `catalog`, `orders_sales`, …) or, for a plug-in, `plugin_`. This is the same prefix used in the database, the metadata `source` column, and the OpenAPI tag. - Segments are `snake_case`. - The last segment names the message itself, not the widget that renders it. - Two plug-ins can never collide: their first segment differs by definition. ## The `Translator` is the only sanctioned way The host injects a `Translator` (from `org.vibeerp.api.v1.i18n`) into every plug-in endpoint, every workflow task handler, every event listener. **There is no string concatenation in user-facing code, anywhere, ever.** ```kotlin val message = translator.format( MessageKey("orders_sales.order.created.notification"), mapOf( "orderNumber" to order.number, "customerName" to order.customer.name, "total" to order.total, ), ) ``` What this means in practice: - A reviewer who sees `"Order " + number + " was created"` in a PR rejects the PR. - A reviewer who sees a `String.format` against a hard-coded format string rejects the PR. - A reviewer who sees `if (locale == "en") "..." else "..."` rejects the PR with prejudice. The `Translator` is locale-aware: it picks up the request's resolved locale from the `LocaleProvider`, which in turn resolves from (in order) the user's profile preference, the request's `Accept-Language`, and the instance default configured in `vibeerp.i18n.default-locale`. ## PII, dates, numbers, currency Locale handling does not stop at strings: - **Dates and times** are formatted through the locale, not through hard-coded patterns. The host exposes locale-aware formatters; plug-ins use them. - **Numbers** use the locale's grouping and decimal separators. - **Currency** values are `Money` (from `api.v1.core`) — an amount plus an ISO 4217 code — and the formatter renders them in the user's locale (e.g. `$1,234.56` vs. `1.234,56 €` vs. `¥1,235`). - **Time zones** are per-user (with an instance default configured in `vibeerp.instance.*`), not per-server. - **PII fields** are tagged in the field metadata. Tagged fields drive auto-generated DSAR exports and erasure jobs (GDPR Articles 15/17), and the audit log records access to them when audit-strict mode is on. - **Address shapes** are not assumed. There is no "state/province" or "ZIP code" hard-coded in the core; address fields are configurable via metadata so a Japanese address can have prefecture/city/ward and a French address can have arrondissement. ## Where to go next - Plug-in author walkthrough that uses `Translator`: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) - Plug-in API surface for i18n: [`../plugin-api/overview.md`](../plugin-api/overview.md)