Skip to content

Commit 5399bbc

Browse files
feat(console): second-pass i18n for Audit Log, Flow Runs, Agent Connect and share links (#13503)
Part of #13460 Clause-②: no Metadata-protocol: none Claim: comment 6393192822 (Seat: domain:ui#4). ## What changed Second pass of #13460, following the conventions of #13489: `useObjectTranslation()` with `t(key, { defaultValue })` where the inline default is the `en` value byte for byte, and `withSlots` for a sentence with an inline element. No new lint gate. - **System → Audit Log** (`AuditLogPage.tsx`) reads `console.system.auditLog.*`: the header, the presets row (a refused preset's reason is now a code, worded at render), the filters, the empty and error states, the table heads, the pagination and the detail drawer. `formatDate` is untouched (#13458 is waiting on a ruling). - **Developer → Flow Runs** (`FlowRunsPage.tsx`) reads `console.developer.flowRuns.*`. "Click Run Flow" and "the Test Run panel" name those controls by their translated labels, and "Paused at screen" puts the node name in a slot. - **Integrations → Connect an AI agent** (`AgentConnectSection.tsx`) reads `console.developer.agentConnect.*`. The disabled notice and the steps keep their inline code inside one translated sentence. Copy, Done and Download reuse existing keys. - **ShareDialog** (`packages/components`) reads `shareDialog.*`. The expiry and audience choices are literal `t()` calls built per render, so the dead-key and call-site gates read them. A failure keeps the server's message, or names the step that failed in the viewer's language. - **Public share landing `/s/:token`** (`SharedRecordPage.tsx`) reads `sharedRecord.*`. Failures are kept as codes and worded at render. - All ten packs carry the 135 new keys (25 + 24 + 36 + 30 + 20). - **Dev-only surfaces moved under `dev/`.** `apps/console/src/components/PerformanceDashboard.tsx` (no importers) moved to `apps/console/src/dev/`. `packages/components/src/debug/` moved to `packages/components/src/dev/`. The package index re-exports `./dev`, so its exports are unchanged. ## Tests - New: `apps/console/src/pages/consolePagesLocale-secondPass-13460.test.tsx` renders the four console surfaces under zh through a real `I18nProvider`, reads its expectations from the pack, and checks that no `{{slot}}` or slot mark leaks. Each surface also has a provider-less English control. - New: `packages/components/src/__tests__/share-dialog-locale-13460.test.tsx` covers the ShareDialog chrome under zh, the `recordLabel` hole, and an English control. - Updated: `AgentConnectSection.keyNameConfirm-13383.test.tsx` queries the new echoed keys. `displayLocale-9909.test.tsx` and `ariaLabelLiteralCensus-12145.test.ts` follow the `debug/` to `dev/` move. ## Verification - 32 affected test files under the verify lock: 32 passed, 223 tests passed (VERDICT 0). - `vitest run packages/i18n/`: 87 files, 1353 tests passed, 13 skipped. - `turbo run type-check` for console, components and i18n: 38 of 38 tasks passed. - `check-i18n-call-site-keys`, `check-i18n-en-drift`, `check-i18n-designer-table-parity`: pass. `check-i18n-dead-keys` lists none of the new keys. - `check-changeset-presence`, `check-changeset-no-major`, `check-changeset-claims`, `check-pending-changeset-literals`, `check:new-line-citations`: pass. - eslint on the touched files: 0 errors (warnings only). - NOT MEASURED: the full repo test suite, a browser run of the pages, and translation quality beyond the pack tests. ## Acceptance notes: what is left for #13460 These are not in this PR: - data-table row titles ('Open record', 'Cancel changes', 'Save row') - ObjectView 'Loading chart…' - react-page messages - ResourceEditPage 'From type' / 'From name' / 'Path' - ConnectAgentWidget product names - the StudioDesignSurface placeholder - the console `*-preview.tsx` dev entries (not moved), and the copy inside `apps/console/src/dev/*` - Flow Runs client-capability diagnostics ('meta.getItems is not available on this client' and similar) - the ShareDialog audience and permission badges, which show raw enum values (enum labels belong to #13353) - the 'Agent key' name prefill The brand lines "Powered by ObjectStack" and "Read-only · Shared via ObjectStack" on the share landing are left literal on purpose. `metadata-admin/datasource/**` (#13353) and the three `formatDate` helpers (#13458) are excluded as the claim asks. Deviation: commit 58af2ad ends with a harness-supplied `Co-Authored-By` trailer that names the model, not the model-free pair from AGENTS.md. History is not rewritten, because force-push is forbidden here. Please drop that line from the squash message when landing. - [x] Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018yk9f7S7q4BrEeWZGRcTKF --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent db9af10 commit 5399bbc

25 files changed

Lines changed: 2221 additions & 214 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@object-ui/console": patch
3+
"@object-ui/components": patch
4+
"@object-ui/i18n": patch
5+
---
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
More console surfaces now speak the session's language (objectui#13460, second pass). System → Audit Log, Developer → Flow Runs, the Integrations page's "Connect an AI agent" card, the share-link dialog and the public `/s/:token` share landing drew their copy as English literals. They now read `console.system.auditLog.*`, `console.developer.flowRuns.*`, `console.developer.agentConnect.*`, `shareDialog.*` and `sharedRecord.*`, each with an `en` inline default and a row in all ten packs. A sentence that names another control names it by that control's translated label, and a sentence with inline code is one translated string. Error states are kept as codes and worded at render.
11+
12+
The dev-only surfaces moved under `dev/`: `PerformanceDashboard` to `apps/console/src/dev/`, and the components package's `debug/` folder (the `DebugPanel`) to `dev/`. The package's exports are unchanged.
File renamed without changes.

‎apps/console/src/pages/SharedRecordPage.tsx‎

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
type RawAiMessageRow,
4040
} from '@object-ui/app-shell';
4141
import { ChatbotEnhanced } from '@object-ui/plugin-chatbot';
42+
import { useObjectTranslation } from '@object-ui/i18n';
4243

