-
The eighth cross-cutting platform service is live: plug-ins and PBCs now have a real Translator and LocaleProvider instead of the UnsupportedOperationException stubs that have shipped since v0.5. What landed ----------- * New Gradle subproject `platform/platform-i18n` (13 modules total). * `IcuTranslator` — backed by ICU4J's MessageFormat (named placeholders, plurals, gender, locale-aware number/date/currency formatting), the format every modern translation tool speaks natively. JDK ResourceBundle handles per-locale fallback (zh_CN → zh → root). * The translator takes a list of `BundleLocation(classLoader, baseName)` pairs and tries them in order. For the **core** translator the chain is just `[(host, "messages")]`; for a **per-plug-in** translator constructed by VibeErpPluginManager it's `[(pluginClassLoader, "META-INF/vibe-erp/i18n/messages"), (host, "messages")]` so plug-in keys override host keys, but plug-ins still inherit shared keys like `errors.not_found`. * Critical detail: the plug-in baseName uses a path the host does NOT publish, because PF4J's `PluginClassLoader` is parent-first — a `getResource("messages.properties")` against the plug-in classloader would find the HOST bundle through the parent chain, defeating the per-plug-in override entirely. Naming the plug-in resource somewhere the host doesn't claim sidesteps the trap. * The translator disables `ResourceBundle.Control`'s automatic JVM-default locale fallback. The default control walks `requested → root → JVM default → root` which would silently serve German strings to a Japanese-locale request just because the German bundle exists. The fallback chain stops at root within a bundle, then moves to the next bundle location, then returns the key string itself. * `RequestLocaleProvider` reads the active HTTP request's Accept-Language via `RequestContextHolder` + the servlet container's `getLocale()`. Outside an HTTP request (background jobs, workflow tasks, MCP agents) it falls back to the configured default locale (`vibeerp.i18n.defaultLocale`, default `en`). Importantly, when an HTTP request HAS no Accept-Language header it ALSO falls back to the configured default — never to the JVM's locale. * `I18nConfiguration` exposes `coreTranslator` and `coreLocaleProvider` beans. Per-plug-in translators are NOT beans — they're constructed imperatively per plug-in start in VibeErpPluginManager because each needs its own classloader at the front of the resolution chain. * `DefaultPluginContext` now wires `translator` and `localeProvider` for real instead of throwing `UnsupportedOperationException`. Bundles ------- * Core: `platform-i18n/src/main/resources/messages.properties` (English), `messages_zh_CN.properties` (Simplified Chinese), `messages_de.properties` (German). Six common keys (errors, ok/cancel/save/delete) and an ICU plural example for `counts.items`. Java 9+ JEP 226 reads .properties files as UTF-8 by default, so Chinese characters are written directly rather than as `\\uXXXX` escapes. * Reference plug-in: moved from the broken `i18n/messages_en-US.properties` / `messages_zh-CN.properties` (wrong path, hyphen-locale filenames ResourceBundle ignores) to the canonical `META-INF/vibe-erp/i18n/messages.properties` / `messages_zh_CN.properties` paths with underscore locale tags. Added a new `printingshop.plate.created` key with an ICU plural for `ink_count` to demonstrate non-trivial argument substitution. End-to-end smoke test --------------------- Reset Postgres, booted the app, hit POST /api/v1/plugins/printing-shop/plates with three different Accept-Language headers: * (no header) → "Plate 'PLATE-001' created with no inks." (en-US, plug-in base bundle) * `Accept-Language: zh-CN` → "已创建印版 'PLATE-002' (无油墨)。" (zh-CN, plug-in zh_CN bundle) * `Accept-Language: de` → "Plate 'PLATE-003' created with no inks." (de, but the plug-in ships no German bundle so it falls back to the plug-in base bundle — correct, the key is plug-in-specific) Regression: identity, catalog, partners, and `GET /plates` all still HTTP 200 after the i18n wiring change. Build ----- * `./gradlew build`: 13 subprojects, 118 unit tests (was 107 / 12), all green. The 11 new tests cover ICU plural rendering, named-arg substitution, locale fallback (zh_CN → root, ja → root via NO_FALLBACK), cross-classloader override (a real JAR built in /tmp at test time), and RequestLocaleProvider's three resolution paths (no request → default; Accept-Language present → request locale; request without Accept-Language → default, NOT JVM locale). * The architectural rule still enforced: platform-plugins now imports platform-i18n, which is a platform-* dependency (allowed), not a pbc-* dependency (forbidden). What was deferred ----------------- * User-preferred locale from the authenticated user's profile row is NOT in the resolution chain yet — the `LocaleProvider` interface leaves room for it but the implementation only consults Accept-Language and the configured default. Adding it slots in between request and default without changing the api.v1 surface. * The metadata translation overrides table (`metadata__translation`) is also deferred — the `Translator` JavaDoc mentions it as the first lookup source, but right now keys come from .properties files only. Once Tier 1 customisation lands (P3.x), key users will be able to override any string from the SPA without touching code. -
The previous commit shipped with `<this commit>` as a placeholder since the SHA wasn't known yet. Pin it now.
-
The third real PBC. Validates the modular-monolith template against a parent-with-children aggregate (Partner → Addresses → Contacts), where the previous two PBCs only had single-table or two-independent-table shapes. What landed ----------- * New Gradle subproject `pbc/pbc-partners` (12 modules total now). * Three JPA entities, all extending `AuditedJpaEntity`: - `Partner` — code, name, type (CUSTOMER/SUPPLIER/BOTH), tax_id, website, email, phone, active, ext jsonb. Single-table for both customers and suppliers because the role flag is a property of the relationship, not the organisation. - `Address` — partner_id FK, address_type (BILLING/SHIPPING/OTHER), line1/line2/city/region/postal_code/country_code (ISO 3166-1), is_primary. Two free address lines + structured city/region/code is the smallest set that round-trips through every postal system. - `Contact` — partner_id FK, full_name, role, email, phone, active. PII-tagged in metadata YAML for the future audit/export tooling. * Spring Data JPA repos, application services with full CRUD and the invariants below, REST controllers under `/api/v1/partners/partners` (+ nested addresses, contacts). * `partners-init.xml` Liquibase changelog with the three tables, FKs, GIN index on `partner.ext`, indexes on type/active/country. * New api.v1 facade `org.vibeerp.api.v1.ext.partners` with `PartnersApi` + `PartnerRef`. Third `ext.<pbc>` after identity and catalog. Inactive partners hidden at the facade boundary. * `PartnersApiAdapter` runtime implementation in pbc-partners, never leaking JPA entity types. * `partners.yml` metadata declaring all 3 entities, 12 permission keys, 1 menu entry. Picked up automatically by `MetadataLoader`. * 15 new unit tests across `PartnerServiceTest`, `AddressServiceTest` and `ContactServiceTest` (mockk-based, mirroring catalog tests). Invariants enforced in code (not blindly delegated to the DB) ------------------------------------------------------------- * Partner code uniqueness — explicit check produces a 400 with a real message instead of a 500 from the unique-index violation. * Partner code is NOT updatable — every external reference uses code, so renaming is a data-migration concern, not an API call. * Partner deactivate cascades to contacts (also flipped to inactive). Addresses are NOT touched (no `active` column — they exist or they don't). Verified end-to-end against Postgres. * "Primary" flag is at most one per (partner, address_type). When a new/updated address is marked primary, all OTHER primaries of the same type for the same partner are demoted in the same transaction. * Addresses and contacts reject operations on unknown partners up-front to give better errors than the FK-violation. End-to-end smoke test --------------------- Reset Postgres, booted the app, hit: * POST /api/v1/auth/login (admin) → JWT * POST /api/v1/partners/partners (CUSTOMER, SUPPLIER) → 201 * GET /api/v1/partners/partners → lists both * GET /api/v1/partners/partners/by-code/CUST-ACME → resolves * POST /api/v1/partners/partners (dup code) → 400 with real message * POST .../{id}/addresses (BILLING, primary) → 201 * POST .../{id}/contacts → 201 * DELETE /api/v1/partners/partners/{id} → 204; partner active=false * GET .../contacts → contact ALSO active=false (cascade verified) * GET /api/v1/_meta/metadata/entities → 3 partners entities present * GET /api/v1/_meta/metadata/permissions → 12 partners permissions * Regression: catalog UoMs/items, identity users, printing-shop plug-in plates all still HTTP 200. Build ----- * `./gradlew build`: 12 subprojects, 107 unit tests, all green (was 11 / 92 before this commit). * The architectural rule still enforced: pbc-partners depends on api-v1 + platform-persistence + platform-security only — no cross-PBC dep, no platform-bootstrap dep. What was deferred ----------------- * Permission enforcement on contact endpoints (P4.3). Currently plain authenticated; the metadata declares the planned `partners.contact.*` keys for when @RequirePermission lands. * Per-country address structure layered on top via metadata forms (P3.x). The current schema is the smallest universal subset. * `deletePartnerCompletely` — out of scope for v1; should be a separate "data scrub" admin tool, not a routine API call. -
The README and CLAUDE.md "Repository state" sections were both stale — they still claimed "v0.1 skeleton, one PBC implemented" and "no source code yet" when in reality the framework is at v0.6+P1.5 with 92 unit tests, 11 modules, 7 cross-cutting platform services live, 2 PBCs, and a real customer-style plug-in serving HTTP from its own DB tables. What landed: * New PROGRESS.md at the repo root. Single-page progress tracker that enumerates every implementation-plan unit (P1.x, P2.x, P3.x, P4.x, P5.x, R, REF.x, H/A/M.x) with status badges, commit refs for the done units, and the "next priority" call. Includes a snapshot table (modules / tests / PBCs / plug-ins / cross-cutting services), a "what's live right now" table per service, a "what the reference plug-in proves end-to-end" walkthrough, the "what's not yet live" deferred list, and a "how to run what exists today" runbook. * README.md "Building" + "Status" rewritten. Drops the obsolete "v0.1 skeleton" claim. New status table shows current counts. Adds the dev workflow (`installToDev` → `bootRun`) and points at PROGRESS.md for the per-feature view. * CLAUDE.md "Repository state" section rewritten from "no source code, build system, package manifest, test suite, or CI yet" to the actual current state: 11 subprojects, 92 tests, 7 services live, 2 PBCs, build commands, package root, and a pointer at PROGRESS.md. * docs/customer-onboarding/guide.md, docs/workflow-authoring/guide.md, docs/form-authoring/guide.md: replaced "v0.1: API only" annotations with "current: API only". The version label was conflating "the v0.1 skeleton" with "the current build" — accurate in spirit (the UI layer still hasn't shipped) but misleading when readers see the framework is on commit 18, not commit 1. Pointed each guide at PROGRESS.md for live status. Build: ./gradlew build still green (no source touched, but verified that nothing in the docs change broke anything). The deeper how-to docs (architecture/overview.md, plugin-api/overview.md, plugin-author/getting-started.md, i18n/guide.md, docs/index.md) were left as-is. They describe HOW the framework is supposed to work architecturally, and the architecture has not changed. Only the status / version / scope statements needed updating.