Skip to content

Commit c86c823

Browse files
fix(i18n): localise the console's round-3 leftovers — data table titles, chart fallback, react pages, References columns, Flow Runs diagnostics (#25978)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi96SEbBcSNaduu5xfTJgz2Y7)_ Part of #25916 Clause-②: no Metadata-protocol: none Third pass on the card's leftover list (the second pass's hand-back, comment 6407864045), under claim 6410094565. Implemented by the card-thread session `https://claude.ai/code/session_01C6T942afhqAP5w9XBmfTDC`. ## What changes | Surface | Before | Now reads | |---|---|---| | `data-table` (components) | hover affordance title "Open record"; inline-edit row buttons "Cancel changes" / "Save row" | `table.openRecord`, `table.cancelChanges`, `table.saveRow` (also in the renderer's provider-less defaults table) | | Chart view fallback (`ObjectView.tsx`, Loading-chart region only) | "Loading chart…" twice | the existing `chart.loading` row | | `kind:'react'` page renderer (`react-page.tsx`, no `t(` before) | capability notice, empty-source refusal, "Loading react runtime…", two error titles | `reactPage.*` (new namespace, six rows) | | Studio References sheet (`ResourceEditPage.tsx`) | column headers From type / From name / Path | `engine.edit.refsColFromType`, `refsColFromName`, `refsColPath` in the Studio's own `engine.*` table (en-US + zh-CN), per the carve-out the first pass used | | Flow Runs (`FlowRunsPage.tsx`) | three English "X is not available on this client" diagnostics stored in error state | error state keeps the missing method's NAME; each panel words it at render through `console.developer.flowRuns.clientMethodUnavailable` (method interpolated). A message the call threw is still drawn verbatim | - New pack rows are in all ten packs (`en` plus nine real translations); each call carries the `en` value as its inline default, so provider-less mounts and existing English tests are unchanged. - The two `reactPage` sentences with inline code chips are ONE translated string each: the chips' slots are interpolated with private-use marks and the translated sentence is split on them, so each language decides where the chip sits. Same technique as `element:metadata_viewer` (a local copy of its `CHIP` / `withChips` helper, in the same package). - `scripts/sdui-manifest.record.json`: `check-sdui-manifest` went red because `data-table.tsx` and `react-page.tsx` are registry sources. Regenerated with `node scripts/gen-sdui-manifest-node.mjs`: `sdui.manifest.json` is byte-unchanged (106 components); only the recorded source hash moved. ## Tests New, through the real renderers / pages and a real `I18nProvider` (zh expectations read from the packs and guarded as real Han rows, plus provider-less English controls): - `ui/packages/components/src/__tests__/renderer-copy-locale-25916.test.tsx`: data-table titles; react-page notice and empty-source refusal with chips in the zh position and no slot leak. 7/7. - `ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.referencesColumns-25916.test.tsx`: References column headers zh / en. 3/3. - `ui/apps/console/src/pages/developer/flowRunsClientDiagnostic-25916.test.tsx`: the three diagnostics in zh with the method name, a thrown message verbatim, English controls. 6/6. Affected set (195 files: every test naming the touched titles, strings or components, plus all of `ui/packages/i18n` and the i18n script tests), under the verify lock at `962206c5`: Test Files 195 passed (195), Tests 3305 passed, 13 skipped. The References-columns file was re-run (3/3) after its typing fix in `522c33d6`. Type-check of `@object-ui/i18n`, `components`, `app-shell`, `console` (turbo, with `^build`): green after one test-typing fix. ## Gates Local: the 34 commands `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived, plus `check-changeset-fixed` (its roster sits under `.changeset`): all exit 0 except `check-sdui-manifest` (cleared by the re-record above, now exit 0) and `check:type-check-debt` (timed out under the 300 s per-command cap; re-run reported in the os-dev report). ui scripts: `check-i18n-call-site-keys`, `check-i18n-en-drift`, `check-i18n-dead-keys` (none of the new keys listed), `check-i18n-designer-table-parity`, `check-new-cross-file-line-citations` (0 new): all exit 0. Repo-wide lint and the full ui shard battery are CI's. ## Acceptance notes - **Dev-only harnesses left English.** `ui/apps/console/src/dev/*` (Dev · … headings, `PerformanceDashboard`) and the console `*-preview.tsx` entries are developer harnesses, moved under `dev/` by the second pass precisely so a census can exclude them by path; translating their headings buys no user-facing language. Not done here; this is why the PR says `Part of`. - **Out of scope by the dispatch ruling, untouched:** ShareDialog audience / permission badges (raw enum values, objectstack-ai/objectui#13353), the three `formatDate` helpers (objectstack-ai/objectui#13458), the Agent key-name prefill (kept by decision), `ConnectAgentWidget` product names (proper nouns). - **`StudioDesignSurface` placeholder:** already localised on `main`; its only remaining literal is `placeholder="https://"`, a URL scheme, not copy. - **`ObjectView.tsx`:** only the Loading-chart region changed; the view-tab regions belong to #25901. - No new lint gate (triage ruling 6407863382). --- _Generated by [Claude Code](https://claude.ai/code/session_01C6T942afhqAP5w9XBmfTDC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b0384f3 commit c86c823