4344
import {
4445
canSendSharePassword,
@@ -49,6 +50,20 @@ import {
4950
type ResolveErrorBody,
5051
} from './shared-record-shape';
5152

53+
/**
54+
* Why the link did not open. The server's own message is shown as it came;
55+
* every other case is a code the page words in the visitor's language at
56+
* render (objectui#13460), so the resolve callback needs no `t`.
57+
*/
58+
type ShareOpenFailure =
59+
| { kind: 'message'; message: string }
60+
| { kind: 'unsendable-password' }
61+
| { kind: 'wrong-password' }
62+
| { kind: 'status'; status: number }
63+
| { kind: 'invalid' }
64+
| { kind: 'expired' }
65+
| { kind: 'unknown' };
66+
5267
function resolveServerUrl(): string {
5368
const env = (import.meta as any).env ?? {};
5469
const explicit = (env.VITE_SERVER_URL as string | undefined) ?? '';
@@ -58,13 +73,14 @@ function resolveServerUrl(): string {
5873
}
5974

6075
export default function SharedRecordPage() {
76+
const { t } = useObjectTranslation();
6177
const { token } = useParams<{ token: string }>();
6278
const location = useLocation();
6379
const serverUrl = useMemo(() => resolveServerUrl(), []);
6480
const apiBase = `${serverUrl}/api/v1`;
6581

6682
const [data, setData] = useState<ResolvedShare | null>(null);
67-
const [error, setError] = useState<string | null>(null);
83+
const [error, setError] = useState<ShareOpenFailure | null>(null);
6884
const [loading, setLoading] = useState(true);
6985
const [gate, setGate] = useState<'password' | 'sign-in' | null>(null);
7086
const [password, setPassword] = useState('');
@@ -80,9 +96,7 @@ export default function SharedRecordPage() {
8096
if (pw && !canSendSharePassword(pw)) {
8197
// Stays on the prompt: the request is never built, so there is no
8298
// answer to show — only why nothing was sent.
83-
setError(
84-
"This password has characters that can't be sent, such as Chinese characters or emoji. Ask the link's owner to set a password without them.",
85-
);
99+
setError({ kind: 'unsendable-password' });
86100
return;
87101
}
88102
setLoading(true);
@@ -99,27 +113,29 @@ export default function SharedRecordPage() {
99113
setGate('sign-in');
100114
} else if (answer) {
101115
setGate('password');
102-
setError(answer === 'wrong-password' ? 'Wrong password.' : null);
116+
setError(answer === 'wrong-password' ? { kind: 'wrong-password' } : null);
103117
} else {
104118
setGate(null);
105-
setError(body?.error?.message ?? `Failed to load (HTTP ${res.status}).`);
119+
const message = body?.error?.message;
120+
setError(message ? { kind: 'message', message } : { kind: 'status', status: res.status });
106121
}
107122
setLoading(false);
108123
return;
109124
}
110125
if (res.status === 404) {
111-
setError('This link is invalid or no longer available.');
126+
setError({ kind: 'invalid' });
112127
setLoading(false);
113128
return;
114129
}
115130
if (res.status === 410) {
116-
setError('This link has expired or was revoked.');
131+
setError({ kind: 'expired' });
117132
setLoading(false);
118133
return;
119134
}
120135
if (!res.ok) {
121136
const body = await res.json().catch(() => null);
122-
setError(body?.error?.message ?? `Failed to load (HTTP ${res.status}).`);
137+
const message = body?.error?.message;
138+
setError(message ? { kind: 'message', message } : { kind: 'status', status: res.status });
123139
setLoading(false);
124140
return;
125141
}
@@ -129,7 +145,7 @@ export default function SharedRecordPage() {
129145
setData(resolved);
130146
setGate(null);
131147
} catch (e: any) {
132-
setError(e?.message ?? 'Failed to load shared content.');
148+
setError(e?.message ? { kind: 'message', message: e.message } : { kind: 'unknown' });
133149
} finally {
134150
setLoading(false);
135151
}
@@ -167,11 +183,35 @@ export default function SharedRecordPage() {
167183
[messages],
168184
);
169185

186+
const errorText = (() => {
187+
switch (error?.kind) {
188+
case undefined:
189+
return null;
190+
case 'message':
191+
return error.message;
192+
case 'unsendable-password':
193+
return t('sharedRecord.passwordUnsendable', {
194+
defaultValue:
195+
"This password has characters that can't be sent, such as Chinese characters or emoji. Ask the link's owner to set a password without them.",
196+
});
197+
case 'wrong-password':
198+
return t('sharedRecord.wrongPassword', { defaultValue: 'Wrong password.' });
199+
case 'status':
200+
return t('sharedRecord.loadFailedStatus', { defaultValue: 'Failed to load (HTTP {{status}}).', status: error.status });
201+
case 'invalid':
202+
return t('sharedRecord.invalidLink', { defaultValue: 'This link is invalid or no longer available.' });
203+
case 'expired':
204+
return t('sharedRecord.expiredLink', { defaultValue: 'This link has expired or was revoked.' });
205+
case 'unknown':
206+
return t('sharedRecord.loadFailed', { defaultValue: 'Failed to load shared content.' });
207+
}
208+
})();
209+
170210
if (loading) {
171211
return (
172212
<div className="flex h-svh w-full items-center justify-center bg-background text-muted-foreground">
173213
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
174-
Loading…
214+
{t('common.loading', { defaultValue: 'Loading…' })}
175215
</div>
176216
);
177217
}
@@ -189,12 +229,12 @@ export default function SharedRecordPage() {
189229
return (
190230
<div className="mx-auto flex h-svh max-w-md flex-col items-center justify-center gap-4 px-4 text-center">
191231
<LogIn className="h-8 w-8 text-muted-foreground" />
192-
<h1 className="text-lg font-semibold">Sign in required</h1>
232+
<h1 className="text-lg font-semibold">{t('sharedRecord.signInTitle', { defaultValue: 'Sign in required' })}</h1>
193233
<p className="text-sm text-muted-foreground">
194-
The owner shared this link with signed-in users only.
234+
{t('sharedRecord.signInBody', { defaultValue: 'The owner shared this link with signed-in users only.' })}
195235
</p>
196236
<Button asChild size="sm">
197-
<Link to={signInPath}>Sign in</Link>
237+
<Link to={signInPath}>{t('sharedRecord.signIn', { defaultValue: 'Sign in' })}</Link>
198238
</Button>
199239
</div>
200240
);
@@ -204,9 +244,9 @@ export default function SharedRecordPage() {
204244
return (
205245
<div className="mx-auto flex h-svh max-w-md flex-col items-center justify-center gap-4 px-4 text-center">
206246
<Link2 className="h-8 w-8 text-muted-foreground" />
207-
<h1 className="text-lg font-semibold">Password required</h1>
247+
<h1 className="text-lg font-semibold">{t('sharedRecord.passwordTitle', { defaultValue: 'Password required' })}</h1>
208248
<p className="text-sm text-muted-foreground">
209-
The owner protected this link with a password.
249+
{t('sharedRecord.passwordBody', { defaultValue: 'The owner protected this link with a password.' })}
210250
</p>
211251
<form
212252
className="flex w-full flex-col gap-2"
@@ -216,31 +256,31 @@ export default function SharedRecordPage() {
216256
}}
217257
>
218258
<Label htmlFor="share-pw" className="sr-only">
219-
Password
259+
{t('sharedRecord.passwordLabel', { defaultValue: 'Password' })}
220260
</Label>
221261
<Input
222262
id="share-pw"
223263
type="password"
224264
value={password}
225265
onChange={(e) => setPassword(e.target.value)}
226266
autoFocus
227-
placeholder="Enter password"
267+
placeholder={t('sharedRecord.passwordPlaceholder', { defaultValue: 'Enter password' })}
228268
/>
229-
{error && <p className="text-xs text-destructive">{error}</p>}
269+
{errorText !== null && <p className="text-xs text-destructive">{errorText}</p>}
230270
<Button type="submit" size="sm">
231-
Continue
271+
{t('sharedRecord.continue', { defaultValue: 'Continue' })}
232272
</Button>
233273
</form>
234274
</div>
235275
);
236276
}
237277

238-
if (error) {
278+
if (errorText !== null) {
239279
return (
240280
<div className="mx-auto flex h-svh max-w-md flex-col items-center justify-center gap-3 px-4 text-center">
241281
<AlertTriangle className="h-8 w-8 text-amber-500" />
242-
<h1 className="text-lg font-semibold">Can't open this link</h1>
243-
<p className="text-sm text-muted-foreground">{error}</p>
282+
<h1 className="text-lg font-semibold">{t('sharedRecord.cannotOpen', { defaultValue: "Can't open this link" })}</h1>
283+
<p className="text-sm text-muted-foreground">{errorText}</p>
244284
</div>
245285
);
246286
}
@@ -255,7 +295,7 @@ export default function SharedRecordPage() {
255295
<MessageSquare className="h-5 w-5 text-muted-foreground" />
256296
<div className="min-w-0 flex-1">
257297
<h1 className="truncate text-sm font-semibold">
258-
{conv.title ?? 'Shared conversation'}
298+
{conv.title ?? t('sharedRecord.sharedConversation', { defaultValue: 'Shared conversation' })}
259299
</h1>
260300
<p className="truncate text-[11px] text-muted-foreground">
261301
Read-only · Shared via ObjectStack
@@ -269,11 +309,11 @@ export default function SharedRecordPage() {
269309
{messages == null ? (
270310
<div className="flex flex-1 items-center justify-center py-12 text-sm text-muted-foreground">
271311
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
272-
Loading messages…
312+
{t('sharedRecord.loadingMessages', { defaultValue: 'Loading messages…' })}
273313
</div>
274314
) : messages.length === 0 ? (
275315
<p className="py-12 text-center text-sm text-muted-foreground">
276-
This conversation has no messages yet.
316+
{t('sharedRecord.noMessages', { defaultValue: 'This conversation has no messages yet.' })}
277317
</p>
278318
) : (
279319
<ChatbotEnhanced
@@ -298,7 +338,7 @@ export default function SharedRecordPage() {
298338
<div className="mx-auto flex h-svh max-w-3xl flex-col bg-background">
299339
<header className="border-b px-4 py-3">
300340
<h1 className="text-sm font-semibold">
301-
Shared {data.link.object_name}
341+
{t('sharedRecord.sharedObject', { defaultValue: 'Shared {{object}}', object: data.link.object_name })}
302342
</h1>
303343
</header>
304344
<main className="flex-1 overflow-y-auto p-4">
@@ -307,7 +347,10 @@ export default function SharedRecordPage() {
307347
</pre>
308348
{data.redactedFields && data.redactedFields.length > 0 && (
309349
<p className="mt-2 text-[11px] text-muted-foreground">
310-
Some fields are hidden by the owner: {data.redactedFields.join(', ')}
350+
{t('sharedRecord.redactedFields', {
351+
defaultValue: 'Some fields are hidden by the owner: {{fields}}',
352+
fields: data.redactedFields.join(', '),
353+
})}
311354
</p>
312355
)}
313356
</main>

0 commit comments

Comments
 (0)