diff --git a/.changeset/zh-branch-migration-part2.md b/.changeset/zh-branch-migration-part2.md new file mode 100644 index 000000000..868781986 --- /dev/null +++ b/.changeset/zh-branch-migration-part2.md @@ -0,0 +1,51 @@ +--- +"@object-ui/i18n": patch +"@object-ui/app-shell": patch +"@object-ui/plugin-chatbot": patch +--- + +fix(i18n): delete the four `pick({en,zh})` clones (objectui#2871, part 2) + +Four files each carried an identical private resolver: + +```ts +function pick(label: I18n): string { + const lang = document.documentElement.getAttribute('lang') || 'en'; + return lang.toLowerCase().startsWith('zh') ? label.zh : label.en; +} +``` + +Only Chinese was ever handled, so ja/ko/de/fr/es/pt/ru/ar silently rendered +English — and because the copy was baked into the components as inline +`{en, zh}` pairs, no translator could reach it. All four copies are deleted +along with their `I18n` type alias. + +Migrated to the locale packs, **all ten languages**: + +- `excelImport.*` (8 keys) — `ExcelImportBar`. The completion toast becomes a + proper `{{count}}` / `{{object}}` interpolation instead of a template literal + baked into both language variants. +- `cloudOnboarding.*` (5 keys) — `CloudOnboardingNext`, the Cloud welcome page. +- `aiModelStatus.*` (11 keys) — `CloudAiModelStatus`, including the + `sourceLabel()` enum→prose helper (now `t`-driven with a `{{source}}` + placeholder) and the three `ModelRow` labels. The conditional + `(HTTP nnn)` fragment becomes two whole sentences rather than a string + spliced mid-clause, which is not translatable into every word order. +- `chatbotQuota.*` (4 keys) — the AI quota banner in `ChatbotEnhanced`. + +The chatbot banner keeps choosing between the server's `quota.message` (zh) and +`quota.messageEn` — that pair is server-owned — but now decides using the +console's active language instead of `navigator.language`, which had ignored +the in-app locale switcher entirely. + +`CloudOnboardingNext`'s tests now render inside a real `I18nProvider`; without +one `t()` returns the raw key, so the previous assertions on literal English +were asserting nothing. + +This completes the `pick()` cluster from #2871. The remaining +`startsWith('zh')` sites are the ones that classification marked KEEP — +`LoadingScreen` (bootstrap, selects real locale packs before i18next is up), +`conversationLanguage` (detects the chat's language for the agent, not UI +copy), `containers.tsx` (normalises author-supplied schema data; its `'与'` +separator is a CJK typography rule), and the Studio catalog / `field-types.ts` +data catalog. diff --git a/packages/app-shell/src/console/ai/ExcelImportBar.tsx b/packages/app-shell/src/console/ai/ExcelImportBar.tsx index 79a2578ab..f6944c0c3 100644 --- a/packages/app-shell/src/console/ai/ExcelImportBar.tsx +++ b/packages/app-shell/src/console/ai/ExcelImportBar.tsx @@ -18,10 +18,9 @@ import { useEffect, useMemo, useState } from 'react'; import { Button, Badge } from '@object-ui/components'; import { createAuthenticatedFetch } from '@object-ui/auth'; import { toast } from 'sonner'; +import { useObjectTranslation } from '@object-ui/i18n'; import { ImportWizard } from '@object-ui/plugin-grid'; -type I18n = { en: string; zh: string }; - interface WizardField { name: string; label: string; @@ -41,12 +40,6 @@ interface ExcelImportBarProps { onDone: () => void; } -function pick(label: I18n): string { - const lang = - (typeof document !== 'undefined' && document.documentElement.getAttribute('lang')) || 'en'; - return lang.toLowerCase().startsWith('zh') ? label.zh : label.en; -} - const SKIP_OBJECT_PREFIXES = ['sys_', 'ai_', 'cloud_']; const NON_WRITABLE_TYPES = ['formula', 'summary', 'autonumber']; @@ -68,6 +61,7 @@ function normalizeFields(schema: any): WizardField[] { } export function ExcelImportBar({ file, dataSource, defaultObjectName, onDone }: ExcelImportBarProps) { + const { t } = useObjectTranslation(); const authFetch = useMemo(() => createAuthenticatedFetch(), []); const [objects, setObjects] = useState | null>(null); const [selected, setSelected] = useState(defaultObjectName ?? ''); @@ -109,13 +103,13 @@ export function ExcelImportBar({ file, dataSource, defaultObjectName, onDone }: const schema = await dataSource.getObjectSchema(selected); const f = normalizeFields(schema); if (!f.length) { - toast.error(pick({ en: 'That object has no importable fields.', zh: '该对象没有可导入的字段。' })); + toast.error(t('excelImport.noImportableFields')); return; } setFields(f); setOpen(true); } catch { - toast.error(pick({ en: 'Could not read the object schema.', zh: '无法读取对象结构。' })); + toast.error(t('excelImport.schemaReadFailed')); } finally { setLoadingFields(false); } @@ -135,10 +129,7 @@ export function ExcelImportBar({ file, dataSource, defaultObjectName, onDone }: initialFile={file} onComplete={(result) => { toast.success( - pick({ - en: `Imported ${result.importedRows} row(s) into ${selectedLabel}.`, - zh: `已把 ${result.importedRows} 行真实数据导入「${selectedLabel}」。`, - }), + t('excelImport.imported', { count: result.importedRows, object: selectedLabel }), ); }} /> @@ -149,10 +140,10 @@ export function ExcelImportBar({ file, dataSource, defaultObjectName, onDone }:
CSV / Excel - {pick({ en: 'Import the real rows from', zh: '把真实数据导入自' })} + {t('excelImport.importFrom')} {file.name} - {pick({ en: 'into', zh: '到' })} + {t('excelImport.into')}