Skip to content

Commit 599890f

Browse files
Copilothotlong
andcommitted
fix(i18n): unwrap spec REST API envelope in loadLanguage for correct translations
The @objectstack/spec REST API wraps i18n responses in { data: { locale, translations } }. loadLanguage now extracts data.translations when present, falling back to raw JSON for mock/dev environments. Includes 6 unit tests. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/c9dec823-2a7f-43fa-a55e-6b357fefe3d5
1 parent 395ff33 commit 599890f

File tree

3 files changed

+120
-2
lines changed

3 files changed

+120
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- **i18n loadLanguage Not Compatible with Spec REST API Response Format** (`apps/console`): Fixed `loadLanguage` in `apps/console/src/main.tsx` to correctly unwrap the `@objectstack/spec` REST API envelope `{ data: { locale, translations } }`. Previously, the function returned the raw JSON response, which meant `useObjectLabel` and `useObjectTranslation` hooks received a wrapped object instead of the flat translation map, causing business object and field labels (e.g., CRM contact/account) to fall back to English. The fix extracts `data.translations` when the spec envelope is detected, while preserving backward compatibility with mock/dev environments that return flat translation objects. Includes 6 unit tests covering spec envelope unwrapping, flat fallback, HTTP errors, network failures, and edge cases.
13+
1214
- **List Toolbar Action Buttons Not Responding** (`apps/console`): Fixed a bug where schema-driven action buttons in the upper-right corner of the list view (rendered via `action:bar` with `locations: ['list_toolbar']`) did not respond to clicks. The root cause: the console `ObjectView` component rendered these action buttons via `SchemaRenderer` without wrapping them in an `ActionProvider`. This caused `useAction()` to fall back to a local `ActionRunner` with no handlers, toast, confirmation, or navigation capabilities — so clicks executed silently with no visible effect. Added `ActionProvider` with proper handlers (toast via Sonner, confirmation dialog, navigation via React Router, param collection dialog, and a generic API handler) to the console `ObjectView`, following the same pattern already used in `RecordDetailView`. Includes 4 new integration tests validating the full action chain (render, confirm, execute, cancel).
1315

