# 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. 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/.properties`. The locale tag follows BCP 47: ``` src/main/resources/ └── i18n/ ├── en-US.properties ├── zh-CN.properties ├── de-DE.properties ├── ja-JP.properties └── es-ES.properties ``` The `plugin.yml` manifest lists the bundles so the loader knows to pick them up: ```yaml metadata: i18n: - i18n/en-US.properties - i18n/de-DE.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 `./plugins/*.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. **Tenant overrides** stored in `metadata__translation` rows tagged with the tenant id. These are entered through the Tier 1 customization UI and let an integrator rewrite any string for any tenant without rebuilding anything. ``` tenant overrides ← highest precedence plug-in bundles core bundles ← lowest precedence ``` The merged `MessageSource` is per-locale and per-tenant. Switching the user's locale or switching tenants picks the right merged bundle without restarting anything. ## 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 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`, the tenant default, and the system default. ## 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-tenant and per-user, 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)