diff --git a/.changeset/13460-console-pages-i18n.md b/.changeset/13460-console-pages-i18n.md new file mode 100644 index 0000000000..0be712a114 --- /dev/null +++ b/.changeset/13460-console-pages-i18n.md @@ -0,0 +1,14 @@ +--- +"@object-ui/console": patch +"@object-ui/app-shell": patch +"@object-ui/i18n": patch +--- + +Clause-②: no +Metadata-protocol: none + +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). + +- 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 `` key are one translated string each, so every language places the element itself. +- 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. +- English renders exactly as before, with or without an i18n provider. Code samples, verbs, paths, example slugs and server messages stay as written. diff --git a/apps/console/src/lib/inlineSlots.tsx b/apps/console/src/lib/inlineSlots.tsx new file mode 100644 index 0000000000..9acc652526 --- /dev/null +++ b/apps/console/src/lib/inlineSlots.tsx @@ -0,0 +1,24 @@ +/** + * Sentences with an inline element in them — a bold action name, a `` + * key — translated as ONE string (objectui#13460). + * + * Splitting such a sentence into a before-part and an after-part would fix the + * word order to English's: ja and ko put the action after its object. So each + * element's `{{slot}}` is interpolated with a private-use mark from + * {@link SLOT}, and {@link withSlots} splits the translated sentence on those + * marks, drawing the element where the language put it. The same technique as + * `element:metadata_viewer`'s chips (objectui#12910). + */ + +import { Fragment, type ReactNode } from 'react'; + +/** The marks a `{{slot}}` is filled with: slot `i` is drawn as `nodes[i]`. */ +export const SLOT = ['', '', ''] as const; + +/** The translated `text` with `nodes[i]` drawn where `SLOT[i]` was interpolated. */ +export function withSlots(text: string, nodes: readonly ReactNode[]): ReactNode { + return text.split(/([-])/u).map((part, i) => { + const at = (SLOT as readonly string[]).indexOf(part); + return {at === -1 ? part : nodes[at]}; + }); +} diff --git a/apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx b/apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx index 46c406281c..afcd00e358 100644 --- a/apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx +++ b/apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx @@ -186,7 +186,7 @@ describe('ApiConsolePage request preset (objectui#10591)', () => { // with no discovery payload), then check nothing hit the preset. expect(await screen.findByText('Auth')).toBeInTheDocument(); expect(fetchedUrls().some((u) => u.includes('/ai/tools/'))).toBe(false); - expect(screen.queryByText('Sending request...')).not.toBeInTheDocument(); + expect(screen.queryByText('Sending request…')).not.toBeInTheDocument(); }); it('control: with no query string it opens exactly as before', async () => { diff --git a/apps/console/src/pages/developer/ApiConsolePage.tsx b/apps/console/src/pages/developer/ApiConsolePage.tsx index d10ed41ae5..ac6654b64c 100644 --- a/apps/console/src/pages/developer/ApiConsolePage.tsx +++ b/apps/console/src/pages/developer/ApiConsolePage.tsx @@ -30,6 +30,7 @@ import { useState, useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useAdapter } from '@object-ui/app-shell'; +import { useObjectTranslation } from '@object-ui/i18n'; import { Badge, Collapsible, @@ -46,6 +47,7 @@ import { Loader2, Search, RefreshCw, Trash2, Plus, X, } from 'lucide-react'; import { useApiDiscovery, type EndpointDef, type HttpMethod } from './hooks/useApiDiscovery'; +import { SLOT, withSlots } from '../../lib/inlineSlots'; const METHOD_COLORS: Record = { 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 = { '5': 'text-red-600 dark:text-red-400', }; -const QUERY_PARAM_PRESETS = [ - { key: '$top', placeholder: '10', hint: 'limit' }, - { key: '$skip', placeholder: '0', hint: 'offset' }, - { key: '$sort', placeholder: 'name', hint: 'sort field' }, - { key: '$select', placeholder: 'name,email', hint: 'fields' }, - { key: '$count', placeholder: 'true', hint: 'total count' }, - { key: '$filter', placeholder: "status eq 'active'", hint: 'OData filter' }, -]; +/** The query-parameter preset chips, in the order the panel offers them; each chip's hint is translated at render. */ +const QUERY_PARAM_PRESETS = ['$top', '$skip', '$sort', '$select', '$count', '$filter'] as const; interface QueryParam { id: number; @@ -154,6 +150,11 @@ function MethodPicker({ } export function ApiConsolePage() { + // objectui#13460 — the page's copy reads `console.developer.apiConsole.*`; + // each inline `defaultValue` is the `en` pack's value, held byte-identical to + // it by `pnpm check:i18n-keys`, so a provider-less mount still reads English. + // Verbs, paths, status codes and the JSON placeholders stay as written. + const { t } = useObjectTranslation(); const adapter = useAdapter(); const client: any = adapter?.getClient?.(); const { groups, loading: discovering, refresh } = useApiDiscovery(); @@ -281,13 +282,16 @@ export function ApiConsolePage() { const duration = Math.round(performance.now() - start); setResponse({ status: 0, - body: `Network Error: ${err.message}`, + body: t('console.developer.apiConsole.networkError', { + defaultValue: 'Network Error: {{message}}', + message: err.message, + }), duration, }); } finally { setLoading(false); } - }, [client, effectiveUrl, effectiveMethod, requestBody, loading]); + }, [client, effectiveUrl, effectiveMethod, requestBody, loading, t]); const replayHistoryEntry = useCallback((entry: RequestHistoryEntry) => { setMethodOverride(entry.method); @@ -318,18 +322,29 @@ export function ApiConsolePage() { const statusColor = (status: number) => STATUS_COLORS[String(status)[0]] ?? 'text-muted-foreground'; + const presetHints: Record<(typeof QUERY_PARAM_PRESETS)[number], string> = { + $top: t('console.developer.apiConsole.hintLimit', { defaultValue: 'limit' }), + $skip: t('console.developer.apiConsole.hintOffset', { defaultValue: 'offset' }), + $sort: t('console.developer.apiConsole.hintSort', { defaultValue: 'sort field' }), + $select: t('console.developer.apiConsole.hintSelect', { defaultValue: 'fields' }), + $count: t('console.developer.apiConsole.hintCount', { defaultValue: 'total count' }), + $filter: t('console.developer.apiConsole.hintFilter', { defaultValue: 'OData filter' }), + }; + return (
-

API Endpoints

+

+ {t('console.developer.apiConsole.endpointsTitle', { defaultValue: 'API Endpoints' })} +

@@ -340,7 +355,7 @@ export function ApiConsolePage() { type="text" value={searchQuery} onChange={e => setSearchQuery(e.target.value)} - placeholder="Search endpoints..." + placeholder={t('console.developer.apiConsole.searchPlaceholder', { defaultValue: 'Search endpoints…' })} 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" />
@@ -350,12 +365,16 @@ export function ApiConsolePage() { {discovering ? (
- Discovering APIs... + {t('console.developer.apiConsole.discovering', { defaultValue: 'Discovering APIs…' })}
) : filteredGroups.length === 0 ? (
- {searchQuery ? 'No matching endpoints' : 'No endpoints found'} + + {searchQuery + ? t('console.developer.apiConsole.noMatching', { defaultValue: 'No matching endpoints' }) + : t('console.developer.apiConsole.noEndpoints', { defaultValue: 'No endpoints found' })} +
) : ( filteredGroups.map(group => { @@ -400,25 +419,27 @@ export function ApiConsolePage() {
-

Query Parameters

+

+ {t('console.developer.apiConsole.queryParameters', { defaultValue: 'Query Parameters' })} +

{QUERY_PARAM_PRESETS.map(preset => ( ))}
@@ -437,14 +458,14 @@ export function ApiConsolePage() { type="text" value={param.key} onChange={e => updateQueryParam(param.id, 'key', e.target.value)} - placeholder="key" + placeholder={t('console.developer.apiConsole.parameterKey', { defaultValue: 'key' })} 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" /> updateQueryParam(param.id, 'value', e.target.value)} - placeholder="value" + placeholder={t('console.developer.apiConsole.parameterValue', { defaultValue: 'value' })} 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" />
{queryParams.some(p => p.enabled && p.key.trim()) && ( @@ -491,7 +512,9 @@ export function ApiConsolePage() {
{['POST', 'PATCH', 'PUT'].includes(effectiveMethod) && (
- +