Skip to content

Commit f004c93

Browse files
authored
Merge pull request #1100 from objectstack-ai/copilot/fix-load-language-i18n
2 parents 7a20eaf + d4b2038 commit f004c93

File tree

4 files changed

+114
-18
lines changed

4 files changed

+114
-18
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: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
import { loadLanguage } from '../loadLanguage';
11+
12+
// ---------------------------------------------------------------------------
13+
// Tests
14+
// ---------------------------------------------------------------------------
15+
describe('loadLanguage', () => {
16+
const fetchSpy = vi.fn<(...args: Parameters<typeof fetch>) => Promise<Response>>();
17+
18+
beforeEach(() => {
19+
vi.stubGlobal('fetch', fetchSpy);
20+
});
21+
22+
afterEach(() => {
23+
vi.restoreAllMocks();
24+
});
25+
26+
it('unwraps spec REST API envelope { data: { locale, translations } }', async () => {
27+
const translations = { crm: { contact: '联系人', account: '客户' } };
28+
fetchSpy.mockResolvedValue({
29+
ok: true,
30+
json: async () => ({ data: { locale: 'zh-CN', translations } }),
31+
} as Response);
32+
33+
const result = await loadLanguage('zh-CN');
34+
expect(result).toEqual(translations);
35+
expect(fetchSpy).toHaveBeenCalledWith('/api/v1/i18n/translations/zh-CN');
36+
});
37+
38+
it('returns flat JSON when mock/dev server returns without envelope', async () => {
39+
const flat = { crm: { contact: 'Contact', account: 'Account' } };
40+
fetchSpy.mockResolvedValue({
41+
ok: true,
42+
json: async () => flat,
43+
} as Response);
44+
45+
const result = await loadLanguage('en');
46+
expect(result).toEqual(flat);
47+
});
48+
49+
it('returns empty object on HTTP error', async () => {
50+
fetchSpy.mockResolvedValue({ ok: false, status: 404 } as Response);
51+
52+
const result = await loadLanguage('xx');
53+
expect(result).toEqual({});
54+
});
55+
56+
it('returns empty object on network failure', async () => {
57+
fetchSpy.mockRejectedValue(new TypeError('Failed to fetch'));
58+
59+
const result = await loadLanguage('en');
60+
expect(result).toEqual({});
61+
});
62+
63+
it('handles envelope with null translations gracefully (fallback)', async () => {
64+
fetchSpy.mockResolvedValue({
65+
ok: true,
66+
json: async () => ({ data: { locale: 'en', translations: null } }),
67+
} as Response);
68+
69+
const result = await loadLanguage('en');
70+
// translations is null (not an object), so fallback to full JSON
71+
expect(result).toEqual({ data: { locale: 'en', translations: null } });
72+
});
73+
74+
it('handles envelope with missing data field (fallback)', async () => {
75+
const flat = { hello: 'world' };
76+
fetchSpy.mockResolvedValue({
77+
ok: true,
78+
json: async () => flat,
79+
} as Response);
80+
81+
const result = await loadLanguage('en');
82+
expect(result).toEqual(flat);
83+
});
84+
});

apps/console/src/loadLanguage.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Load application-specific translations for a given language from the API.
3+
*
4+
* The @objectstack/spec REST API (`/api/v1/i18n/translations/:locale`) wraps
5+
* its response in the standard envelope: `{ data: { locale, translations } }`.
6+
* We extract `data.translations` when present, and fall back to the raw JSON
7+
* for mock / local-dev environments that may return flat translation objects.
8+
*/
9+
export async function loadLanguage(lang: string): Promise<Record<string, unknown>> {
10+
try {
11+
const res = await fetch(`/api/v1/i18n/translations/${lang}`);
12+
if (!res.ok) {
13+
console.warn(`[i18n] Failed to load translations for '${lang}': HTTP ${res.status}`);
14+
return {};
15+
}
16+
const json = await res.json();
17+
// Unwrap the spec REST API envelope when present
18+
if (json?.data?.translations && typeof json.data.translations === 'object') {
19+
return json.data.translations as Record<string, unknown>;
20+
}
21+
// Fallback: mock server / local dev returns flat translation objects
22+
return json;
23+
} catch (err) {
24+
console.warn(`[i18n] Failed to load translations for '${lang}':`, err);
25+
return {};
26+
}
27+
}

apps/console/src/main.tsx

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import './index.css';
1010
import { App } from './App';
1111
import { I18nProvider } from '@object-ui/i18n';
1212
import { MobileProvider } from '@object-ui/mobile';
13+
import { loadLanguage } from './loadLanguage';
1314

1415
// Register plugins (side-effect imports for ComponentRegistry)
1516
import '@object-ui/plugin-grid';
@@ -27,24 +28,6 @@ import '@object-ui/plugin-dashboard';
2728
import '@object-ui/plugin-report';
2829
import '@object-ui/plugin-markdown';
2930

30-
/**
31-
* Load application-specific translations for a given language from the API.
32-
* Falls back gracefully when translations are unavailable.
33-
*/
34-
async function loadLanguage(lang: string): Promise<Record<string, unknown>> {
35-
try {
36-
const res = await fetch(`/api/v1/i18n/translations/${lang}`);
37-
if (!res.ok) {
38-
console.warn(`[i18n] Failed to load translations for '${lang}': HTTP ${res.status}`);
39-
return {};
40-
}
41-
return await res.json();
42-
} catch (err) {
43-
console.warn(`[i18n] Failed to load translations for '${lang}':`, err);
44-
return {};
45-
}
46-
}
47-
4831
// Start MSW before rendering the app
4932
async function bootstrap() {
5033
// Initialize Mock Service Worker if enabled (lazy-loaded to keep production bundle lean)

0 commit comments

Comments
 (0)