Skip to content

Commit 0bee120

Browse files
Copilothotlong
andcommitted
fix(i18n): add spec-format translations array for server mode + full i18n service
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
1 parent 3d034e2 commit 0bee120

File tree

4 files changed

+96
-2
lines changed

4 files changed

+96
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ 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 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`.
21+
2022
- **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.
2123

2224
- **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.

apps/console/objectstack.shared.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ const i18nBundles = allConfigs
3030
.map((c: any) => c.i18n)
3131
.filter((i: any) => i?.namespace && i?.translations);
3232

33+
// Build the spec `translations` array for the runtime's AppPlugin.
34+
// AppPlugin.loadTranslations expects `translations: Array<{ [locale]: data }>`.
35+
// Each locale's data is nested under the bundle's namespace so that
36+
// both the server-mode (AppPlugin → memory i18n) and MSW-mode (createKernel)
37+
// produce the same structure: `{ crm: { objects: { ... } } }`.
38+
const specTranslations: Record<string, any>[] = i18nBundles.map((bundle: any) => {
39+
const result: Record<string, any> = {};
40+
for (const [locale, data] of Object.entries(bundle.translations)) {
41+
result[locale] = { [bundle.namespace]: data };
42+
}
43+
return result;
44+
});
45+
3346
// Protocol-level composition via @objectstack/spec: handles object dedup,
3447
// array concatenation, actions→objects mapping, and manifest selection.
3548
const composed = composeStacks(allConfigs as any[], { objectConflict: 'override' }) as any;
@@ -97,7 +110,13 @@ export const sharedConfig = {
97110
},
98111
i18n: {
99112
bundles: i18nBundles,
113+
defaultLocale: 'en',
100114
},
115+
// Spec-format translations array consumed by AppPlugin.loadTranslations()
116+
// in real-server mode (pnpm start). Each entry maps locale → namespace-scoped
117+
// translation data so the runtime's memory i18n fallback serves the same
118+
// structure as the MSW mock handler.
119+
translations: specTranslations,
101120
plugins: [],
102121
datasources: [
103122
{

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,42 @@ describe('i18n translations pipeline', () => {
111111

112112
expect(json?.data?.translations?.crm?.objects?.account?.label).toBe(EXPECTED_EN_ACCOUNT_LABEL);
113113
});
114-
});
114+
115+
// ── Server-mode compatibility (AppPlugin.loadTranslations) ────────
116+
117+
it('kernel i18n service supports loadTranslations (AppPlugin compat)', () => {
118+
const i18nService = result.kernel.getService('i18n');
119+
120+
// AppPlugin.loadTranslations calls these methods; they must exist
121+
expect(typeof i18nService.loadTranslations).toBe('function');
122+
expect(typeof i18nService.getLocales).toBe('function');
123+
expect(typeof i18nService.getDefaultLocale).toBe('function');
124+
expect(typeof i18nService.setDefaultLocale).toBe('function');
125+
});
126+
127+
it('kernel i18n service getLocales returns all CRM locales', () => {
128+
const i18nService = result.kernel.getService('i18n');
129+
const locales = i18nService.getLocales();
130+
131+
// CRM declares 10 locales: en, zh, ja, ko, de, fr, es, pt, ru, ar
132+
expect(locales).toContain('en');
133+
expect(locales).toContain('zh');
134+
expect(locales.length).toBeGreaterThanOrEqual(10);
135+
});
136+
137+
it('appConfig.translations is spec-format array for AppPlugin', () => {
138+
const translations = (appConfig as any).translations;
139+
140+
expect(Array.isArray(translations)).toBe(true);
141+
expect(translations.length).toBeGreaterThan(0);
142+
143+
// Each entry maps locale → namespace-scoped data
144+
const first = translations[0];
145+
expect(first).toHaveProperty('zh');
146+
expect(first).toHaveProperty('en');
147+
// Data must be nested under namespace (e.g. 'crm')
148+
expect(first.zh).toHaveProperty('crm');
149+
expect(first.zh.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL);
150+
expect(first.en.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL);
151+
});
152+
});

apps/console/src/mocks/createKernel.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,43 @@ export async function createKernel(options: KernelOptions): Promise<KernelResult
332332
const i18nBundles: I18nBundle[] = appConfig.i18n?.bundles ?? [];
333333

334334
if (i18nBundles.length > 0) {
335+
// Build a complete i18n service that satisfies both:
336+
// - HttpDispatcher.handleI18n (calls getTranslations, getLocales)
337+
// - AppPlugin.loadTranslations (calls loadTranslations, setDefaultLocale)
338+
// In MSW mode the custom handler serves translations directly, but the
339+
// kernel service is still needed for the broker shim and dispatcher paths.
340+
const appPluginTranslations = new Map<string, Record<string, unknown>>();
341+
let defaultLocale = 'en';
342+
335343
kernel.registerService('i18n', {
336-
getTranslations: (lang: string) => resolveI18nTranslations(i18nBundles, lang),
344+
getTranslations: (lang: string) => {
345+
const resolved = resolveI18nTranslations(i18nBundles, lang);
346+
const extra = appPluginTranslations.get(lang);
347+
if (extra) {
348+
// Shallow merge: AppPlugin-loaded translations override bundle translations
349+
return { ...resolved, ...extra };
350+
}
351+
return resolved;
352+
},
353+
loadTranslations: (locale: string, data: Record<string, unknown>) => {
354+
const existing = appPluginTranslations.get(locale) ?? {};
355+
appPluginTranslations.set(locale, { ...existing, ...data });
356+
},
357+
getLocales: () => {
358+
// Collect all available locales from bundles + any loaded via loadTranslations
359+
const locales = new Set<string>();
360+
for (const bundle of i18nBundles) {
361+
for (const lang of Object.keys(bundle.translations)) {
362+
locales.add(lang);
363+
}
364+
}
365+
for (const lang of appPluginTranslations.keys()) {
366+
locales.add(lang);
367+
}
368+
return [...locales];
369+
},
370+
getDefaultLocale: () => defaultLocale,
371+
setDefaultLocale: (locale: string) => { defaultLocale = locale; },
337372
});
338373
}
339374

0 commit comments

Comments
 (0)