21 files changed

Lines changed: 682 additions & 30 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@object-ui/i18n': patch
3+
'@object-ui/components': patch
4+
'@object-ui/app-shell': patch
5+
'@object-ui/console': patch
6+
---
7+
8+
fix(i18n): the last English literals on the data table, the chart view, react pages, the References sheet and Flow Runs read the user's language
9+
10+
Clause-②: no
11+
Metadata-protocol: none
12+
13+
- **Data table.** The hover "open record" affordance and the inline-edit row's cancel and save buttons are titled from `table.openRecord`, `table.cancelChanges` and `table.saveRow`.
14+
- **Chart view.** A chart view's loading fallback reads the existing `chart.loading` row.
15+
- **`kind:'react'` pages.** The capability notice, the empty-source refusal, the loading line and the two error titles read `reactPage.*`. Sentences with an inline code chip are one translated string, so each language decides where the chip sits.
16+
- **References sheet (Studio).** The table's column headers read `engine.edit.refsColFromType`, `refsColFromName` and `refsColPath` from the Studio's own table, in English and Chinese.
17+
- **Flow Runs.** When the client lacks the method a panel calls (`meta.getItems`, `automation.execute`, `automation.listRuns`), the panel keeps the method's name and words the diagnostic at render from `console.developer.flowRuns.clientMethodUnavailable`. A message the call threw is still shown as it came.
18+
19+
All ten locale packs carry the new rows. With no i18n provider every surface still renders its English.

‎scripts/sdui-manifest.record.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@
4949
"ui/packages/types"
5050
],
5151
"registrySources": 922,
52-
"registrySourcesSha256": "7734fe6bc3763fe8318db624b9e0dd4c48b2dc7b15bf2a3fb1f78e46bb8d959e"
52+
"registrySourcesSha256": "3fe916234f1b5346bff70f6f99322077630b851ebef81c9ae6ba8c4ac08e2063"
5353
}
5454
}

‎ui/apps/console/src/pages/developer/FlowRunsPage.tsx‎

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ import {
5656
Play, Loader2, CheckCircle2, XCircle, Clock, AlertCircle, RefreshCw, Workflow,
5757
} from 'lucide-react';
5858