1416
- **CRM Seed Data Lookup References** (`examples/crm`): Fixed all CRM seed data files to use natural key (`name`) references for lookup fields instead of raw `id` values. The `SeedLoaderService` resolves foreign key references by the target object's `name` field (the default `externalId`), so seed data values like `order: "o1"` or `account: "2"` could not be resolved and were set to `null`. Updated all 8 affected data files (`account`, `contact`, `opportunity`, `order`, `order_item`, `opportunity_contact`, `project_task`, `event`) to use human-readable name references (e.g., `order: "ORD-2024-001"`, `product: "Workstation Pro Laptop"`, `account: "Salesforce Tower"`). This fixes the issue where Order and Product columns displayed as empty in the Order Item grid view.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* loadLanguage Tests
3+
*
4+
* Verifies that the loadLanguage helper correctly unwraps the
5+
* @objectstack/spec REST API envelope (`{ data: { locale, translations } }`)
6+
* while remaining backward-compatible with flat mock/dev responses.
7+
*/
8+
9+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10+
11+
// ---------------------------------------------------------------------------
12+
// Re-implement the loadLanguage logic under test (mirrors apps/console/src/main.tsx)
13+
// ---------------------------------------------------------------------------
14+
async function loadLanguage(lang: string): Promise<Record<string, unknown>> {
15+
try {
16+
const res = await fetch(`/api/v1/i18n/translations/${lang}`);
17+
if (!res.ok) {
18+
console.warn(`[i18n] Failed to load translations for '${lang}': HTTP ${res.status}`);
19+
return {};
20+
}
21+
const json = await res.json();
22+
// Unwrap the spec REST API envelope when present
23+
if (json && typeof json === 'object' && json.data && json.data.translations && typeof json.data.translations === 'object') {
24+
return json.data.translations as Record<string, unknown>;
25+
}
26+
// Fallback: mock server / local dev returns flat translation objects
27+
return json;
28+
} catch (err) {
29+
console.warn(`[i18n] Failed to load translations for '${lang}':`, err);
30+
return {};
31+
}
32+
}
33+
34+
// ---------------------------------------------------------------------------
35+
// Tests
36+
// ---------------------------------------------------------------------------
37+
describe('loadLanguage', () => {
38+
const fetchSpy = vi.fn<(...args: any[]) => Promise<Response>>();
39+
40+
beforeEach(() => {
41+
vi.stubGlobal('fetch', fetchSpy);
42+
});
43+
44+
afterEach(() => {
45+
vi.restoreAllMocks();
46+
});
47+
48+
it('unwraps spec REST API envelope { data: { locale, translations } }', async () => {
49+
const translations = { crm: { contact: '联系人', account: '客户' } };
50+
fetchSpy.mockResolvedValue({
51+
ok: true,
52+
json: async () => ({ data: { locale: 'zh-CN', translations } }),
53+
} as Response);
54+
55+
const result = await loadLanguage('zh-CN');
56+
expect(result).toEqual(translations);
57+
expect(fetchSpy).toHaveBeenCalledWith('/api/v1/i18n/translations/zh-CN');
58+
});
59+
60+
it('returns flat JSON when mock/dev server returns without envelope', async () => {
61+
const flat = { crm: { contact: 'Contact', account: 'Account' } };
62+
fetchSpy.mockResolvedValue({
63+
ok: true,
64+
json: async () => flat,
65+
} as Response);
66+
67+
const result = await loadLanguage('en');
68+
expect(result).toEqual(flat);
69+
});
70+
71+
it('returns empty object on HTTP error', async () => {
72+
fetchSpy.mockResolvedValue({ ok: false, status: 404 } as Response);
73+
74+
const result = await loadLanguage('xx');
75+
expect(result).toEqual({});
76+
});
77+
78+
it('returns empty object on network failure', async () => {
79+
fetchSpy.mockRejectedValue(new TypeError('Failed to fetch'));
80+
81+
const result = await loadLanguage('en');
82+
expect(result).toEqual({});
83+
});
84+
85+
it('handles envelope with null translations gracefully (fallback)', async () => {
86+
fetchSpy.mockResolvedValue({
87+
ok: true,
88+
json: async () => ({ data: { locale: 'en', translations: null } }),
89+
} as Response);
90+
91+
const result = await loadLanguage('en');
92+
// translations is null (not an object), so fallback to full JSON
93+
expect(result).toEqual({ data: { locale: 'en', translations: null } });
94+
});
95+
96+
it('handles envelope with missing data field (fallback)', async () => {
97+
const flat = { hello: 'world' };
98+
fetchSpy.mockResolvedValue({
99+
ok: true,
100+
json: async () => flat,
101+
} as Response);
102+
103+
const result = await loadLanguage('en');
104+
expect(result).toEqual(flat);
105+
});
106+
});

apps/console/src/main.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import '@object-ui/plugin-markdown';
2929

3030
/**
3131
* Load application-specific translations for a given language from the API.
32-
* Falls back gracefully when translations are unavailable.
32+
*
33+
* The @objectstack/spec REST API (`/api/v1/i18n/translations/:locale`) wraps
34+
* its response in the standard envelope: `{ data: { locale, translations } }`.
35+
* We extract `data.translations` when present, and fall back to the raw JSON
36+
* for mock / local-dev environments that may return flat translation objects.
3337
*/
3438
async function loadLanguage(lang: string): Promise<Record<string, unknown>> {
3539
try {
@@ -38,7 +42,13 @@ async function loadLanguage(lang: string): Promise<Record<string, unknown>> {
3842
console.warn(`[i18n] Failed to load translations for '${lang}': HTTP ${res.status}`);
3943
return {};
4044
}
41-
return await res.json();
45+
const json = await res.json();
46+
// Unwrap the spec REST API envelope when present
47+
if (json && typeof json === 'object' && json.data && json.data.translations && typeof json.data.translations === 'object') {
48+
return json.data.translations as Record<string, unknown>;
49+
}
50+
// Fallback: mock server / local dev returns flat translation objects
51+
return json;
4252
} catch (err) {
4353
console.warn(`[i18n] Failed to load translations for '${lang}':`, err);
4454
return {};

0 commit comments

Comments
 (0)