Skip to content

Commit 2e842bb

Browse files
Copilothotlong
andcommitted
fix(i18n): add explicit AppPlugin to config — fixes server mode translations
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
1 parent 7d89c16 commit 2e842bb

File tree

3 files changed

+35
-18
lines changed

3 files changed

+35
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
- **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<string, any>` 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).
1919

20-
- **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.
20+
- **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.
2121

2222
- **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.
2323

apps/console/objectstack.config.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const MSWPlugin = MSWPluginPkg.MSWPlugin || (MSWPluginPkg as any).default?.MSWPl
2323
const ObjectQLPlugin = ObjectQLPluginPkg.ObjectQLPlugin || (ObjectQLPluginPkg as any).default?.ObjectQLPlugin || (ObjectQLPluginPkg as any).default;
2424
const InMemoryDriver = DriverMemoryPkg.InMemoryDriver || (DriverMemoryPkg as any).default?.InMemoryDriver || (DriverMemoryPkg as any).default;
2525
const DriverPlugin = RuntimePkg.DriverPlugin || (RuntimePkg as any).default?.DriverPlugin || (RuntimePkg as any).default;
26+
const AppPlugin = RuntimePkg.AppPlugin || (RuntimePkg as any).default?.AppPlugin || (RuntimePkg as any).default;
2627
const HonoServerPlugin = HonoServerPluginPkg.HonoServerPlugin || (HonoServerPluginPkg as any).default?.HonoServerPlugin || (HonoServerPluginPkg as any).default;
2728
const createMemoryI18n = CorePkg.createMemoryI18n || (CorePkg as any).default?.createMemoryI18n;
2829

@@ -32,17 +33,17 @@ import { ConsolePlugin } from './plugin';
3233
* Lightweight plugin that registers the in-memory i18n service during the
3334
* init phase. This is critical for server mode (`pnpm start`) because:
3435
*
35-
* 1. The CLI auto-registers AppPlugin(config) before config plugins.
36-
* 2. During bootstrap, all plugin inits run first, then all starts.
37-
* 3. AppPlugin.start() → loadTranslations() needs the i18n service.
38-
* 4. The kernel's own memory fallback is registered in
39-
* validateSystemRequirements() which runs AFTER all starts — too late.
36+
* 1. AppPlugin.start() → loadTranslations() needs an i18n service.
37+
* 2. The kernel's own memory i18n fallback is auto-registered in
38+
* validateSystemRequirements() — which runs AFTER all plugin starts.
39+
* 3. Without an early-registered i18n service, loadTranslations() finds
40+
* nothing and silently skips — translations never get loaded.
4041
*
41-
* By providing the service during init, AppPlugin.start() finds it and
42-
* loads the spec-format `translations` array from the config.
42+
* By registering the service during init (Phase 1), AppPlugin.start()
43+
* (Phase 2) finds it and loads the spec-format `translations` array.
4344
*
44-
* Name matches the check in CLI's serve command so it won't attempt to
45-
* duplicate-register I18nServicePlugin from @objectstack/service-i18n.
45+
* Name matches the CLI's dedup check so it won't attempt to also import
46+
* @objectstack/service-i18n.
4647
*/
4748
class MemoryI18nPlugin {
4849
readonly name = 'com.objectstack.service.i18n';
@@ -55,8 +56,24 @@ class MemoryI18nPlugin {
5556
}
5657
}
5758

59+
/**
60+
* Plugin ordering matters for server mode (`pnpm start`):
61+
*
62+
* The CLI's isHostConfig() detects that config.plugins contains objects with
63+
* init methods (ObjectQLPlugin, DriverPlugin, etc.) and treats this as a
64+
* "host config" — skipping auto-registration of AppPlugin.
65+
*
66+
* We therefore include AppPlugin explicitly so that:
67+
* - Objects/metadata are registered with the kernel
68+
* - Seed data is loaded into the in-memory driver
69+
* - Translations are loaded into the i18n service (via loadTranslations)
70+
*
71+
* MemoryI18nPlugin MUST come before AppPlugin so that the i18n service
72+
* exists when AppPlugin.start() → loadTranslations() runs.
73+
*/
5874
const plugins: any[] = [
5975
new MemoryI18nPlugin(),
76+
new AppPlugin(sharedConfig),
6077
new ObjectQLPlugin(),
6178
new DriverPlugin(new InMemoryDriver(), 'memory'),
6279
new HonoServerPlugin({ port: 3000 }),

apps/console/src/__tests__/i18n-translations.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('i18n translations pipeline', () => {
5050
// ── Kernel service layer ───────────────────────────────────────────
5151

5252
it('kernel i18n service returns flat translations dict (not wrapped)', () => {
53-
const i18nService = result.kernel.getService('i18n');
53+
const i18nService = result.kernel.getService('i18n') as any;
5454
const translations = i18nService.getTranslations('zh');
5555

5656
// Must NOT have the { locale, translations } wrapper
@@ -60,14 +60,14 @@ describe('i18n translations pipeline', () => {
6060
});
6161

6262
it('kernel i18n service returns English translations', () => {
63-
const i18nService = result.kernel.getService('i18n');
63+
const i18nService = result.kernel.getService('i18n') as any;
6464
const translations = i18nService.getTranslations('en');
6565

6666
expect(translations.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL);
6767
});
6868

6969
it('kernel i18n service returns empty for unknown locale', () => {
70-
const i18nService = result.kernel.getService('i18n');
70+
const i18nService = result.kernel.getService('i18n') as any;
7171
const translations = i18nService.getTranslations('xx');
7272

7373
expect(Object.keys(translations)).toHaveLength(0);
@@ -79,7 +79,7 @@ describe('i18n translations pipeline', () => {
7979
const { HttpDispatcher } = await import('@objectstack/runtime');
8080
const dispatcher = new HttpDispatcher(result.kernel);
8181

82-
const dispatchResult = await dispatcher.handleI18n('/translations/zh', 'GET', {}, {});
82+
const dispatchResult = await dispatcher.handleI18n('/translations/zh', 'GET', {}, {} as any);
8383

8484
expect(dispatchResult.handled).toBe(true);
8585
expect(dispatchResult.response?.status).toBe(200);
@@ -115,7 +115,7 @@ describe('i18n translations pipeline', () => {
115115
// ── Server-mode compatibility (AppPlugin.loadTranslations) ────────
116116

117117
it('kernel i18n service supports loadTranslations (AppPlugin compat)', () => {
118-
const i18nService = result.kernel.getService('i18n');
118+
const i18nService = result.kernel.getService('i18n') as any;
119119

120120
// AppPlugin.loadTranslations calls these methods; they must exist
121121
expect(typeof i18nService.loadTranslations).toBe('function');
@@ -125,7 +125,7 @@ describe('i18n translations pipeline', () => {
125125
});
126126

127127
it('kernel i18n service getLocales returns all CRM locales', () => {
128-
const i18nService = result.kernel.getService('i18n');
128+
const i18nService = result.kernel.getService('i18n') as any;
129129
const locales = i18nService.getLocales();
130130

131131
// CRM declares 10 locales: en, zh, ja, ko, de, fr, es, pt, ru, ar
@@ -172,11 +172,11 @@ describe('i18n translations pipeline', () => {
172172
}
173173

174174
// After loading, getTranslations must return populated CRM data
175-
const zh = svc.getTranslations('zh');
175+
const zh = svc.getTranslations('zh') as any;
176176
expect(zh).toHaveProperty('crm');
177177
expect(zh.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL);
178178

179-
const en = svc.getTranslations('en');
179+
const en = svc.getTranslations('en') as any;
180180
expect(en).toHaveProperty('crm');
181181
expect(en.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL);
182182

0 commit comments

Comments
 (0)