Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit d0c9650

Browse files
fix(console, app-shell): localise API Console, Public Forms, Integrations, AI Approvals and the datasource page (objectui#13460) (#13489)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DTZuga4rm1n3SpVeS6ojvFA)_ Part of #13460 Clause-②: no Metadata-protocol: none Claim: objectui#13460 comment 6392274945, amended by comment 6392316827 (file surface adds `packages/app-shell/src/views/metadata-admin/i18n.ts`). Seat: `domain:ui#4`. ## What changed Four routed console pages imported nothing from `@object-ui/i18n`, and the Studio datasource page drew its copy as English literals, so a zh / ja / ar operator got English on these screens. - **Developer → API Console** (`ApiConsolePage.tsx`), **Developer → Public Forms** (`PublicFormsPage.tsx`), **Developer → Integrations & APIs** (`IntegrationsPage.tsx`) and **System → AI Approvals** (`AiPendingActionsPage.tsx`) read `console.developer.apiConsole.*`, `console.developer.publicForms.*`, `console.developer.integrations.*` and `console.system.aiApprovals.*` through `useObjectTranslation`. 108 new keys, translated in all ten packs (no English copies). Cancel / Save / Refresh / Actions reuse `common.*`. Each call carries the `en` value as its inline `defaultValue` (held byte-identical by `pnpm check:i18n-keys`), so a provider-less mount and every existing English test read exactly what they read before. - A sentence with an inline element (a bold action name, a `<code>` key) is ONE translated string: the element's `{{slot}}` is filled with a private-use mark and the sentence is split on it, so each language places the element (ja and ko put the action after its object). Helper: `apps/console/src/lib/inlineSlots.tsx` (`withSlots`), the same technique as `element:metadata_viewer`'s chips (objectui#12910). - Aria names that used to be glued as `` `Copy ${label}` `` are whole translated keys (`copyBaseUrl`, `copySample`); toasts likewise (`copiedUrl`, `copiedIframe`, `copiedReact`). - **Setup → Datasources** (`DatasourceResourcePage.tsx`) reads 55 new `engine.datasource.*` rows in the Studio table (`metadata-admin/i18n.ts`, en-US + zh-CN). That page already read `engine.*`, and `packages/i18n/README.md` § "Scope — the engine.* carve-out" keeps that namespace out of the ten packs. Covered: heading, toolbar, empty state (action slot), status chips, row actions, the create/edit dialog including the Schema mode options, the sync dialog, and the toasts. The driver config schema's enum option values are untouched (objectui#13353). - Left as written on purpose: the cURL / iframe / React snippets (code), HTTP verbs, paths and status codes, example slugs and paths in placeholders (`contact-us`, `/thanks`, `my_database`, `/api/v1/...`), server messages, and the redirect refusal (the spec's own prose). - Typography: three new English values spell the ellipsis U+2026 (`Search endpoints…`, `Discovering APIs…`, `Sending request…`; they were `...`), because `ellipsis-glyph-3878` holds every pack value to it. ## Tests - `apps/console/src/pages/developer/consolePagesLocale-13460.test.tsx` (new): under zh each page draws its heading and copy from the zh pack (guarded as real Han-script rows, not English copies); the three slotted sentences render with the element where the zh string puts it and no `{{` or slot mark leaks; control: with no provider each page still reads its English. - `packages/app-shell/src/views/metadata-admin/datasource/DatasourceResourcePage.locale-13460.test.tsx` (new): zh heading, empty state with its action slot, status chip, row actions and the create dialog read the zh rows; en control. - `DatasourceResourcePage.selectPlaceholderLocale-11252.test.tsx`: finds the New datasource button by its locale's row (the button is now translated). `ApiConsolePage.requestPreset-10591.test.tsx`: the `Sending request…` glyph. ## Verification (local, objectui `main` `c85408f` + this branch) - `turbo run type-check` for `@object-ui/console`, `@object-ui/app-shell`, `@object-ui/i18n` (38 tasks incl. deps): pass. - `vitest run packages/i18n/`: 87 files, 1353 passed. - `vitest run` over every test file importing the five pages, `inlineSlots`, or reading the Studio table's source (32 files): 559 passed. - `node scripts/check-i18n-call-site-keys.mjs`, `check-i18n-en-drift.mjs`, `check-i18n-dead-keys.mjs`, `check-i18n-designer-table-parity.mjs`: exit 0. - `eslint` on the touched sources: 0 errors, and the same warning set as `main` (no new findings). - `check-changeset-presence.mjs`, `check-changeset-no-major.mjs`, `check:new-line-citations`: pass. - NOT MEASURED locally, left to CI: the sharded full `pnpm test`; `check:eager-closure` (the eager `en` catalogue grows by the new keys; `i18n-locale-en` ceiling is in `scripts/check-eager-closure-budget.mjs`); `check:readme-exports` (needs every dist). ## Acceptance notes - Out of this PR by the claim, left on the card: objectui#13353 (enum option labels), moving dev-only surfaces under `dev/`, and the remaining literals on other surfaces (`AuditLogPage`, `FlowRunsPage`, `ShareDialog`, …). - The Studio table covers en + zh only (the carve-out), so the datasource page renders English in the other eight languages, as every other Studio screen does. - Overlap: the objectui#13461 card thread may add keys to the same ten packs; whichever lands second merges `main`. - [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_01QY7vnZv639mvLbooY6nBKU --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6c3340c commit d0c9650

22 files changed

Lines changed: 1979 additions & 187 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@object-ui/console": patch
3+
"@object-ui/app-shell": patch
4+
"@object-ui/i18n": patch
5+
---
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
fix(console, app-shell): four routed console pages and the Studio datasource page speak the session's language instead of hard-coded English (objectui#13460).
11+
12+
- Developer → API Console, Developer → Public Forms, Developer → Integrations & APIs and System → AI Approvals read their copy from new `console.developer.apiConsole.*`, `console.developer.publicForms.*`, `console.developer.integrations.*` and `console.system.aiApprovals.*` keys, translated in all ten locale packs. Sentences with an inline bold action or `<code>` key are one translated string each, so every language places the element itself.
13+
- Setup → Datasources reads its copy from new `engine.datasource.*` rows in the Studio table (en-US and zh-CN, the `engine.*` carve-out). The driver config form's enum option values are unchanged.
14+
- English renders exactly as before, with or without an i18n provider. Code samples, verbs, paths, example slugs and server messages stay as written.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Sentences with an inline element in them — a bold action name, a `<code>`
3+
* key — translated as ONE string (objectui#13460).
4+
*
5+
* Splitting such a sentence into a before-part and an after-part would fix the
6+
* word order to English's: ja and ko put the action after its object. So each
7+
* element's `{{slot}}` is interpolated with a private-use mark from
8+
* {@link SLOT}, and {@link withSlots} splits the translated sentence on those
9+
* marks, drawing the element where the language put it. The same technique as
10+
* `element:metadata_viewer`'s chips (objectui#12910).
11+
*/
12+
13+
import { Fragment, type ReactNode } from 'react';
14+
15+
/** The marks a `{{slot}}` is filled with: slot `i` is drawn as `nodes[i]`. */
16+
export const SLOT = ['', '', ''] as const;
17+
18+
/** The translated `text` with `nodes[i]` drawn where `SLOT[i]` was interpolated. */
19+
export function withSlots(text: string, nodes: readonly ReactNode[]): ReactNode {
20+
return text.split(/([-])/u).map((part, i) => {
21+
const at = (SLOT as readonly string[]).indexOf(part);
22+
return <Fragment key={i}>{at === -1 ? part : nodes[at]}</Fragment>;
23+
});
24+
}

‎apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ describe('ApiConsolePage request preset (objectui#10591)', () => {
186186
// with no discovery payload), then check nothing hit the preset.
187187
expect(await screen.findByText('Auth')).toBeInTheDocument();
188188
expect(fetchedUrls().some((u) => u.includes('/ai/tools/'))).toBe(false);
189-
expect(screen.queryByText('Sending request...')).not.toBeInTheDocument();
189+
expect(screen.queryByText('Sending request…')).not.toBeInTheDocument();
190190
});
191191

192192
it('control: with no query string it opens exactly as before', async () => {

‎apps/console/src/pages/developer/ApiConsolePage.tsx‎

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import { useState, useCallback, useMemo } from 'react';
3131
import { useSearchParams } from 'react-router-dom';
3232
import { useAdapter } from '@object-ui/app-shell';
33+
import { useObjectTranslation } from '@object-ui/i18n';
3334
import {
3435
Badge,
3536
Collapsible,
@@ -46,6 +47,7 @@ import {
4647
Loader2, Search, RefreshCw, Trash2, Plus, X,
4748
} from 'lucide-react';
4849
import { useApiDiscovery, type EndpointDef, type HttpMethod } from './hooks/useApiDiscovery';
50+
import { SLOT, withSlots } from '../../lib/inlineSlots';
4951

5052
const METHOD_COLORS: Record<string, string> = {
5153
GET: 'text-emerald-600 bg-emerald-50 border-emerald-200 dark:text-emerald-400 dark:bg-emerald-950/50',
@@ -62,14 +64,8 @@ const STATUS_COLORS: Record<string, string> = {
6264
'5': 'text-red-600 dark:text-red-400',
6365
};
6466

65-
const QUERY_PARAM_PRESETS = [
66-
{ key: '$top', placeholder: '10', hint: 'limit' },
67-
{ key: '$skip', placeholder: '0', hint: 'offset' },
68-
{ key: '$sort', placeholder: 'name', hint: 'sort field' },
69-
{ key: '$select', placeholder: 'name,email', hint: 'fields' },
70-
{ key: '$count', placeholder: 'true', hint: 'total count' },
71-
{ key: '$filter', placeholder: "status eq 'active'", hint: 'OData filter' },
72-
];
67+
/** The query-parameter preset chips, in the order the panel offers them; each chip's hint is translated at render. */
68+
const QUERY_PARAM_PRESETS = ['$top', '$skip', '$sort', '$select', '$count', '$filter'] as const;
7369

7470
interface QueryParam {
7571
id: number;
@@ -154,6 +150,11 @@ function MethodPicker({
154150
}
155151

156152
export function ApiConsolePage() {
153+
// objectui#13460 — the page's copy reads `console.developer.apiConsole.*`;
154+
// each inline `defaultValue` is the `en` pack's value, held byte-identical to
155+
// it by `pnpm check:i18n-keys`, so a provider-less mount still reads English.
156+
// Verbs, paths, status codes and the JSON placeholders stay as written.
157+
const { t } = useObjectTranslation();
157158
const adapter = useAdapter();
158159
const client: any = adapter?.getClient?.();
159160
const { groups, loading: discovering, refresh } = useApiDiscovery();
@@ -281,13 +282,16 @@ export function ApiConsolePage() {
281282
const duration = Math.round(performance.now() - start);
282283
setResponse({
283284
status: 0,
284-
body: `Network Error: ${err.message}`,
285+
body: t('console.developer.apiConsole.networkError', {
286+
defaultValue: 'Network Error: {{message}}',
287+
message: err.message,
288+
}),
285289
duration,
286290
});
287291
} finally {
288292
setLoading(false);
289293
}
290-
}, [client, effectiveUrl, effectiveMethod, requestBody, loading]);
294+
}, [client, effectiveUrl, effectiveMethod, requestBody, loading, t]);
291295

292296
const replayHistoryEntry = useCallback((entry: RequestHistoryEntry) => {
293297
setMethodOverride(entry.method);
@@ -318,18 +322,29 @@ export function ApiConsolePage() {
318322

319323
const statusColor = (status: number) => STATUS_COLORS[String(status)[0]] ?? 'text-muted-foreground';
320324

325+
const presetHints: Record<(typeof QUERY_PARAM_PRESETS)[number], string> = {
326+
$top: t('console.developer.apiConsole.hintLimit', { defaultValue: 'limit' }),
327+
$skip: t('console.developer.apiConsole.hintOffset', { defaultValue: 'offset' }),
328+
$sort: t('console.developer.apiConsole.hintSort', { defaultValue: 'sort field' }),
329+
$select: t('console.developer.apiConsole.hintSelect', { defaultValue: 'fields' }),
330+
$count: t('console.developer.apiConsole.hintCount', { defaultValue: 'total count' }),
331+
$filter: t('console.developer.apiConsole.hintFilter', { defaultValue: 'OData filter' }),
332+
};
333+
321334
return (
322335
<div className="flex flex-1 min-h-0 overflow-hidden">
323336

324337
<div className="w-72 shrink-0 border-r flex flex-col overflow-hidden">
325338
<div className="p-3 border-b space-y-2">
326339
<div className="flex items-center gap-2">
327-
<h3 className="text-sm font-semibold text-foreground flex-1">API Endpoints</h3>
340+
<h3 className="text-sm font-semibold text-foreground flex-1">
341+
{t('console.developer.apiConsole.endpointsTitle', { defaultValue: 'API Endpoints' })}
342+
</h3>
328343
<button type="button"
329344
onClick={refresh}
330345
disabled={discovering}
331346
className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
332-
title="Refresh endpoints"
347+
title={t('console.developer.apiConsole.refreshEndpoints', { defaultValue: 'Refresh endpoints' })}
333348
>
334349
<RefreshCw className={`h-3.5 w-3.5 ${discovering ? 'animate-spin' : ''}`} />
335350
</button>
@@ -340,7 +355,7 @@ export function ApiConsolePage() {
340355
type="text"
341356
value={searchQuery}
342357
onChange={e => setSearchQuery(e.target.value)}
343-
placeholder="Search endpoints..."
358+
placeholder={t('console.developer.apiConsole.searchPlaceholder', { defaultValue: 'Search endpoints…' })}
344359
className="w-full rounded-md border bg-background pl-8 pr-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
345360
/>
346361
</div>
@@ -350,12 +365,16 @@ export function ApiConsolePage() {
350365
{discovering ? (
351366
<div className="flex items-center justify-center py-8 text-muted-foreground text-sm">
352367
<Loader2 className="h-4 w-4 animate-spin mr-2" />
353-
Discovering APIs...
368+
{t('console.developer.apiConsole.discovering', { defaultValue: 'Discovering APIs…' })}
354369
</div>
355370
) : filteredGroups.length === 0 ? (
356371
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground text-sm">
357372
<Globe className="h-6 w-6 opacity-30 mb-2" />
358-
<span>{searchQuery ? 'No matching endpoints' : 'No endpoints found'}</span>
373+
<span>
374+
{searchQuery
375+
? t('console.developer.apiConsole.noMatching', { defaultValue: 'No matching endpoints' })
376+
: t('console.developer.apiConsole.noEndpoints', { defaultValue: 'No endpoints found' })}
377+
</span>
359378
</div>
360379
) : (
361380
filteredGroups.map(group => {
@@ -400,25 +419,27 @@ export function ApiConsolePage() {
400419

401420
<div className="p-3 border-t space-y-2">
402421
<div className="flex items-center justify-between">
403-
<h4 className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">Query Parameters</h4>
422+
<h4 className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
423+
{t('console.developer.apiConsole.queryParameters', { defaultValue: 'Query Parameters' })}
424+
</h4>
404425
<button type="button"
405426
onClick={() => addQueryParam()}
406427
className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground hover:text-foreground transition-colors"
407428
>
408429
<Plus className="h-3 w-3" />
409-
Add
430+
{t('console.developer.apiConsole.addParameter', { defaultValue: 'Add' })}
410431
</button>
411432
</div>
412433

413434
<div className="flex flex-wrap gap-1">
414435
{QUERY_PARAM_PRESETS.map(preset => (
415436
<button type="button"
416-
key={preset.key}
417-
onClick={() => addQueryParam(preset.key, '')}
437+
key={preset}
438+
onClick={() => addQueryParam(preset, '')}
418439
className="text-[10px] px-1.5 py-0.5 rounded border bg-muted/30 text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-colors font-mono"
419-
title={preset.hint}
440+
title={presetHints[preset]}
420441
>
421-
{preset.key}
442+
{preset}
422443
</button>
423444
))}
424445
</div>
@@ -437,14 +458,14 @@ export function ApiConsolePage() {
437458
type="text"
438459
value={param.key}
439460
onChange={e => updateQueryParam(param.id, 'key', e.target.value)}
440-
placeholder="key"
461+
placeholder={t('console.developer.apiConsole.parameterKey', { defaultValue: 'key' })}
441462
className="flex-1 min-w-0 rounded border bg-background px-1.5 py-0.5 text-[10px] font-mono focus:outline-none focus:ring-1 focus:ring-ring"
442463
/>
443464
<input
444465
type="text"
445466
value={param.value}
446467
onChange={e => updateQueryParam(param.id, 'value', e.target.value)}
447-
placeholder="value"
468+
placeholder={t('console.developer.apiConsole.parameterValue', { defaultValue: 'value' })}
448469
className="flex-1 min-w-0 rounded border bg-background px-1.5 py-0.5 text-[10px] font-mono focus:outline-none focus:ring-1 focus:ring-ring"
449470
/>
450471
<button type="button"
@@ -478,7 +499,7 @@ export function ApiConsolePage() {
478499
className="inline-flex items-center gap-1.5 rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
479500
>
480501
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Play className="h-3.5 w-3.5" />}
481-
Send
502+
{t('console.developer.apiConsole.send', { defaultValue: 'Send' })}
482503
</button>
483504
</div>
484505
{queryParams.some(p => p.enabled && p.key.trim()) && (
@@ -491,7 +512,9 @@ export function ApiConsolePage() {
491512
<div className="flex-1 flex flex-col min-h-0 overflow-auto">
492513
{['POST', 'PATCH', 'PUT'].includes(effectiveMethod) && (
493514
<div className="p-3 border-b space-y-1">
494-
<label className="text-xs font-medium text-muted-foreground">Request Body (JSON)</label>
515+
<label className="text-xs font-medium text-muted-foreground">
516+
{t('console.developer.apiConsole.requestBody', { defaultValue: 'Request Body (JSON)' })}
517+
</label>
495518
<textarea
496519
value={requestBody}
497520
onChange={e => setRequestBody(e.target.value)}
@@ -505,7 +528,7 @@ export function ApiConsolePage() {
505528
{loading && (
506529
<div className="flex items-center justify-center py-8 text-muted-foreground text-sm">
507530
<Loader2 className="h-4 w-4 animate-spin mr-2" />
508-
Sending request...
531+
{t('console.developer.apiConsole.sending', { defaultValue: 'Sending request…' })}
509532
</div>
510533
)}
511534

@@ -526,7 +549,9 @@ export function ApiConsolePage() {
526549
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
527550
>
528551
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
529-
{copied ? 'Copied' : 'Copy'}
552+
{copied
553+
? t('console.developer.apiConsole.copied', { defaultValue: 'Copied' })
554+
: t('console.developer.apiConsole.copy', { defaultValue: 'Copy' })}
530555
</button>
531556
</div>
532557
<pre className="flex-1 overflow-auto p-3 text-xs font-mono bg-muted/10 whitespace-pre-wrap break-all">
@@ -539,8 +564,20 @@ export function ApiConsolePage() {
539564
<div className="flex-1 flex items-center justify-center text-muted-foreground text-sm">
540565
<div className="text-center space-y-2">
541566
<Globe className="h-10 w-10 mx-auto opacity-20" />
542-
<p>Select an endpoint or type a URL and click <strong>Send</strong></p>
543-
<p className="text-xs opacity-60">All registered APIs are auto-discovered from the platform</p>
567+
<p>
568+
{withSlots(
569+
t('console.developer.apiConsole.emptyHint', {
570+
defaultValue: 'Select an endpoint or type a URL and click {{send}}',
571+
send: SLOT[0],
572+
}),
573+
[<strong key="send">{t('console.developer.apiConsole.send', { defaultValue: 'Send' })}</strong>],
574+
)}
575+
</p>
576+
<p className="text-xs opacity-60">
577+
{t('console.developer.apiConsole.emptySubhint', {
578+
defaultValue: 'All registered APIs are auto-discovered from the platform',
579+
})}
580+
</p>
544581
</div>
545582
</div>
546583
)}
@@ -549,14 +586,14 @@ export function ApiConsolePage() {
549586
<div className="border-t">
550587
<div className="flex items-center justify-between px-3 py-2">
551588
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
552-
History ({history.length})
589+
{t('console.developer.apiConsole.history', { defaultValue: 'History ({{total}})', total: history.length })}
553590
</h4>
554591
<button type="button"
555592
onClick={() => setHistory([])}
556593
className="inline-flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground transition-colors"
557594
>
558595
<Trash2 className="h-3 w-3" />
559-
Clear
596+
{t('console.developer.apiConsole.clearHistory', { defaultValue: 'Clear' })}
560597
</button>
561598
</div>
562599
<div className="space-y-0.5 px-2 pb-2 max-h-60 overflow-auto">
@@ -581,7 +618,7 @@ export function ApiConsolePage() {
581618
<button type="button"
582619
onClick={e => { e.stopPropagation(); replayHistoryEntry(entry); }}
583620
className="shrink-0 p-0.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
584-
title="Replay request"
621+
title={t('console.developer.apiConsole.replay', { defaultValue: 'Replay request' })}
585622
>
586623
<Play className="h-2.5 w-2.5" />
587624
</button>

0 commit comments

Comments
 (0)