From ffd0eb4549ed044e565705083edcc8af79aab4ff Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 04:00:52 +0000 Subject: [PATCH] fix(i18n): close the last three zh-branch gaps (#2871, part 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three items the classification marked as real but not a migrate-the-copy fix. Each needed a different remedy. LoadingScreen collapsed ten languages into two. It already selected real locale packs rather than inline copy, but via `startsWith('zh') ? zh : en`, so a ja/ko/de user watched the entire boot in English. Now indexes builtInLocales by two-letter prefix, with a PER-FIELD fallback to `en` — `console.*` is one of the namespaces trailing in the non-zh packs (#2872 part a), so a whole-object swap would have rendered `undefined` on the splash instead of English. `console.loadingHint` was in fact missing from all eight; added, because a blank line under the progress list is worse than an English one. containers.tsx had two language sources that could disagree: the call sites resolved `language` from useObjectTranslation, then translateLabel called detectLocale() and read document.documentElement.lang itself. Those update independently, so an in-app language switch could leave a tab label and its chrome in different languages until reload. `language` is threaded in and detectLocale is deleted so nothing reaches for the DOM again. field-types.ts carried a `labelZh` column beside `label`, capping the field-type picker at two languages by construction. The 46 type names and 9 category names move to the Studio catalog as `engine.fieldType.` / `engine.fieldCategory.`, generated from the existing values so no wording changes. This removes `isZh` from BOTH ObjectFieldInspector and ObjectFormCanvas — the two files classified as "keep the component, fix the catalog". Also caught while there: the picker's search filter matched id, the English label, and `labelZh`, so searching in Japanese or German matched nothing. It now matches the label as the user actually sees it. Full suite 7730 passed. ESLint errors identical to baseline (25, all pre-existing). turbo type-check 31/31 for the changed packages. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC --- .changeset/zh-branch-migration-part3.md | 41 ++++++ .../app-shell/src/chrome/LoadingScreen.tsx | 31 +++-- .../src/views/metadata-admin/i18n.ts | 110 +++++++++++++++ .../inspectors/ObjectFieldInspector.tsx | 18 ++- .../previews/ObjectFormCanvas.tsx | 17 ++- .../metadata-admin/previews/field-types.ts | 126 +++++++++--------- .../src/renderers/layout/containers.tsx | 27 ++-- packages/i18n/src/locales/ar.ts | 1 + packages/i18n/src/locales/de.ts | 1 + packages/i18n/src/locales/es.ts | 1 + packages/i18n/src/locales/fr.ts | 1 + packages/i18n/src/locales/ja.ts | 1 + packages/i18n/src/locales/ko.ts | 1 + packages/i18n/src/locales/pt.ts | 1 + packages/i18n/src/locales/ru.ts | 1 + 15 files changed, 273 insertions(+), 105 deletions(-) create mode 100644 .changeset/zh-branch-migration-part3.md diff --git a/.changeset/zh-branch-migration-part3.md b/.changeset/zh-branch-migration-part3.md new file mode 100644 index 000000000..29a9884d6 --- /dev/null +++ b/.changeset/zh-branch-migration-part3.md @@ -0,0 +1,41 @@ +--- +"@object-ui/i18n": patch +"@object-ui/components": patch +"@object-ui/app-shell": patch +--- + +fix(i18n): close the last three zh-branch gaps (objectui#2871, part 3) + +The three items the #2871 classification marked as real but *not* a +migrate-the-copy fix. Each needed a different remedy. + +**`LoadingScreen` — ten languages collapsed to two.** The boot splash already +selected real locale packs (not inline copy), but through +`lang.startsWith('zh') ? zh : en`, so a ja/ko/de user watched the whole startup +in English. It now indexes `builtInLocales` by the two-letter prefix. + +Each field falls back to `en` **individually**, which matters: `console.*` is +one of the namespaces that trails in the non-`zh` packs (objectui#2872 part a), +so a whole-object swap would have rendered `undefined` on the splash rather +than English. `console.loadingHint` was in fact missing from all eight — added +here, since a blank line under the progress list is worse than an English one. + +**`containers.tsx` — two language sources that could disagree.** The tab-label +call sites resolved `language` from `useObjectTranslation()`, then handed the +string to `translateLabel`, which called `detectLocale()` and read +`document.documentElement.lang` on its own. Those update independently, so an +in-app language switch could leave a tab label and its surrounding chrome in +different languages until the next reload. `language` is now threaded in, and +`detectLocale` is deleted so nothing reaches for the DOM again. + +**`field-types.ts` — a two-language data catalog.** `FieldTypeMeta` carried a +`labelZh` column beside `label`, which capped the field-type picker at English +or Chinese by construction. The 46 type names and 9 category names move into +the Studio catalog as `engine.fieldType.` / `engine.fieldCategory.`, +generated from the existing values so no wording changes. This removes the +`isZh` helper from **both** `ObjectFieldInspector` and `ObjectFormCanvas` — the +two files the classification listed as "keep the component, fix the catalog". + +The picker's search filter previously matched `id`, the English label, and +`labelZh` — so searching in Japanese or German matched nothing. It now matches +the label as the user actually sees it. diff --git a/packages/app-shell/src/chrome/LoadingScreen.tsx b/packages/app-shell/src/chrome/LoadingScreen.tsx index cf072a89d..4deb9512b 100644 --- a/packages/app-shell/src/chrome/LoadingScreen.tsx +++ b/packages/app-shell/src/chrome/LoadingScreen.tsx @@ -3,7 +3,7 @@ import { Spinner, Button } from '@object-ui/components'; import { Database, CheckCircle2, Loader2, AlertCircle, RefreshCw } from 'lucide-react'; import { useState, useEffect, useMemo } from 'react'; import { getProductName, getLogoUrl } from '../runtime-config'; -import { en as enLocale, zh as zhLocale } from '@object-ui/i18n'; +import { en as enLocale, builtInLocales } from '@object-ui/i18n'; interface LoadingScreenProps { /** Optional message override */ @@ -23,14 +23,29 @@ interface LoadingScreenProps { // synchronous dictionary for the startup shell instead. // The product name is read from the runtime-config singleton (sync) so it // reflects server-pushed branding when available, falling back to 'ObjectOS'. +// +// Indexed by the two-letter prefix across ALL built-in packs rather than a +// `zh ? … : en` check: the previous form collapsed ten shipped languages into +// two, so a ja/ko/de user saw English for the whole boot (objectui#2871). +// +// Each field falls back to `en` individually. A pack that is behind on some +// `console.*` keys — several are, see objectui#2872 part (a) — must degrade to +// English, not to `undefined`, which would render blank on the splash. function getStartupStrings() { - if (typeof document !== 'undefined' && document.documentElement.lang?.startsWith('zh')) { - return zhLocale.console; - } - if (typeof navigator !== 'undefined' && navigator.language?.startsWith('zh')) { - return zhLocale.console; - } - return enLocale.console; + const tag = + (typeof document !== 'undefined' ? document.documentElement.lang : '') || + (typeof navigator !== 'undefined' ? navigator.language : '') || + 'en'; + const base = tag.toLowerCase().split('-')[0] as keyof typeof builtInLocales; + const pack = builtInLocales[base]?.console; + if (!pack || pack === enLocale.console) return enLocale.console; + return { + ...enLocale.console, + ...pack, + loadingSteps: { ...enLocale.console.loadingSteps, ...pack.loadingSteps }, + error: { ...enLocale.console.error, ...pack.error }, + actions: { ...enLocale.console.actions, ...pack.actions }, + }; } export function LoadingScreen({ message, error, onRetry, retrying }: LoadingScreenProps) { diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index 7eaebefc6..775bf1300 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -1207,6 +1207,61 @@ const ENGINE_STRINGS_EN: Record = { 'engine.studio.toggleRail': 'Toggle sidebar', 'engine.studio.home': 'Back to home', // Pillar tab labels + 'engine.fieldType.text': 'Text', + 'engine.fieldType.textarea': 'Text Area', + 'engine.fieldType.email': 'Email', + 'engine.fieldType.url': 'URL', + 'engine.fieldType.phone': 'Phone', + 'engine.fieldType.password': 'Password', + 'engine.fieldType.markdown': 'Markdown', + 'engine.fieldType.html': 'HTML', + 'engine.fieldType.richtext': 'Rich Text', + 'engine.fieldType.number': 'Number', + 'engine.fieldType.currency': 'Currency', + 'engine.fieldType.percent': 'Percent', + 'engine.fieldType.date': 'Date', + 'engine.fieldType.datetime': 'Date/Time', + 'engine.fieldType.time': 'Time', + 'engine.fieldType.boolean': 'Checkbox', + 'engine.fieldType.toggle': 'Toggle', + 'engine.fieldType.select': 'Picklist', + 'engine.fieldType.multiselect': 'Multi-Select', + 'engine.fieldType.radio': 'Radio', + 'engine.fieldType.checkboxes': 'Checkboxes', + 'engine.fieldType.lookup': 'Lookup', + 'engine.fieldType.master_detail': 'Master-Detail', + 'engine.fieldType.tree': 'Tree', + 'engine.fieldType.image': 'Image', + 'engine.fieldType.file': 'File', + 'engine.fieldType.avatar': 'Avatar', + 'engine.fieldType.video': 'Video', + 'engine.fieldType.audio': 'Audio', + 'engine.fieldType.formula': 'Formula', + 'engine.fieldType.summary': 'Rollup', + 'engine.fieldType.autonumber': 'Auto Number', + 'engine.fieldType.composite': 'Composite', + 'engine.fieldType.repeater': 'Repeater', + 'engine.fieldType.location': 'Location', + 'engine.fieldType.address': 'Address', + 'engine.fieldType.code': 'Code', + 'engine.fieldType.json': 'JSON', + 'engine.fieldType.color': 'Color', + 'engine.fieldType.rating': 'Rating', + 'engine.fieldType.slider': 'Slider', + 'engine.fieldType.signature': 'Signature', + 'engine.fieldType.qrcode': 'QR Code', + 'engine.fieldType.progress': 'Progress', + 'engine.fieldType.tags': 'Tags', + 'engine.fieldType.vector': 'Vector', + 'engine.fieldCategory.text': 'Text', + 'engine.fieldCategory.number': 'Number', + 'engine.fieldCategory.date': 'Date / time', + 'engine.fieldCategory.logic': 'Logic', + 'engine.fieldCategory.selection': 'Selection', + 'engine.fieldCategory.relation': 'Relation', + 'engine.fieldCategory.media': 'Media', + 'engine.fieldCategory.calculated': 'Calculated', + 'engine.fieldCategory.advanced': 'Advanced', 'engine.studio.aiCopilot': 'AI copilot', 'engine.studio.pillar.data': 'Data', 'engine.studio.pillar.automations': 'Automations', @@ -2742,6 +2797,61 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.studio.toggleRail': '切换侧栏', 'engine.studio.home': '返回主页', // Pillar tab labels + 'engine.fieldType.text': '单行文本', + 'engine.fieldType.textarea': '多行文本', + 'engine.fieldType.email': '邮箱', + 'engine.fieldType.url': '网址', + 'engine.fieldType.phone': '电话', + 'engine.fieldType.password': '密码', + 'engine.fieldType.markdown': 'Markdown', + 'engine.fieldType.html': 'HTML', + 'engine.fieldType.richtext': '富文本', + 'engine.fieldType.number': '数字', + 'engine.fieldType.currency': '货币', + 'engine.fieldType.percent': '百分比', + 'engine.fieldType.date': '日期', + 'engine.fieldType.datetime': '日期时间', + 'engine.fieldType.time': '时间', + 'engine.fieldType.boolean': '复选框', + 'engine.fieldType.toggle': '开关', + 'engine.fieldType.select': '下拉选择', + 'engine.fieldType.multiselect': '多选', + 'engine.fieldType.radio': '单选', + 'engine.fieldType.checkboxes': '复选组', + 'engine.fieldType.lookup': '查找关系', + 'engine.fieldType.master_detail': '主从关系', + 'engine.fieldType.tree': '树形关系', + 'engine.fieldType.image': '图片', + 'engine.fieldType.file': '文件', + 'engine.fieldType.avatar': '头像', + 'engine.fieldType.video': '视频', + 'engine.fieldType.audio': '音频', + 'engine.fieldType.formula': '公式', + 'engine.fieldType.summary': '汇总', + 'engine.fieldType.autonumber': '自动编号', + 'engine.fieldType.composite': '复合字段', + 'engine.fieldType.repeater': '重复字段', + 'engine.fieldType.location': '地理坐标', + 'engine.fieldType.address': '地址', + 'engine.fieldType.code': '代码', + 'engine.fieldType.json': 'JSON', + 'engine.fieldType.color': '颜色', + 'engine.fieldType.rating': '评分', + 'engine.fieldType.slider': '滑块', + 'engine.fieldType.signature': '签名', + 'engine.fieldType.qrcode': '二维码', + 'engine.fieldType.progress': '进度条', + 'engine.fieldType.tags': '标签', + 'engine.fieldType.vector': '向量', + 'engine.fieldCategory.text': '文本', + 'engine.fieldCategory.number': '数值', + 'engine.fieldCategory.date': '日期/时间', + 'engine.fieldCategory.logic': '逻辑', + 'engine.fieldCategory.selection': '选择', + 'engine.fieldCategory.relation': '关系', + 'engine.fieldCategory.media': '媒体', + 'engine.fieldCategory.calculated': '计算', + 'engine.fieldCategory.advanced': '高级', 'engine.studio.aiCopilot': 'AI 副驾', 'engine.studio.pillar.data': '数据', 'engine.studio.pillar.automations': '自动化', diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx index 9ad21c6af..75d620099 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx @@ -51,14 +51,11 @@ import { import { FIELD_TYPE_META, TYPES_BY_CATEGORY, - CATEGORY_LABEL_EN, - CATEGORY_LABEL_ZH, type FieldTypeId, } from '../previews/field-types'; import { CelPredicateField } from '../CelPredicateField'; import { t, tFormat } from '../i18n'; -const isZh = (locale?: string) => (locale ?? '').toLowerCase().startsWith('zh'); interface Option { value: string; @@ -162,13 +159,14 @@ function writePredicate(orig: unknown, next: string): unknown { } function buildTypeOptions(locale?: string): Array<{ value: string; label: string }> { - const zh = (locale ?? '').toLowerCase().startsWith('zh'); - const cats = zh ? CATEGORY_LABEL_ZH : CATEGORY_LABEL_EN; + // Type and category names come from the Studio catalog rather than the + // `labelZh` column that used to sit on FIELD_TYPE_META, so this no longer + // needs a zh/en branch (objectui#2871). return TYPES_BY_CATEGORY.flatMap((g) => - g.types.map((id) => { - const m = FIELD_TYPE_META[id]; - return { value: id, label: `${cats[g.category]} · ${zh ? m.labelZh : m.label}` }; - }), + g.types.map((id) => ({ + value: id, + label: `${t(`engine.fieldCategory.${g.category}`, locale)} · ${t(`engine.fieldType.${id}`, locale)}`, + })), ); } @@ -350,7 +348,7 @@ export function ObjectFieldInspector({ /> ); - const typeMetaLabel = isZh(locale) ? typeMeta?.labelZh : typeMeta?.label; + const typeMetaLabel = typeMeta ? t(`engine.fieldType.${typeMeta.id}`, locale) : undefined; return ( (locale ?? '').toLowerCase().startsWith('zh'); -/** Field-type display label in the active locale (data carries both). */ +// Both resolve through the Studio catalog now. They used to read a `labelZh` +// column on FIELD_TYPE_META behind an `isZh` check, which meant the picker +// only ever spoke English or Chinese (objectui#2871). const typeLabel = (meta: FieldTypeMeta | undefined, locale?: string): string | undefined => - meta ? (isZh(locale) ? meta.labelZh : meta.label) : undefined; + meta ? t(`engine.fieldType.${meta.id}`, locale) : undefined; const categoryLabel = (cat: FieldTypeCategory, locale?: string): string => - (isZh(locale) ? CATEGORY_LABEL_ZH : CATEGORY_LABEL_EN)[cat]; + t(`engine.fieldCategory.${cat}`, locale); export interface ObjectFormCanvasProps { objectName: string; @@ -1274,7 +1273,11 @@ function AddFieldButton({ onPick, compact, locale }: { onPick: (type: FieldTypeI category: g.category, types: g.types.filter((id) => { const m = FIELD_TYPE_META[id]; - return id.includes(q) || m.label.toLowerCase().includes(q) || m.labelZh.includes(filter.trim()); + // Match the id, the English label, and the label as the user + // actually sees it — previously the third arm was hard-wired to + // Chinese, so searching in ja/de matched nothing (objectui#2871). + const shown = typeLabel(m, locale) ?? ''; + return id.includes(q) || m.label.toLowerCase().includes(q) || shown.toLowerCase().includes(q); }), })) .filter((g) => g.types.length > 0); diff --git a/packages/app-shell/src/views/metadata-admin/previews/field-types.ts b/packages/app-shell/src/views/metadata-admin/previews/field-types.ts index d8c57fd03..867a2f996 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/field-types.ts +++ b/packages/app-shell/src/views/metadata-admin/previews/field-types.ts @@ -46,71 +46,74 @@ export type FieldTypeCategory = export interface FieldTypeMeta { id: FieldTypeId; + /** + * English display label. The localized name lives in the Studio catalog as + * `engine.fieldType.` — this column used to carry a `labelZh` sibling, + * which capped the picker at two languages (objectui#2871). + */ label: string; - /** Brief Chinese label for the picker / type badge. */ - labelZh: string; category: FieldTypeCategory; Icon: LucideIcon; } -const M = (id: FieldTypeId, label: string, labelZh: string, category: FieldTypeCategory, Icon: LucideIcon): FieldTypeMeta => - ({ id, label, labelZh, category, Icon }); +const M = (id: FieldTypeId, label: string, category: FieldTypeCategory, Icon: LucideIcon): FieldTypeMeta => + ({ id, label, category, Icon }); export const FIELD_TYPE_META: Record = { - text: M('text', 'Text', '单行文本', 'text', Type), - textarea: M('textarea', 'Text Area', '多行文本', 'text', AlignLeft), - email: M('email', 'Email', '邮箱', 'text', AtSign), - url: M('url', 'URL', '网址', 'text', Globe), - phone: M('phone', 'Phone', '电话', 'text', Phone), - password: M('password', 'Password', '密码', 'text', Lock), - markdown: M('markdown', 'Markdown', 'Markdown', 'text', FileText), - html: M('html', 'HTML', 'HTML', 'text', Code), - richtext: M('richtext', 'Rich Text', '富文本', 'text', Sparkles), - - number: M('number', 'Number', '数字', 'number', Hash), - currency: M('currency', 'Currency', '货币', 'number', DollarSign), - percent: M('percent', 'Percent', '百分比', 'number', Percent), - - date: M('date', 'Date', '日期', 'date', Calendar), - datetime: M('datetime', 'Date/Time', '日期时间', 'date', Calendar), - time: M('time', 'Time', '时间', 'date', Clock), - - boolean: M('boolean', 'Checkbox', '复选框', 'logic', CheckSquare), - toggle: M('toggle', 'Toggle', '开关', 'logic', ToggleLeft), - - select: M('select', 'Picklist', '下拉选择', 'selection', ListOrdered), - multiselect: M('multiselect', 'Multi-Select', '多选', 'selection', ListChecks), - radio: M('radio', 'Radio', '单选', 'selection', CircleDot), - checkboxes: M('checkboxes', 'Checkboxes', '复选组', 'selection', ListChecks), - - lookup: M('lookup', 'Lookup', '查找关系', 'relation', Link2), - master_detail: M('master_detail', 'Master-Detail', '主从关系', 'relation', Workflow), - tree: M('tree', 'Tree', '树形关系', 'relation', Network), - - image: M('image', 'Image', '图片', 'media', Image), - file: M('file', 'File', '文件', 'media', Paperclip), - avatar: M('avatar', 'Avatar', '头像', 'media', UserCircle), - video: M('video', 'Video', '视频', 'media', Video), - audio: M('audio', 'Audio', '音频', 'media', Music), - - formula: M('formula', 'Formula', '公式', 'calculated', Calculator), - summary: M('summary', 'Rollup', '汇总', 'calculated', Sigma), - autonumber: M('autonumber', 'Auto Number', '自动编号', 'calculated', Hash), - - composite: M('composite', 'Composite', '复合字段', 'advanced', Boxes), - repeater: M('repeater', 'Repeater', '重复字段', 'advanced', Repeat2), - location: M('location', 'Location', '地理坐标', 'advanced', MapPin), - address: M('address', 'Address', '地址', 'advanced', Map), - code: M('code', 'Code', '代码', 'advanced', Code), - json: M('json', 'JSON', 'JSON', 'advanced', FileJson), - color: M('color', 'Color', '颜色', 'advanced', Palette), - rating: M('rating', 'Rating', '评分', 'advanced', Star), - slider: M('slider', 'Slider', '滑块', 'advanced', SlidersHorizontal), - signature: M('signature', 'Signature', '签名', 'advanced', PenLine), - qrcode: M('qrcode', 'QR Code', '二维码', 'advanced', QrCode), - progress: M('progress', 'Progress', '进度条', 'advanced', BarChart3), - tags: M('tags', 'Tags', '标签', 'advanced', Tags), - vector: M('vector', 'Vector', '向量', 'advanced', Atom), + text: M('text', 'Text', 'text', Type), + textarea: M('textarea', 'Text Area', 'text', AlignLeft), + email: M('email', 'Email', 'text', AtSign), + url: M('url', 'URL', 'text', Globe), + phone: M('phone', 'Phone', 'text', Phone), + password: M('password', 'Password', 'text', Lock), + markdown: M('markdown', 'Markdown', 'text', FileText), + html: M('html', 'HTML', 'text', Code), + richtext: M('richtext', 'Rich Text', 'text', Sparkles), + + number: M('number', 'Number', 'number', Hash), + currency: M('currency', 'Currency', 'number', DollarSign), + percent: M('percent', 'Percent', 'number', Percent), + + date: M('date', 'Date', 'date', Calendar), + datetime: M('datetime', 'Date/Time', 'date', Calendar), + time: M('time', 'Time', 'date', Clock), + + boolean: M('boolean', 'Checkbox', 'logic', CheckSquare), + toggle: M('toggle', 'Toggle', 'logic', ToggleLeft), + + select: M('select', 'Picklist', 'selection', ListOrdered), + multiselect: M('multiselect', 'Multi-Select', 'selection', ListChecks), + radio: M('radio', 'Radio', 'selection', CircleDot), + checkboxes: M('checkboxes', 'Checkboxes', 'selection', ListChecks), + + lookup: M('lookup', 'Lookup', 'relation', Link2), + master_detail: M('master_detail', 'Master-Detail', 'relation', Workflow), + tree: M('tree', 'Tree', 'relation', Network), + + image: M('image', 'Image', 'media', Image), + file: M('file', 'File', 'media', Paperclip), + avatar: M('avatar', 'Avatar', 'media', UserCircle), + video: M('video', 'Video', 'media', Video), + audio: M('audio', 'Audio', 'media', Music), + + formula: M('formula', 'Formula', 'calculated', Calculator), + summary: M('summary', 'Rollup', 'calculated', Sigma), + autonumber: M('autonumber', 'Auto Number', 'calculated', Hash), + + composite: M('composite', 'Composite', 'advanced', Boxes), + repeater: M('repeater', 'Repeater', 'advanced', Repeat2), + location: M('location', 'Location', 'advanced', MapPin), + address: M('address', 'Address', 'advanced', Map), + code: M('code', 'Code', 'advanced', Code), + json: M('json', 'JSON', 'advanced', FileJson), + color: M('color', 'Color', 'advanced', Palette), + rating: M('rating', 'Rating', 'advanced', Star), + slider: M('slider', 'Slider', 'advanced', SlidersHorizontal), + signature: M('signature', 'Signature', 'advanced', PenLine), + qrcode: M('qrcode', 'QR Code', 'advanced', QrCode), + progress: M('progress', 'Progress', 'advanced', BarChart3), + tags: M('tags', 'Tags', 'advanced', Tags), + vector: M('vector', 'Vector', 'advanced', Atom), }; export const CATEGORY_ORDER: FieldTypeCategory[] = [ @@ -158,12 +161,6 @@ export function resolveCategoryTone(type: unknown): CategoryTone { return CATEGORY_TONE[resolveFieldTypeMeta(type).category]; } -export const CATEGORY_LABEL_ZH: Record = { - text: '文本', number: '数值', date: '日期/时间', logic: '逻辑', - selection: '选择', relation: '关系', media: '媒体', - calculated: '计算', advanced: '高级', -}; - /** All type ids grouped by category, in category order. */ export const TYPES_BY_CATEGORY: Array<{ category: FieldTypeCategory; types: FieldTypeId[] }> = CATEGORY_ORDER.map((category) => ({ @@ -181,7 +178,6 @@ export function resolveFieldTypeMeta(type: unknown): FieldTypeMeta { return { id: 'text', label: typeof type === 'string' ? type : 'unknown', - labelZh: typeof type === 'string' ? type : '未知', category: 'advanced', Icon: Type, }; diff --git a/packages/components/src/renderers/layout/containers.tsx b/packages/components/src/renderers/layout/containers.tsx index bd94161c6..16da8d345 100644 --- a/packages/components/src/renderers/layout/containers.tsx +++ b/packages/components/src/renderers/layout/containers.tsx @@ -152,20 +152,17 @@ const KNOWN_LABEL_DICT: Record> = { }, }; -const detectLocale = (): string => { - if (typeof document !== 'undefined') { - const docLang = document.documentElement?.lang; - if (docLang) return docLang; - } - if (typeof navigator !== 'undefined' && navigator.language) { - return navigator.language; - } - return 'en'; -}; - -const translateLabel = (text: string): string => { +/** + * `locale` is passed in rather than re-detected. Both call sites already + * resolve it from `useObjectTranslation().language`; this function used to + * call `detectLocale()` and read `document.documentElement.lang` on its own, + * so the tab label and the chrome around it could render from two different + * language sources — they desync the moment the user switches language + * in-app, because the DOM attribute and the i18n instance update + * independently (objectui#2871). + */ +const translateLabel = (text: string, locale: string): string => { if (!text) return text; - const locale = detectLocale(); // Match `zh-CN`, `zh-TW`, then base `zh` → `zh-CN`. const exact = KNOWN_LABEL_DICT[locale]; const base = locale.split('-')[0]; @@ -448,7 +445,7 @@ const PageTabsRenderer: React.FC = ({ schema, className, ...props }) => { value: typeof (it as any).value === 'string' && (it as any).value !== '' ? (it as any).value : `tab-${idx}`, // pickLocalized first (honours `{ en, zh }` / `{ default }`); translateLabel // then maps any plain-English well-known token (Details/Related/…) to the locale. - labelStr: translateLabel(pickLocalized(it.label, language)), + labelStr: translateLabel(pickLocalized(it.label, language), language), // Explicit spec count wins; otherwise fall back to the derived probe. count: it.count !== undefined && it.count !== null && it.count !== '' ? it.count @@ -634,7 +631,7 @@ const PageAccordionRenderer: React.FC = ({ schema, className, ...props }) = const itemsWithValue = items.map((it, idx) => ({ ...it, value: `panel-${idx}`, - labelStr: translateLabel(pickLocalized(it.label, language)), + labelStr: translateLabel(pickLocalized(it.label, language), language), })); const defaultOpen = itemsWithValue diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 5d9d32ad3..d574b2c4b 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -850,6 +850,7 @@ const ar = { console: { title: "وحدة تحكم ObjectStack", initializing: "جاري تهيئة التطبيق...", + loadingHint: "قد يستغرق إعداد بيئة جديدة بعض الوقت.", breadcrumb: { dashboards: "لوحات المعلومات", pages: "الصفحات", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index f51d813fc..247ad0e69 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -852,6 +852,7 @@ const de = { console: { title: "ObjectStack Konsole", initializing: "Anwendung wird initialisiert...", + loadingHint: "Die Einrichtung einer neuen Umgebung kann einen Moment dauern.", breadcrumb: { dashboards: "Dashboards", pages: "Seiten", diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 3bbed2109..a2e5e9701 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -852,6 +852,7 @@ const es = { console: { title: "Consola ObjectStack", initializing: "Inicializando aplicación...", + loadingHint: "Configurar un entorno nuevo puede tardar unos momentos.", breadcrumb: { dashboards: "Paneles", pages: "Páginas", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index dac946a88..2e686eaf6 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -850,6 +850,7 @@ const fr = { console: { title: "Console ObjectStack", initializing: "Initialisation de l'application...", + loadingHint: "La configuration d'un nouvel environnement peut prendre quelques instants.", breadcrumb: { dashboards: "Tableaux de bord", pages: "Pages", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 1d9f39e8c..e2aa34123 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -852,6 +852,7 @@ const ja = { console: { title: "ObjectStack コンソール", initializing: "アプリケーションを初期化中...", + loadingHint: "新しい環境のセットアップには少し時間がかかることがあります。", breadcrumb: { dashboards: "ダッシュボード", pages: "ページ", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 636026b39..19c6bc835 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -852,6 +852,7 @@ const ko = { console: { title: "ObjectStack 콘솔", initializing: "애플리케이션 초기화 중...", + loadingHint: "새 환경을 설정하는 데 시간이 조금 걸릴 수 있습니다.", breadcrumb: { dashboards: "대시보드", pages: "페이지", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 92c31ff70..bb9228fdb 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -850,6 +850,7 @@ const pt = { console: { title: "Console ObjectStack", initializing: "Inicializando aplicação...", + loadingHint: "Configurar um novo ambiente pode levar alguns instantes.", breadcrumb: { dashboards: "Painéis", pages: "Páginas", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 6548bfc0d..906060b3e 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -850,6 +850,7 @@ const ru = { console: { title: "Консоль ObjectStack", initializing: "Инициализация приложения...", + loadingHint: "Настройка нового окружения может занять некоторое время.", breadcrumb: { dashboards: "Панели", pages: "Страницы",