Skip to content

Commit 3d034e2

Browse files
Copilothotlong
andcommitted
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<string, any> 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
1 parent 36a5042 commit 3d034e2

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* i18n Translations Pipeline Tests
3+
*
4+
* Validates that the i18n translation pipeline delivers CRM translations
5+
* correctly through all code paths:
6+
* - Kernel service: getTranslations returns flat dict (not wrapped)
7+
* - HttpDispatcher: wraps into standard spec envelope
8+
* - MSW handler: returns correct { data: { locale, translations } }
9+
*
10+
* Regression test for empty translations:{} response.
11+
*/
12+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
13+
import { setupServer } from 'msw/node';
14+
import { createKernel, type KernelResult } from '../mocks/createKernel';
15+
import { createAuthHandlers } from '../mocks/authHandlers';
16+
import appConfig from '../../objectstack.shared';
17+
import { crmLocales } from '@object-ui/example-crm';
18+
19+
// Expected values from the CRM i18n bundles — avoid hard-coding in assertions
20+
const EXPECTED_ZH_ACCOUNT_LABEL = crmLocales.zh.objects.account.label;
21+
const EXPECTED_EN_ACCOUNT_LABEL = crmLocales.en.objects.account.label;
22+
23+
describe('i18n translations pipeline', () => {
24+
let result: KernelResult;
25+
let server: ReturnType<typeof setupServer>;
26+
27+
beforeAll(async () => {
28+
result = await createKernel({
29+
appConfig,
30+
persistence: false,
31+
mswOptions: {
32+
enableBrowser: false,
33+
baseUrl: '/api/v1',
34+
logRequests: false,
35+
customHandlers: [
36+
...createAuthHandlers('/api/v1/auth'),
37+
],
38+
},
39+
});
40+
41+
const handlers = result.mswPlugin?.getHandlers() ?? [];
42+
server = setupServer(...handlers);
43+
server.listen({ onUnhandledRequest: 'bypass' });
44+
});
45+
46+
afterAll(() => {
47+
server?.close();
48+
});
49+
50+
// ── Kernel service layer ───────────────────────────────────────────
51+
52+
it('kernel i18n service returns flat translations dict (not wrapped)', () => {
53+
const i18nService = result.kernel.getService('i18n');
54+
const translations = i18nService.getTranslations('zh');
55+
56+
// Must NOT have the { locale, translations } wrapper
57+
expect(translations).not.toHaveProperty('locale');
58+
expect(translations).toHaveProperty('crm');
59+
expect(translations.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL);
60+
});
61+
62+
it('kernel i18n service returns English translations', () => {
63+
const i18nService = result.kernel.getService('i18n');
64+
const translations = i18nService.getTranslations('en');
65+
66+
expect(translations.crm.objects.account.label).toBe(EXPECTED_EN_ACCOUNT_LABEL);
67+
});
68+
69+
it('kernel i18n service returns empty for unknown locale', () => {
70+
const i18nService = result.kernel.getService('i18n');
71+
const translations = i18nService.getTranslations('xx');
72+
73+
expect(Object.keys(translations)).toHaveLength(0);
74+
});
75+
76+
// ── HttpDispatcher layer ───────────────────────────────────────────
77+
78+
it('HttpDispatcher returns populated translations in spec envelope', async () => {
79+
const { HttpDispatcher } = await import('@objectstack/runtime');
80+
const dispatcher = new HttpDispatcher(result.kernel);
81+
82+
const dispatchResult = await dispatcher.handleI18n('/translations/zh', 'GET', {}, {});
83+
84+
expect(dispatchResult.handled).toBe(true);
85+
expect(dispatchResult.response?.status).toBe(200);
86+
const body = dispatchResult.response?.body;
87+
expect(body?.success).toBe(true);
88+
expect(body?.data?.locale).toBe('zh');
89+
expect(Object.keys(body?.data?.translations ?? {}).length).toBeGreaterThan(0);
90+
expect(body?.data?.translations?.crm?.objects?.account?.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL);
91+
});
92+
93+
// ── MSW handler layer (fetch) ──────────────────────────────────────
94+
95+
it('GET /api/v1/i18n/translations/zh returns CRM zh translations', async () => {
96+
const res = await fetch('http://localhost/api/v1/i18n/translations/zh');
97+
expect(res.ok).toBe(true);
98+
99+
const json = await res.json();
100+
const translations = json?.data?.translations;
101+
102+
expect(translations).toBeDefined();
103+
expect(Object.keys(translations).length).toBeGreaterThan(0);
104+
expect(translations.crm).toBeDefined();
105+
expect(translations.crm.objects.account.label).toBe(EXPECTED_ZH_ACCOUNT_LABEL);
106+
});
107+
108+
it('GET /api/v1/i18n/translations/en returns CRM en translations', async () => {
109+
const res = await fetch('http://localhost/api/v1/i18n/translations/en');
110+
const json = await res.json();
111+
112+
expect(json?.data?.translations?.crm?.objects?.account?.label).toBe(EXPECTED_EN_ACCOUNT_LABEL);
113+
});
114+
});

0 commit comments

Comments
 (0)