59+
/**
60+
* A panel's error: the message a call threw, or the client method the panel
61+
* needs and this client lacks. The method is kept by name and worded at render,
62+
* so the diagnostic reads in the viewer's locale (#25916).
63+
*/
64+
type PanelError = { message: string } | { unavailable: string };
65+
66+
function panelErrorText(t: ReturnType<typeof useObjectTranslation>['t'], error: PanelError): string {
67+
return 'unavailable' in error
68+
? t('console.developer.flowRuns.clientMethodUnavailable', {
69+
defaultValue: '{{method}} is not available on this client',
70+
method: error.unavailable,
71+
})
72+
: error.message;
73+
}
74+
5975
interface FlowVariable {
6076
name: string;
6177
type: string;
@@ -197,13 +213,13 @@ export function FlowRunsPage() {
197213

198214
const [flows, setFlows] = useState<FlowItem[]>([]);
199215
const [flowsLoading, setFlowsLoading] = useState(true);
200-
const [flowsError, setFlowsError] = useState<string | null>(null);
216+
const [flowsError, setFlowsError] = useState<PanelError | null>(null);
201217
const [selectedFlowName, setSelectedFlowName] = useState<string>('');
202218
const [refreshRuns, setRefreshRuns] = useState(0);
203219

204220
const loadFlows = useCallback(async () => {
205221
if (!client?.meta?.getItems) {
206-
setFlowsError('meta.getItems is not available on this client');
222+
setFlowsError({ unavailable: 'meta.getItems' });
207223
setFlowsLoading(false);
208224
return;
209225
}
@@ -233,7 +249,7 @@ export function FlowRunsPage() {
233249
setSelectedFlowName(normalized[0].name);
234250
}
235251
} catch (e: any) {
236-
setFlowsError(e?.message ?? String(e));
252+
setFlowsError({ message: e?.message ?? String(e) });
237253
} finally {
238254
setFlowsLoading(false);
239255
}
@@ -276,7 +292,7 @@ export function FlowRunsPage() {
276292
<CardContent>
277293
{flowsLoading && <Skeleton className="h-9 w-72" />}
278294
{flowsError && (
279-
<p className="text-sm text-red-500 font-mono break-all">{flowsError}</p>
295+
<p className="text-sm text-red-500 font-mono break-all">{panelErrorText(t, flowsError)}</p>
280296
)}
281297
{!flowsLoading && !flowsError && flows.length === 0 && (
282298
<p className="text-sm text-muted-foreground">{t('console.developer.flowRuns.noFlows', { defaultValue: 'No flow definitions found.' })}</p>
@@ -335,7 +351,7 @@ function FlowTestRunner({
335351
const [values, setValues] = useState<Record<string, string>>({});
336352
const [running, setRunning] = useState(false);
337353
const [result, setResult] = useState<any>(null);
338-
const [error, setError] = useState<string | null>(null);
354+
const [error, setError] = useState<PanelError | null>(null);
339355
// The paused screen a test run stopped at — driven to completion by
340356
// <FlowRunner>, exactly as a `type: 'flow'` action would.
341357
const [screenFlow, setScreenFlow] = useState<ScreenFlowState | null>(null);
@@ -351,7 +367,7 @@ function FlowTestRunner({
351367

352368
const handleRun = async () => {
353369
if (!client?.automation?.execute) {
354-
setError('automation.execute is not available on this client');
370+
setError({ unavailable: 'automation.execute' });
355371
return;
356372
}
357373
setRunning(true);
@@ -376,7 +392,7 @@ function FlowTestRunner({
376392
}
377393
onExecuted?.();
378394
} catch (e: any) {
379-
setError(e?.message ?? String(e));
395+
setError({ message: e?.message ?? String(e) });
380396
} finally {
381397
setRunning(false);
382398
}
@@ -455,7 +471,7 @@ function FlowTestRunner({
455471
</Badge>
456472
)}
457473
</div>
458-
{error && <p className="text-sm text-red-500 font-mono break-all">{error}</p>}
474+
{error && <p className="text-sm text-red-500 font-mono break-all">{panelErrorText(t, error)}</p>}
459475
{/* A run left paused (the runner was dismissed) can be reopened —
460476
the suspension is durable, so the screen is still waiting. */}
461477
{result?.status === 'paused' && result?.screen && result?.runId && !screenFlow && (
@@ -518,7 +534,7 @@ function FlowRunsPanel({
518534
const displayLocale = useDisplayLocale();
519535
const [runs, setRuns] = useState<RunSummary[]>([]);
520536
const [loading, setLoading] = useState(true);
521-
const [error, setError] = useState<string | null>(null);
537+
const [error, setError] = useState<PanelError | null>(null);
522538

523539
const [openRunId, setOpenRunId] = useState<string | null>(null);
524540
const [runDetail, setRunDetail] = useState<any>(null);
@@ -527,7 +543,7 @@ function FlowRunsPanel({
527543

528544
const loadRuns = useCallback(async () => {
529545
if (!client?.automation?.listRuns) {
530-
setError('automation.listRuns is not available on this client');
546+
setError({ unavailable: 'automation.listRuns' });
531547
setLoading(false);
532548
return;
533549
}
@@ -538,7 +554,7 @@ function FlowRunsPanel({
538554
const items: RunSummary[] = Array.isArray(res) ? res : (res?.items ?? res?.runs ?? []);
539555
setRuns(items);
540556
} catch (e: any) {
541-
setError(e?.message ?? String(e));
557+
setError({ message: e?.message ?? String(e) });
542558
} finally {
543559
setLoading(false);
544560
}
@@ -585,7 +601,7 @@ function FlowRunsPanel({
585601
</div>
586602
)}
587603
{!loading && error && (
588-
<p className="text-sm text-red-500 font-mono break-all">{error}</p>
604+
<p className="text-sm text-red-500 font-mono break-all">{panelErrorText(t, error)}</p>
589605
)}
590606
{!loading && !error && runs.length === 0 && (
591607
<p className="text-sm text-muted-foreground py-6 text-center">
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #25916, third pass — Flow Runs' client-capability diagnostics speak the
5+
* session's language.
6+
*
7+
* Each of the page's three panels calls one client method (`meta.getItems`,
8+
* `automation.execute`, `automation.listRuns`). A client without it used to
9+
* leave an English sentence in the panel's error state. The state now keeps
10+
* the method's NAME and the panel words it at render through
11+
* `console.developer.flowRuns.clientMethodUnavailable`, so under zh the
12+
* sentence is the zh row with the method name in it. A thrown message is
13+
* still drawn verbatim. Control: with no provider the English sentence is
14+
* unchanged.
15+
*/
16+
17+
import { afterEach, describe, expect, it, vi } from 'vitest';
18+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
19+
import { MemoryRouter } from 'react-router-dom';
20+
import { I18nProvider } from '@object-ui/i18n';
21+
import { builtInLocales } from '@object-ui/i18n/locales';
22+
23+
const CLIENT = vi.hoisted(() => ({ current: {} as Record<string, unknown> }));
24+
const ADAPTER = vi.hoisted(() => ({ getClient: () => CLIENT.current }));
25+
vi.mock('@object-ui/app-shell', async (importOriginal) => ({
26+
...(await importOriginal<Record<string, unknown>>()),
27+
useAdapter: () => ADAPTER,
28+
useMetadata: () => ({ objects: [], getTypeStatus: () => 'loaded' }),
29+
}));
30+
31+
// Imported after the mock so the page picks it up.
32+
import { FlowRunsPage } from './FlowRunsPage';
33+
34+
afterEach(() => cleanup());
35+
36+
const { zh } = builtInLocales;
37+
const zhFlow = zh.console.developer.flowRuns;
38+
39+
/** A client that serves one flow but has no automation surface at all. */
40+
const NO_AUTOMATION = { meta: { getItems: async () => ({ type: 'flow', items: [{ name: 'welcome_flow' }] }) } };
41+
/** A client that throws from the one method it has. */
42+
const THROWS = { meta: { getItems: async () => { throw new Error('boom: 503'); } } };
43+
44+
function page(language: 'zh' | null) {
45+
const routed = (
46+
<MemoryRouter>
47+
<FlowRunsPage />
48+
</MemoryRouter>
49+
);
50+
return render(
51+
language ? (
52+
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }} persistLanguage={false}>
53+
{routed}
54+
</I18nProvider>
55+
) : (
56+
routed
57+
),
58+
);
59+
}
60+
61+
const unavailable = (method: string) => zhFlow.clientMethodUnavailable.replace('{{method}}', method);
62+
63+
describe('Flow Runs client diagnostics under zh (#25916)', () => {
64+
it('guard: the zh row is a real zh row', () => {
65+
expect(zhFlow.clientMethodUnavailable).toMatch(/\p{Script=Han}/u);
66+
expect(zhFlow.clientMethodUnavailable).toContain('{{method}}');
67+
});
68+
69+
it('a client without meta.getItems: the flow panel says so in zh', async () => {
70+
CLIENT.current = {};
71+
page('zh');
72+
expect(await screen.findByText(unavailable('meta.getItems'))).toBeTruthy();
73+
});
74+
75+
it('a client without automation: the runs panel and a test run say so in zh', async () => {
76+
CLIENT.current = NO_AUTOMATION;
77+
page('zh');
78+
expect(await screen.findByText(unavailable('automation.listRuns'))).toBeTruthy();
79+
fireEvent.click(screen.getByRole('button', { name: zhFlow.runFlow }));
80+
expect(await screen.findByText(unavailable('automation.execute'))).toBeTruthy();
81+
});
82+
83+
it('a thrown message is still drawn verbatim', async () => {
84+
CLIENT.current = THROWS;
85+
page('zh');
86+
expect(await screen.findByText('boom: 503')).toBeTruthy();
87+
});
88+
});
89+
90+
describe('control: with no provider the diagnostics read their English (#25916)', () => {
91+
it('meta.getItems', async () => {
92+
CLIENT.current = {};
93+
page(null);
94+
expect(await screen.findByText('meta.getItems is not available on this client')).toBeTruthy();
95+
});
96+
97+
it('automation.listRuns and automation.execute', async () => {
98+
CLIENT.current = NO_AUTOMATION;
99+
page(null);
100+
expect(await screen.findByText('automation.listRuns is not available on this client')).toBeTruthy();
101+
fireEvent.click(screen.getByRole('button', { name: 'Run Flow' }));
102+
expect(await screen.findByText('automation.execute is not available on this client')).toBeTruthy();
103+
});
104+
});

‎ui/packages/app-shell/src/views/ObjectView.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3176,7 +3176,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
31763176
const dims: string[] = Array.isArray(chartConfig.dimensions) ? chartConfig.dimensions : [];
31773177
const vals: string[] = Array.isArray(chartConfig.values) ? chartConfig.values : [];
31783178
return withViewTabs(
3179-
<Suspense key={identityKey} fallback={<div className="p-4 text-sm text-muted-foreground">Loading chart…</div>}>
3179+
<Suspense key={identityKey} fallback={<div className="p-4 text-sm text-muted-foreground">{t('chart.loading')}</div>}>
31803180
<ObjectChart
31813181
dataSource={ds}
31823182
schema={{
@@ -3208,7 +3208,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
32083208
// `chart-missing-category-axis`) instead of aggregating on a name
32093209
// nobody wrote. ⛔ Do not restore a floor here.
32103210
return withViewTabs(
3211-
<Suspense key={identityKey} fallback={<div className="p-4 text-sm text-muted-foreground">Loading chart…</div>}>
3211+
<Suspense key={identityKey} fallback={<div className="p-4 text-sm text-muted-foreground">{t('chart.loading')}</div>}>
32123212
<ObjectChart
32133213
dataSource={ds}
32143214
schema={{

0 commit comments

Comments
 (0)