From 9d618b44745e9b317cb6f3eadeb3ae5f9a5f1046 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 03:37:17 +0000 Subject: [PATCH 1/7] Initial plan From 36a50429dfd3f86c1f8d58b7d3018d1477d7d55f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 03:42:52 +0000 Subject: [PATCH 2/7] fix(i18n): return flat translations dict from kernel service getTranslations The createKernel i18n service's getTranslations was returning { locale, translations } instead of the flat Record expected by the spec II18nService interface. The HttpDispatcher wraps the return in { data: { locale, translations } }, so the extra layer caused /api/v1/i18n/translations/:lang to return empty translations. Now returns resolveI18nTranslations() directly. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/a9fb6c1a-aac9-485a-afb5-952077e8f771 --- CHANGELOG.md | 2 ++ apps/console/src/mocks/createKernel.ts | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d011fffd..3fc22a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **i18n Kernel Service `getTranslations` Returns Wrong Format** (`apps/console`): Fixed the `createKernel` i18n service registration where `getTranslations` returned a wrapped `{ locale, translations }` object instead of the flat `Record` dictionary expected by the spec's `II18nService` interface. The HttpDispatcher wraps the service return value in `{ data: { locale, translations } }`, so the extra wrapping caused translations to be empty or double-nested in the API response (`/api/v1/i18n/translations/:lang`). The service now returns `resolveI18nTranslations(bundles, lang)` directly, aligning with the spec and making CRM business translations work correctly in all environments (MSW, server, dev). + - **Duplicate Data in Calendar and Kanban Views** (`@object-ui/plugin-view`, `@object-ui/plugin-kanban`, `@object-ui/plugin-list`): Fixed a bug where Calendar and Kanban views displayed every record twice. The root cause was that `ObjectView` (plugin-view) unconditionally fetched data even when a `renderListView` callback was provided — causing parallel duplicate requests since `ListView` (plugin-list) independently fetches its own data. The duplicate fetch results triggered re-renders that destabilised child component rendering, leading to duplicate events in the calendar and duplicate cards on the kanban board. Additionally, `ObjectKanban` lacked proper external-data handling (`data`/`loading` props with `hasExternalData` guard), unlike `ObjectCalendar` which already had this pattern. Fixes: (1) `ObjectView` now skips its own fetch when `renderListView` is provided, (2) `ObjectKanban` now accepts explicit `data`/`loading` props and skips internal fetch when external data is supplied (matching `ObjectCalendar`'s pattern), (3) `ListView` now handles `{ value: [] }` OData response format consistently with `extractRecords` utility. Includes regression tests. - **CI Build Fix: Replace dynamic `require()` with static imports** (`@object-ui/plugin-view`): Replaced module-level `require('@object-ui/react')` calls in `index.tsx` and `ObjectView.tsx` with static `import` statements. The dynamic `require()` pattern is not supported by Next.js Turbopack, causing the docs site build to fail during SSR prerendering of the `/docs/components/complex/view-switcher` page with `Error: dynamic usage of require is not supported`. Since `@object-ui/react` is already a declared dependency and other files in the package use static imports from it, replacing the `require()` with static imports is safe and eliminates the SSR compatibility issue. diff --git a/apps/console/src/mocks/createKernel.ts b/apps/console/src/mocks/createKernel.ts index a6d7898f..cb81efea 100644 --- a/apps/console/src/mocks/createKernel.ts +++ b/apps/console/src/mocks/createKernel.ts @@ -333,10 +333,7 @@ export async function createKernel(options: KernelOptions): Promise 0) { kernel.registerService('i18n', { - getTranslations: (lang: string) => ({ - locale: lang, - translations: resolveI18nTranslations(i18nBundles, lang), - }), + getTranslations: (lang: string) => resolveI18nTranslations(i18nBundles, lang), }); } From 3d034e238b2430dcd8e6aa10a3a2ecc6c7d6a8f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 04:45:30 +0000 Subject: [PATCH 3/7] fix(i18n): return flat translations dict from kernel service + regression tests The createKernel i18n service's getTranslations returned { locale, translations } instead of the flat Record expected by the spec II18nService interface. The HttpDispatcher wraps the return in { data: { locale, translations } }, so the extra layer caused /api/v1/i18n/translations/:lang to return empty or double-nested translations. Now returns resolveI18nTranslations() directly. Added 6 regression tests covering: kernel service, HttpDispatcher, and MSW handler. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/9e014d2c-cb4d-4779-8774-216d48126f73 --- .../src/__tests__/i18n-translations.test.ts | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 apps/console/src/__tests__/i18n-translations.test.ts diff --git a/apps/console/src/__tests__/i18n-translations.test.ts b/apps/console/src/__tests__/i18n-translations.test.ts new file mode 100644 index 00000000..112166d6 --- /dev/null +++ b/apps/console/src/__tests__/i18n-translations.test.ts @@ -0,0 +1,114 @@ +/** + * i18n Translations Pipeline Tests + * + * Validates that the i18n translation pipeline delivers CRM translations + * correctly through all code paths: + * - Kernel service: getTranslations returns flat dict (not wrapped) + * - HttpDispatcher: wraps into standard spec envelope + * - MSW handler: returns correct { data: { locale, translations } } + * + * Regression test for empty translations:{} response. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { setupServer } from 'msw/node'; +import { createKernel, type KernelResult } from '../mocks/createKernel'; +import { createAuthHandlers } from '../mocks/authHandlers'; +import appConfig from '../../objectstack.shared'; +import { crmLocales } from '@object-ui/example-crm'; + +// Expected values from the CRM i18n bundles — avoid hard-coding in assertions +const EXPECTED_ZH_ACCOUNT_LABEL = crmLocales.zh.objects.account.label; +const EXPECTED_EN_ACCOUNT_LABEL = crmLocales.en.objects.account.label; + +describe('i18n translations pipeline', () => { + let result: KernelResult; + let server: ReturnType; + + beforeAll(async () => { + result = await createKernel({ + appConfig, + persistence: false, + mswOptions: { + enableBrowser: false, + baseUrl: '/api/v1', + logRequests: false, + customHandlers: [ + ...createAuthHandlers('/api/v1/auth'), + ], + }, + }); + + const handlers = result.mswPlugin?.getHandlers() ?? []; + server = setupServer(...handlers); + server.listen({ onUnhandledRequest: 'bypass' }); + }); + + afterAll(() => { + server?.close(); + }); + + // ── Kernel service layer ─────────────────────────────────────────── + + it('kernel i18n service returns flat translations dict (not wrapped)', () => { + const i18nService = result.kernel.getService('i18n'); + const translations = i18nService.getTranslations('zh'); + + // Must NOT have the { locale, translations } wrapper + expect(translations).not.toHaveProperty('locale'); + expect(translations).toHaveProperty('crm'); + expect(translations.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL); + }); + + it('kernel i18n service returns English translations', () => { + const i18nService = result.kernel.getService('i18n'); + const translations = i18nService.getTranslations('en'); + + expect(translations.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL); + }); + + it('kernel i18n service returns empty for unknown locale', () => { + const i18nService = result.kernel.getService('i18n'); + const translations = i18nService.getTranslations('xx'); + + expect(Object.keys(translations)).toHaveLength(0); + }); + + // ── HttpDispatcher layer ─────────────────────────────────────────── + + it('HttpDispatcher returns populated translations in spec envelope', async () => { + const { HttpDispatcher } = await import('@objectstack/runtime'); + const dispatcher = new HttpDispatcher(result.kernel); + + const dispatchResult = await dispatcher.handleI18n('/translations/zh', 'GET', {}, {}); + + expect(dispatchResult.handled).toBe(true); + expect(dispatchResult.response?.status).toBe(200); + const body = dispatchResult.response?.body; + expect(body?.success).toBe(true); + expect(body?.data?.locale).toBe('zh'); + expect(Object.keys(body?.data?.translations ?? {}).length).toBeGreaterThan(0); + expect(body?.data?.translations?.crm?.objects?.account?.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL); + }); + + // ── MSW handler layer (fetch) ────────────────────────────────────── + + it('GET /api/v1/i18n/translations/zh returns CRM zh translations', async () => { + const res = await fetch('http://localhost/api/v1/i18n/translations/zh'); + expect(res.ok).toBe(true); + + const json = await res.json(); + const translations = json?.data?.translations; + + expect(translations).toBeDefined(); + expect(Object.keys(translations).length).toBeGreaterThan(0); + expect(translations.crm).toBeDefined(); + expect(translations.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL); + }); + + it('GET /api/v1/i18n/translations/en returns CRM en translations', async () => { + const res = await fetch('http://localhost/api/v1/i18n/translations/en'); + const json = await res.json(); + + expect(json?.data?.translations?.crm?.objects?.account?.label).toBe(EXPECTED_EN_ACCOUNT_LABEL); + }); +}); From 0bee120dd466743cc0c1b1460cbe89360dceb070 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 05:06:36 +0000 Subject: [PATCH 4/7] fix(i18n): add spec-format translations array for server mode + full i18n service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real ObjectStack server (pnpm start) uses AppPlugin.loadTranslations() which reads from a top-level `translations` array, not `i18n.bundles`. Changes: - objectstack.shared.ts: add `translations` array in spec format (each entry maps locale → namespace-scoped data) so AppPlugin can load them into the kernel's in-memory i18n fallback - createKernel.ts: expand i18n service with loadTranslations(), getLocales(), getDefaultLocale(), setDefaultLocale() methods required by AppPlugin and HttpDispatcher - i18n-translations.test.ts: add 3 server-mode compatibility tests Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/5581f722-7675-404d-9a1b-a025dca6484a --- CHANGELOG.md | 2 + apps/console/objectstack.shared.ts | 19 +++++++++ .../src/__tests__/i18n-translations.test.ts | 40 ++++++++++++++++++- apps/console/src/mocks/createKernel.ts | 37 ++++++++++++++++- 4 files changed, 96 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc22a39..cc36bbcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **i18n Kernel Service `getTranslations` Returns Wrong Format** (`apps/console`): Fixed the `createKernel` i18n service registration where `getTranslations` returned a wrapped `{ locale, translations }` object instead of the flat `Record` dictionary expected by the spec's `II18nService` interface. The HttpDispatcher wraps the service return value in `{ data: { locale, translations } }`, so the extra wrapping caused translations to be empty or double-nested in the API response (`/api/v1/i18n/translations/:lang`). The service now returns `resolveI18nTranslations(bundles, lang)` directly, aligning with the spec and making CRM business translations work correctly in all environments (MSW, server, dev). +- **i18n Translations Empty in Server Mode (`pnpm start`)** (`apps/console`): Fixed translations returning `{}` when running the real ObjectStack server on port 3000. The runtime's `AppPlugin.loadTranslations()` looks for translations in a top-level `translations` array in the config, but the console's `sharedConfig` only provided them in the custom `i18n.bundles` format used by `createKernel.ts`. Added a `translations` array in the spec-expected format (each entry maps `{ [locale]: { namespace: data } }`) to `objectstack.shared.ts` so that `AppPlugin` can load them into the kernel's in-memory i18n fallback. Also expanded the i18n service registration in `createKernel.ts` to implement `loadTranslations()`, `getLocales()`, `getDefaultLocale()`, and `setDefaultLocale()` — methods required by both `AppPlugin` and `HttpDispatcher`. + - **Duplicate Data in Calendar and Kanban Views** (`@object-ui/plugin-view`, `@object-ui/plugin-kanban`, `@object-ui/plugin-list`): Fixed a bug where Calendar and Kanban views displayed every record twice. The root cause was that `ObjectView` (plugin-view) unconditionally fetched data even when a `renderListView` callback was provided — causing parallel duplicate requests since `ListView` (plugin-list) independently fetches its own data. The duplicate fetch results triggered re-renders that destabilised child component rendering, leading to duplicate events in the calendar and duplicate cards on the kanban board. Additionally, `ObjectKanban` lacked proper external-data handling (`data`/`loading` props with `hasExternalData` guard), unlike `ObjectCalendar` which already had this pattern. Fixes: (1) `ObjectView` now skips its own fetch when `renderListView` is provided, (2) `ObjectKanban` now accepts explicit `data`/`loading` props and skips internal fetch when external data is supplied (matching `ObjectCalendar`'s pattern), (3) `ListView` now handles `{ value: [] }` OData response format consistently with `extractRecords` utility. Includes regression tests. - **CI Build Fix: Replace dynamic `require()` with static imports** (`@object-ui/plugin-view`): Replaced module-level `require('@object-ui/react')` calls in `index.tsx` and `ObjectView.tsx` with static `import` statements. The dynamic `require()` pattern is not supported by Next.js Turbopack, causing the docs site build to fail during SSR prerendering of the `/docs/components/complex/view-switcher` page with `Error: dynamic usage of require is not supported`. Since `@object-ui/react` is already a declared dependency and other files in the package use static imports from it, replacing the `require()` with static imports is safe and eliminates the SSR compatibility issue. diff --git a/apps/console/objectstack.shared.ts b/apps/console/objectstack.shared.ts index eadb5c80..92b1e738 100644 --- a/apps/console/objectstack.shared.ts +++ b/apps/console/objectstack.shared.ts @@ -30,6 +30,19 @@ const i18nBundles = allConfigs .map((c: any) => c.i18n) .filter((i: any) => i?.namespace && i?.translations); +// Build the spec `translations` array for the runtime's AppPlugin. +// AppPlugin.loadTranslations expects `translations: Array<{ [locale]: data }>`. +// Each locale's data is nested under the bundle's namespace so that +// both the server-mode (AppPlugin → memory i18n) and MSW-mode (createKernel) +// produce the same structure: `{ crm: { objects: { ... } } }`. +const specTranslations: Record[] = i18nBundles.map((bundle: any) => { + const result: Record = {}; + for (const [locale, data] of Object.entries(bundle.translations)) { + result[locale] = { [bundle.namespace]: data }; + } + return result; +}); + // Protocol-level composition via @objectstack/spec: handles object dedup, // array concatenation, actions→objects mapping, and manifest selection. const composed = composeStacks(allConfigs as any[], { objectConflict: 'override' }) as any; @@ -97,7 +110,13 @@ export const sharedConfig = { }, i18n: { bundles: i18nBundles, + defaultLocale: 'en', }, + // Spec-format translations array consumed by AppPlugin.loadTranslations() + // in real-server mode (pnpm start). Each entry maps locale → namespace-scoped + // translation data so the runtime's memory i18n fallback serves the same + // structure as the MSW mock handler. + translations: specTranslations, plugins: [], datasources: [ { diff --git a/apps/console/src/__tests__/i18n-translations.test.ts b/apps/console/src/__tests__/i18n-translations.test.ts index 112166d6..93a5416f 100644 --- a/apps/console/src/__tests__/i18n-translations.test.ts +++ b/apps/console/src/__tests__/i18n-translations.test.ts @@ -111,4 +111,42 @@ describe('i18n translations pipeline', () => { expect(json?.data?.translations?.crm?.objects?.account?.label).toBe(EXPECTED_EN_ACCOUNT_LABEL); }); -}); + + // ── Server-mode compatibility (AppPlugin.loadTranslations) ──────── + + it('kernel i18n service supports loadTranslations (AppPlugin compat)', () => { + const i18nService = result.kernel.getService('i18n'); + + // AppPlugin.loadTranslations calls these methods; they must exist + expect(typeof i18nService.loadTranslations).toBe('function'); + expect(typeof i18nService.getLocales).toBe('function'); + expect(typeof i18nService.getDefaultLocale).toBe('function'); + expect(typeof i18nService.setDefaultLocale).toBe('function'); + }); + + it('kernel i18n service getLocales returns all CRM locales', () => { + const i18nService = result.kernel.getService('i18n'); + const locales = i18nService.getLocales(); + + // CRM declares 10 locales: en, zh, ja, ko, de, fr, es, pt, ru, ar + expect(locales).toContain('en'); + expect(locales).toContain('zh'); + expect(locales.length).toBeGreaterThanOrEqual(10); + }); + + it('appConfig.translations is spec-format array for AppPlugin', () => { + const translations = (appConfig as any).translations; + + expect(Array.isArray(translations)).toBe(true); + expect(translations.length).toBeGreaterThan(0); + + // Each entry maps locale → namespace-scoped data + const first = translations[0]; + expect(first).toHaveProperty('zh'); + expect(first).toHaveProperty('en'); + // Data must be nested under namespace (e.g. 'crm') + expect(first.zh).toHaveProperty('crm'); + expect(first.zh.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL); + expect(first.en.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL); + }); +}); \ No newline at end of file diff --git a/apps/console/src/mocks/createKernel.ts b/apps/console/src/mocks/createKernel.ts index cb81efea..0e84f284 100644 --- a/apps/console/src/mocks/createKernel.ts +++ b/apps/console/src/mocks/createKernel.ts @@ -332,8 +332,43 @@ export async function createKernel(options: KernelOptions): Promise 0) { + // Build a complete i18n service that satisfies both: + // - HttpDispatcher.handleI18n (calls getTranslations, getLocales) + // - AppPlugin.loadTranslations (calls loadTranslations, setDefaultLocale) + // In MSW mode the custom handler serves translations directly, but the + // kernel service is still needed for the broker shim and dispatcher paths. + const appPluginTranslations = new Map>(); + let defaultLocale = 'en'; + kernel.registerService('i18n', { - getTranslations: (lang: string) => resolveI18nTranslations(i18nBundles, lang), + getTranslations: (lang: string) => { + const resolved = resolveI18nTranslations(i18nBundles, lang); + const extra = appPluginTranslations.get(lang); + if (extra) { + // Shallow merge: AppPlugin-loaded translations override bundle translations + return { ...resolved, ...extra }; + } + return resolved; + }, + loadTranslations: (locale: string, data: Record) => { + const existing = appPluginTranslations.get(locale) ?? {}; + appPluginTranslations.set(locale, { ...existing, ...data }); + }, + getLocales: () => { + // Collect all available locales from bundles + any loaded via loadTranslations + const locales = new Set(); + for (const bundle of i18nBundles) { + for (const lang of Object.keys(bundle.translations)) { + locales.add(lang); + } + } + for (const lang of appPluginTranslations.keys()) { + locales.add(lang); + } + return [...locales]; + }, + getDefaultLocale: () => defaultLocale, + setDefaultLocale: (locale: string) => { defaultLocale = locale; }, }); } From 7d89c1690c4ff82d6ca9270e9f5a9dd604aaa5c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:14:52 +0000 Subject: [PATCH 5/7] fix(i18n): add MemoryI18nPlugin to fix timing issue in server mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: In server mode (`pnpm start`), the kernel bootstrap sequence runs plugin starts BEFORE validateSystemRequirements(). The memory i18n fallback service is auto-registered in validateSystemRequirements() — AFTER AppPlugin.start() → loadTranslations() has already tried (and failed) to find the i18n service, silently skipping translation loading. Fix: Add a MemoryI18nPlugin to objectstack.config.ts that registers the in-memory i18n service during the init phase (Phase 1). Since ALL inits complete before ANY starts, the service is available when AppPlugin.start() runs in Phase 2. The plugin's name ('com.objectstack.service.i18n') matches the CLI's dedup check, preventing conflicts. Also added a server-mode simulation test that validates the full flow: createMemoryI18n → loadTranslations (AppPlugin-style) → getTranslations. All 746 console tests pass. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/8eb7854e-6a7b-4b91-bcca-6d28bff373a3 --- CHANGELOG.md | 2 +- apps/console/objectstack.config.ts | 31 ++++++++++++++++ .../src/__tests__/i18n-translations.test.ts | 37 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc36bbcf..ead64a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **i18n Kernel Service `getTranslations` Returns Wrong Format** (`apps/console`): Fixed the `createKernel` i18n service registration where `getTranslations` returned a wrapped `{ locale, translations }` object instead of the flat `Record` dictionary expected by the spec's `II18nService` interface. The HttpDispatcher wraps the service return value in `{ data: { locale, translations } }`, so the extra wrapping caused translations to be empty or double-nested in the API response (`/api/v1/i18n/translations/:lang`). The service now returns `resolveI18nTranslations(bundles, lang)` directly, aligning with the spec and making CRM business translations work correctly in all environments (MSW, server, dev). -- **i18n Translations Empty in Server Mode (`pnpm start`)** (`apps/console`): Fixed translations returning `{}` when running the real ObjectStack server on port 3000. The runtime's `AppPlugin.loadTranslations()` looks for translations in a top-level `translations` array in the config, but the console's `sharedConfig` only provided them in the custom `i18n.bundles` format used by `createKernel.ts`. Added a `translations` array in the spec-expected format (each entry maps `{ [locale]: { namespace: data } }`) to `objectstack.shared.ts` so that `AppPlugin` can load them into the kernel's in-memory i18n fallback. Also expanded the i18n service registration in `createKernel.ts` to implement `loadTranslations()`, `getLocales()`, `getDefaultLocale()`, and `setDefaultLocale()` — methods required by both `AppPlugin` and `HttpDispatcher`. +- **i18n Translations Empty in Server Mode (`pnpm start`)** (`apps/console`): Fixed translations returning `{}` when running the real ObjectStack server on port 3000. The root cause was a **timing issue in the kernel bootstrap sequence**: `AppPlugin.start()` → `loadTranslations()` runs during Phase 2 (start), but the kernel's memory i18n fallback is only auto-registered in `validateSystemRequirements()` which runs AFTER Phase 2 — so `loadTranslations()` found no i18n service and silently skipped loading. Fixed by adding a `MemoryI18nPlugin` to `objectstack.config.ts` that registers the in-memory i18n service during Phase 1 (init), ensuring it's available when `AppPlugin.start()` runs. Also added a spec-format `translations` array to `objectstack.shared.ts` so `AppPlugin.loadTranslations()` can iterate and load the CRM locale bundles. The plugin's name (`com.objectstack.service.i18n`) matches the CLI's deduplication check, preventing conflicts with `@objectstack/service-i18n` if installed later. - **Duplicate Data in Calendar and Kanban Views** (`@object-ui/plugin-view`, `@object-ui/plugin-kanban`, `@object-ui/plugin-list`): Fixed a bug where Calendar and Kanban views displayed every record twice. The root cause was that `ObjectView` (plugin-view) unconditionally fetched data even when a `renderListView` callback was provided — causing parallel duplicate requests since `ListView` (plugin-list) independently fetches its own data. The duplicate fetch results triggered re-renders that destabilised child component rendering, leading to duplicate events in the calendar and duplicate cards on the kanban board. Additionally, `ObjectKanban` lacked proper external-data handling (`data`/`loading` props with `hasExternalData` guard), unlike `ObjectCalendar` which already had this pattern. Fixes: (1) `ObjectView` now skips its own fetch when `renderListView` is provided, (2) `ObjectKanban` now accepts explicit `data`/`loading` props and skips internal fetch when external data is supplied (matching `ObjectCalendar`'s pattern), (3) `ListView` now handles `{ value: [] }` OData response format consistently with `extractRecords` utility. Includes regression tests. diff --git a/apps/console/objectstack.config.ts b/apps/console/objectstack.config.ts index 6dfe7c9e..767f1162 100644 --- a/apps/console/objectstack.config.ts +++ b/apps/console/objectstack.config.ts @@ -16,16 +16,47 @@ import * as HonoServerPluginPkg from '@objectstack/plugin-hono-server'; import * as DriverMemoryPkg from '@objectstack/driver-memory'; // @ts-ignore import * as RuntimePkg from '@objectstack/runtime'; +// @ts-ignore +import * as CorePkg from '@objectstack/core'; const MSWPlugin = MSWPluginPkg.MSWPlugin || (MSWPluginPkg as any).default?.MSWPlugin || (MSWPluginPkg as any).default; const ObjectQLPlugin = ObjectQLPluginPkg.ObjectQLPlugin || (ObjectQLPluginPkg as any).default?.ObjectQLPlugin || (ObjectQLPluginPkg as any).default; const InMemoryDriver = DriverMemoryPkg.InMemoryDriver || (DriverMemoryPkg as any).default?.InMemoryDriver || (DriverMemoryPkg as any).default; const DriverPlugin = RuntimePkg.DriverPlugin || (RuntimePkg as any).default?.DriverPlugin || (RuntimePkg as any).default; const HonoServerPlugin = HonoServerPluginPkg.HonoServerPlugin || (HonoServerPluginPkg as any).default?.HonoServerPlugin || (HonoServerPluginPkg as any).default; +const createMemoryI18n = CorePkg.createMemoryI18n || (CorePkg as any).default?.createMemoryI18n; import { ConsolePlugin } from './plugin'; +/** + * Lightweight plugin that registers the in-memory i18n service during the + * init phase. This is critical for server mode (`pnpm start`) because: + * + * 1. The CLI auto-registers AppPlugin(config) before config plugins. + * 2. During bootstrap, all plugin inits run first, then all starts. + * 3. AppPlugin.start() → loadTranslations() needs the i18n service. + * 4. The kernel's own memory fallback is registered in + * validateSystemRequirements() which runs AFTER all starts — too late. + * + * By providing the service during init, AppPlugin.start() finds it and + * loads the spec-format `translations` array from the config. + * + * Name matches the check in CLI's serve command so it won't attempt to + * duplicate-register I18nServicePlugin from @objectstack/service-i18n. + */ +class MemoryI18nPlugin { + readonly name = 'com.objectstack.service.i18n'; + readonly version = '1.0.0'; + readonly type = 'service' as const; + + init(ctx: any) { + const svc = createMemoryI18n(); + ctx.registerService('i18n', svc); + } +} + const plugins: any[] = [ + new MemoryI18nPlugin(), new ObjectQLPlugin(), new DriverPlugin(new InMemoryDriver(), 'memory'), new HonoServerPlugin({ port: 3000 }), diff --git a/apps/console/src/__tests__/i18n-translations.test.ts b/apps/console/src/__tests__/i18n-translations.test.ts index 93a5416f..91c2c91a 100644 --- a/apps/console/src/__tests__/i18n-translations.test.ts +++ b/apps/console/src/__tests__/i18n-translations.test.ts @@ -149,4 +149,41 @@ describe('i18n translations pipeline', () => { expect(first.zh.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL); expect(first.en.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL); }); + + // ── Server-mode flow simulation ─────────────────────────────────── + // Simulates the exact flow that `pnpm start` (CLI serve) uses: + // 1. createMemoryI18n() registers the i18n service + // 2. AppPlugin.loadTranslations() iterates config.translations + // 3. HttpDispatcher.handleI18n() calls getTranslations(locale) + + it('server-mode: memory i18n + AppPlugin loadTranslations produces populated response', async () => { + // Import the same createMemoryI18n used by the MemoryI18nPlugin in objectstack.config.ts + const { createMemoryI18n } = await import('@objectstack/core'); + const svc = createMemoryI18n(); + + // Simulate AppPlugin.loadTranslations() iterating the spec-format translations array + const translations = (appConfig as any).translations; + for (const bundle of translations) { + for (const [locale, data] of Object.entries(bundle)) { + if (data && typeof data === 'object') { + svc.loadTranslations(locale, data as Record); + } + } + } + + // After loading, getTranslations must return populated CRM data + const zh = svc.getTranslations('zh'); + expect(zh).toHaveProperty('crm'); + expect(zh.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL); + + const en = svc.getTranslations('en'); + expect(en).toHaveProperty('crm'); + expect(en.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL); + + // getLocales must list all loaded languages + const locales = svc.getLocales(); + expect(locales).toContain('en'); + expect(locales).toContain('zh'); + expect(locales.length).toBeGreaterThanOrEqual(10); + }); }); \ No newline at end of file From 2e842bba0c67d576e4fd5d1ba7dfcb471c6f2a99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 08:27:43 +0000 Subject: [PATCH 6/7] =?UTF-8?q?fix(i18n):=20add=20explicit=20AppPlugin=20t?= =?UTF-8?q?o=20config=20=E2=80=94=20fixes=20server=20mode=20translations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: The CLI's isHostConfig() detects plugins with init methods and skips auto-registration of AppPlugin. Without AppPlugin, translations were never loaded via loadTranslations(). Fix: explicitly include AppPlugin(sharedConfig) in the plugins array after MemoryI18nPlugin. Verified by running the actual server (pnpm start) and testing: - GET /api/v1/i18n/translations/zh → {"crm":{"objects":{"account":{"label":"客户"}},...}} - GET /api/v1/i18n/translations/en → {"crm":{"objects":{"account":{"label":"Account"}},...}} - GET /api/v1/i18n/locales → all 10 CRM locales Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/555f26bf-5911-46a3-b3e4-c717756ddedc --- CHANGELOG.md | 2 +- apps/console/objectstack.config.ts | 35 ++++++++++++++----- .../src/__tests__/i18n-translations.test.ts | 16 ++++----- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ead64a8a..00b9bdb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **i18n Kernel Service `getTranslations` Returns Wrong Format** (`apps/console`): Fixed the `createKernel` i18n service registration where `getTranslations` returned a wrapped `{ locale, translations }` object instead of the flat `Record` dictionary expected by the spec's `II18nService` interface. The HttpDispatcher wraps the service return value in `{ data: { locale, translations } }`, so the extra wrapping caused translations to be empty or double-nested in the API response (`/api/v1/i18n/translations/:lang`). The service now returns `resolveI18nTranslations(bundles, lang)` directly, aligning with the spec and making CRM business translations work correctly in all environments (MSW, server, dev). -- **i18n Translations Empty in Server Mode (`pnpm start`)** (`apps/console`): Fixed translations returning `{}` when running the real ObjectStack server on port 3000. The root cause was a **timing issue in the kernel bootstrap sequence**: `AppPlugin.start()` → `loadTranslations()` runs during Phase 2 (start), but the kernel's memory i18n fallback is only auto-registered in `validateSystemRequirements()` which runs AFTER Phase 2 — so `loadTranslations()` found no i18n service and silently skipped loading. Fixed by adding a `MemoryI18nPlugin` to `objectstack.config.ts` that registers the in-memory i18n service during Phase 1 (init), ensuring it's available when `AppPlugin.start()` runs. Also added a spec-format `translations` array to `objectstack.shared.ts` so `AppPlugin.loadTranslations()` can iterate and load the CRM locale bundles. The plugin's name (`com.objectstack.service.i18n`) matches the CLI's deduplication check, preventing conflicts with `@objectstack/service-i18n` if installed later. +- **i18n Translations Empty in Server Mode (`pnpm start`)** (`apps/console`): Fixed translations returning `{}` when running the real ObjectStack server on port 3000. The root cause was that the CLI's `isHostConfig()` function detects plugins with `init` methods in the config's `plugins` array and treats it as a "host config" — **skipping auto-registration of `AppPlugin`**. Without `AppPlugin`, the config's translations, objects, and seed data were never loaded. Additionally, the kernel's memory i18n fallback is only auto-registered in `validateSystemRequirements()` (after all plugin starts), too late for `AppPlugin.start()` → `loadTranslations()`. Fixed by: (1) explicitly adding `AppPlugin(sharedConfig)` to `objectstack.config.ts` plugins, and (2) adding `MemoryI18nPlugin` before it to register the i18n service during init phase. Also added a spec-format `translations` array to `objectstack.shared.ts` so `AppPlugin.loadTranslations()` can iterate and load the CRM locale bundles. - **Duplicate Data in Calendar and Kanban Views** (`@object-ui/plugin-view`, `@object-ui/plugin-kanban`, `@object-ui/plugin-list`): Fixed a bug where Calendar and Kanban views displayed every record twice. The root cause was that `ObjectView` (plugin-view) unconditionally fetched data even when a `renderListView` callback was provided — causing parallel duplicate requests since `ListView` (plugin-list) independently fetches its own data. The duplicate fetch results triggered re-renders that destabilised child component rendering, leading to duplicate events in the calendar and duplicate cards on the kanban board. Additionally, `ObjectKanban` lacked proper external-data handling (`data`/`loading` props with `hasExternalData` guard), unlike `ObjectCalendar` which already had this pattern. Fixes: (1) `ObjectView` now skips its own fetch when `renderListView` is provided, (2) `ObjectKanban` now accepts explicit `data`/`loading` props and skips internal fetch when external data is supplied (matching `ObjectCalendar`'s pattern), (3) `ListView` now handles `{ value: [] }` OData response format consistently with `extractRecords` utility. Includes regression tests. diff --git a/apps/console/objectstack.config.ts b/apps/console/objectstack.config.ts index 767f1162..3ebbe2d4 100644 --- a/apps/console/objectstack.config.ts +++ b/apps/console/objectstack.config.ts @@ -23,6 +23,7 @@ const MSWPlugin = MSWPluginPkg.MSWPlugin || (MSWPluginPkg as any).default?.MSWPl const ObjectQLPlugin = ObjectQLPluginPkg.ObjectQLPlugin || (ObjectQLPluginPkg as any).default?.ObjectQLPlugin || (ObjectQLPluginPkg as any).default; const InMemoryDriver = DriverMemoryPkg.InMemoryDriver || (DriverMemoryPkg as any).default?.InMemoryDriver || (DriverMemoryPkg as any).default; const DriverPlugin = RuntimePkg.DriverPlugin || (RuntimePkg as any).default?.DriverPlugin || (RuntimePkg as any).default; +const AppPlugin = RuntimePkg.AppPlugin || (RuntimePkg as any).default?.AppPlugin || (RuntimePkg as any).default; const HonoServerPlugin = HonoServerPluginPkg.HonoServerPlugin || (HonoServerPluginPkg as any).default?.HonoServerPlugin || (HonoServerPluginPkg as any).default; const createMemoryI18n = CorePkg.createMemoryI18n || (CorePkg as any).default?.createMemoryI18n; @@ -32,17 +33,17 @@ import { ConsolePlugin } from './plugin'; * Lightweight plugin that registers the in-memory i18n service during the * init phase. This is critical for server mode (`pnpm start`) because: * - * 1. The CLI auto-registers AppPlugin(config) before config plugins. - * 2. During bootstrap, all plugin inits run first, then all starts. - * 3. AppPlugin.start() → loadTranslations() needs the i18n service. - * 4. The kernel's own memory fallback is registered in - * validateSystemRequirements() which runs AFTER all starts — too late. + * 1. AppPlugin.start() → loadTranslations() needs an i18n service. + * 2. The kernel's own memory i18n fallback is auto-registered in + * validateSystemRequirements() — which runs AFTER all plugin starts. + * 3. Without an early-registered i18n service, loadTranslations() finds + * nothing and silently skips — translations never get loaded. * - * By providing the service during init, AppPlugin.start() finds it and - * loads the spec-format `translations` array from the config. + * By registering the service during init (Phase 1), AppPlugin.start() + * (Phase 2) finds it and loads the spec-format `translations` array. * - * Name matches the check in CLI's serve command so it won't attempt to - * duplicate-register I18nServicePlugin from @objectstack/service-i18n. + * Name matches the CLI's dedup check so it won't attempt to also import + * @objectstack/service-i18n. */ class MemoryI18nPlugin { readonly name = 'com.objectstack.service.i18n'; @@ -55,8 +56,24 @@ class MemoryI18nPlugin { } } +/** + * Plugin ordering matters for server mode (`pnpm start`): + * + * The CLI's isHostConfig() detects that config.plugins contains objects with + * init methods (ObjectQLPlugin, DriverPlugin, etc.) and treats this as a + * "host config" — skipping auto-registration of AppPlugin. + * + * We therefore include AppPlugin explicitly so that: + * - Objects/metadata are registered with the kernel + * - Seed data is loaded into the in-memory driver + * - Translations are loaded into the i18n service (via loadTranslations) + * + * MemoryI18nPlugin MUST come before AppPlugin so that the i18n service + * exists when AppPlugin.start() → loadTranslations() runs. + */ const plugins: any[] = [ new MemoryI18nPlugin(), + new AppPlugin(sharedConfig), new ObjectQLPlugin(), new DriverPlugin(new InMemoryDriver(), 'memory'), new HonoServerPlugin({ port: 3000 }), diff --git a/apps/console/src/__tests__/i18n-translations.test.ts b/apps/console/src/__tests__/i18n-translations.test.ts index 91c2c91a..09873378 100644 --- a/apps/console/src/__tests__/i18n-translations.test.ts +++ b/apps/console/src/__tests__/i18n-translations.test.ts @@ -50,7 +50,7 @@ describe('i18n translations pipeline', () => { // ── Kernel service layer ─────────────────────────────────────────── it('kernel i18n service returns flat translations dict (not wrapped)', () => { - const i18nService = result.kernel.getService('i18n'); + const i18nService = result.kernel.getService('i18n') as any; const translations = i18nService.getTranslations('zh'); // Must NOT have the { locale, translations } wrapper @@ -60,14 +60,14 @@ describe('i18n translations pipeline', () => { }); it('kernel i18n service returns English translations', () => { - const i18nService = result.kernel.getService('i18n'); + const i18nService = result.kernel.getService('i18n') as any; const translations = i18nService.getTranslations('en'); expect(translations.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL); }); it('kernel i18n service returns empty for unknown locale', () => { - const i18nService = result.kernel.getService('i18n'); + const i18nService = result.kernel.getService('i18n') as any; const translations = i18nService.getTranslations('xx'); expect(Object.keys(translations)).toHaveLength(0); @@ -79,7 +79,7 @@ describe('i18n translations pipeline', () => { const { HttpDispatcher } = await import('@objectstack/runtime'); const dispatcher = new HttpDispatcher(result.kernel); - const dispatchResult = await dispatcher.handleI18n('/translations/zh', 'GET', {}, {}); + const dispatchResult = await dispatcher.handleI18n('/translations/zh', 'GET', {}, {} as any); expect(dispatchResult.handled).toBe(true); expect(dispatchResult.response?.status).toBe(200); @@ -115,7 +115,7 @@ describe('i18n translations pipeline', () => { // ── Server-mode compatibility (AppPlugin.loadTranslations) ──────── it('kernel i18n service supports loadTranslations (AppPlugin compat)', () => { - const i18nService = result.kernel.getService('i18n'); + const i18nService = result.kernel.getService('i18n') as any; // AppPlugin.loadTranslations calls these methods; they must exist expect(typeof i18nService.loadTranslations).toBe('function'); @@ -125,7 +125,7 @@ describe('i18n translations pipeline', () => { }); it('kernel i18n service getLocales returns all CRM locales', () => { - const i18nService = result.kernel.getService('i18n'); + const i18nService = result.kernel.getService('i18n') as any; const locales = i18nService.getLocales(); // CRM declares 10 locales: en, zh, ja, ko, de, fr, es, pt, ru, ar @@ -172,11 +172,11 @@ describe('i18n translations pipeline', () => { } // After loading, getTranslations must return populated CRM data - const zh = svc.getTranslations('zh'); + const zh = svc.getTranslations('zh') as any; expect(zh).toHaveProperty('crm'); expect(zh.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL); - const en = svc.getTranslations('en'); + const en = svc.getTranslations('en') as any; expect(en).toHaveProperty('crm'); expect(en.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL); From b836c5c0d80700b53add5ce099c5ebee42bb4cc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 09:08:56 +0000 Subject: [PATCH 7/7] fix(i18n): add i18n bundle aggregation to root objectstack.config.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: `pnpm dev` from the monorepo root runs `objectstack serve --dev` which loads the ROOT objectstack.config.ts — not apps/console/objectstack.config.ts. The root config collects plugin configs via getConfig() and composes them with composeStacks(), but composeStacks() doesn't handle the custom `i18n` field. So translation data from CRM (and other stacks) was lost during composition, and AppPlugin.loadTranslations() found nothing to load. Fix: 1. Aggregate i18n bundles from all plugin configs (same pattern as objectstack.shared.ts) 2. Build spec-format translations array passed to AppPlugin(mergedApp) 3. Add MemoryI18nPlugin to register i18n service during init phase Verified by actually running the server and testing: - GET /api/v1/i18n/translations/zh → {"crm":{"objects":{"account":{"label":"客户"}},...}} - GET /api/v1/i18n/translations/en → {"crm":{"objects":{"account":{"label":"Account"}},...}} - GET /api/v1/i18n/locales → all 10 CRM locales Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/8b23b829-56d5-4f03-b8f4-fe066b662587 --- CHANGELOG.md | 2 ++ objectstack.config.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00b9bdb7..07656b49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **i18n Translations Empty in Server Mode (`pnpm start`)** (`apps/console`): Fixed translations returning `{}` when running the real ObjectStack server on port 3000. The root cause was that the CLI's `isHostConfig()` function detects plugins with `init` methods in the config's `plugins` array and treats it as a "host config" — **skipping auto-registration of `AppPlugin`**. Without `AppPlugin`, the config's translations, objects, and seed data were never loaded. Additionally, the kernel's memory i18n fallback is only auto-registered in `validateSystemRequirements()` (after all plugin starts), too late for `AppPlugin.start()` → `loadTranslations()`. Fixed by: (1) explicitly adding `AppPlugin(sharedConfig)` to `objectstack.config.ts` plugins, and (2) adding `MemoryI18nPlugin` before it to register the i18n service during init phase. Also added a spec-format `translations` array to `objectstack.shared.ts` so `AppPlugin.loadTranslations()` can iterate and load the CRM locale bundles. +- **i18n Translations Empty in Root Dev Mode (`pnpm dev`)** (root `objectstack.config.ts`): Fixed translations returning `{}` when running `pnpm dev` from the monorepo root. The root config uses `objectstack dev` → `objectstack serve --dev` which loads the root `objectstack.config.ts` — but this config did not aggregate i18n bundles from the example stacks (CRM, Todo, etc.). The `composeStacks()` function doesn't handle the custom `i18n` field, so translation data was lost during composition. Fixed by: (1) aggregating i18n bundles from all plugin configs (same pattern as `objectstack.shared.ts`), (2) building a spec-format `translations` array passed to `AppPlugin(mergedApp)`, and (3) adding `MemoryI18nPlugin` to register the i18n service during init phase. + - **Duplicate Data in Calendar and Kanban Views** (`@object-ui/plugin-view`, `@object-ui/plugin-kanban`, `@object-ui/plugin-list`): Fixed a bug where Calendar and Kanban views displayed every record twice. The root cause was that `ObjectView` (plugin-view) unconditionally fetched data even when a `renderListView` callback was provided — causing parallel duplicate requests since `ListView` (plugin-list) independently fetches its own data. The duplicate fetch results triggered re-renders that destabilised child component rendering, leading to duplicate events in the calendar and duplicate cards on the kanban board. Additionally, `ObjectKanban` lacked proper external-data handling (`data`/`loading` props with `hasExternalData` guard), unlike `ObjectCalendar` which already had this pattern. Fixes: (1) `ObjectView` now skips its own fetch when `renderListView` is provided, (2) `ObjectKanban` now accepts explicit `data`/`loading` props and skips internal fetch when external data is supplied (matching `ObjectCalendar`'s pattern), (3) `ListView` now handles `{ value: [] }` OData response format consistently with `extractRecords` utility. Includes regression tests. - **CI Build Fix: Replace dynamic `require()` with static imports** (`@object-ui/plugin-view`): Replaced module-level `require('@object-ui/react')` calls in `index.tsx` and `ObjectView.tsx` with static `import` statements. The dynamic `require()` pattern is not supported by Next.js Turbopack, causing the docs site build to fail during SSR prerendering of the `/docs/components/complex/view-switcher` page with `Error: dynamic usage of require is not supported`. Since `@object-ui/react` is already a declared dependency and other files in the package use static imports from it, replacing the `require()` with static imports is safe and eliminates the SSR compatibility issue. diff --git a/objectstack.config.ts b/objectstack.config.ts index b3edb2b7..d7d9478d 100644 --- a/objectstack.config.ts +++ b/objectstack.config.ts @@ -22,6 +22,7 @@ import { AuthPlugin } from '@objectstack/plugin-auth'; import { ConsolePlugin } from '@object-ui/console'; import { composeStacks } from '@objectstack/spec'; import { mergeViewsIntoObjects } from '@object-ui/core'; +import { createMemoryI18n } from '@objectstack/core'; import { CRMPlugin } from './examples/crm/plugin'; import { TodoPlugin } from './examples/todo/plugin'; import { KitchenSinkPlugin } from './examples/kitchen-sink/plugin'; @@ -39,6 +40,24 @@ const allConfigs = plugins.map((p) => { // so we collect data from all stacks before composing). const allData = allConfigs.flatMap((c: any) => c.manifest?.data || c.data || []); +// Aggregate i18n bundles from all stacks that declare an i18n section. +// Each bundle carries a namespace (e.g. 'crm') and per-language translations. +const i18nBundles = allConfigs + .map((c: any) => c.i18n) + .filter((i: any) => i?.namespace && i?.translations); + +// Build the spec `translations` array for AppPlugin.loadTranslations(). +// Format: Array<{ [locale]: { namespace: data } }>. +// Each locale's data is nested under the bundle's namespace so that the +// i18n service serves `{ crm: { objects: { account: { label: '客户' } } } }`. +const specTranslations: Record[] = i18nBundles.map((bundle: any) => { + const result: Record = {}; + for (const [locale, data] of Object.entries(bundle.translations)) { + result[locale] = { [bundle.namespace]: data }; + } + return result; +}); + // Protocol-level composition via @objectstack/spec: handles object dedup, // array concatenation, actions→objects mapping, and manifest selection. const composed = composeStacks(allConfigs as any[], { objectConflict: 'override' }) as any; @@ -58,13 +77,37 @@ const mergedApp = { type: 'app', data: allData, }, + // Spec-format translations consumed by AppPlugin.loadTranslations() in server mode + translations: specTranslations, + i18n: { + bundles: i18nBundles, + defaultLocale: 'en', + }, }; +/** + * Registers the in-memory i18n service during the init phase (Phase 1) + * so that AppPlugin.start() → loadTranslations() (Phase 2) can find + * and load translation bundles. Without this, the kernel's memory i18n + * fallback only registers in validateSystemRequirements() — too late. + */ +class MemoryI18nPlugin { + readonly name = 'com.objectstack.service.i18n'; + readonly version = '1.0.0'; + readonly type = 'service' as const; + + init(ctx: any) { + const svc = createMemoryI18n(); + ctx.registerService('i18n', svc); + } +} + // Export only plugins — no top-level objects/manifest/apps. // The CLI auto-creates an AppPlugin from the config if it detects objects/manifest/apps, // which would conflict with our explicit AppPlugin and skip seed data loading. export default { plugins: [ + new MemoryI18nPlugin(), new ObjectQLPlugin(), new DriverPlugin(new InMemoryDriver()), new AppPlugin(mergedApp),