-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Background
CRM internationalization is partially broken: navigation/dashboard/app labels work (already using I18nLabel objects), but object names, field names, select options, view labels, action labels, report labels, and page titles all display in English regardless of locale setting.
Root cause: These metadata files use plain string labels, and the Console renders them directly without i18n lookup — even though complete translations exist in examples/crm/src/i18n/zh.ts (and 9 other locales).
Screenshot (Order Item page in zh locale — all labels still English):
Problem with the Naive Fix
The naive fix (converting every label to { key: 'crm.xxx.yyy', defaultValue: 'English' }) would require ~200+ manual I18nLabel conversions across 10 objects, 10 view files, 10 action files, 2 reports, and 3 pages. This is unmaintainable and error-prone.
Proposed Solution: Convention-based Auto-resolution (Salesforce-style)
Implement a single useMetadataI18n() hook that automatically constructs translation keys from metadata identifiers at render time. Zero changes to metadata files. Zero changes to translation files.
Convention Table (16 categories)
| # | Category | Key Pattern | Example | Priority |
|---|---|---|---|---|
| 1 | Object label | {app}.objects.{objectName}.label |
crm.objects.order_item.label → 订单明细 |
P0 |
| 2 | Object description | {app}.objects.{objectName}.description |
crm.objects.order_item.description |
P1 |
| 3 | Field label | {app}.fields.{objectName}.{fieldName} |
crm.fields.order_item.unit_price → 单价 |
P0 |
| 4 | Select option label | {app}.fieldOptions.{objectName}.{fieldName}.{value} |
crm.fieldOptions.order_item.item_type.product → 产品 |
P0 |
| 5 | View label | {app}.views.{objectName}.{viewId}.label |
crm.views.opportunity.pipeline.label → 管道 |
P1 |
| 6 | Form section title | {app}.views.{objectName}.{sectionId} |
crm.views.account.basicInfo → 基本信息 |
P1 |
| 7 | Action label | {app}.actions.{actionName}.label |
crm.actions.account_send_email.label → 发送邮件 |
P1 |
| 8 | Action successMessage | {app}.actions.{actionName}.successMessage |
P2 | |
| 9 | Action confirmText | {app}.actions.{actionName}.confirmText |
P2 | |
| 10 | Report label | {app}.reports.{reportName}.label |
crm.reports.salesReport.label → 销售报表 |
P1 |
| 11 | Report description | {app}.reports.{reportName}.description |
P2 | |
| 12 | Report column label | {app}.reports.columns.{columnKey} |
crm.reports.columns.orderNumber → 订单编号 |
P2 |
| 13 | Dashboard title | {app}.dashboard.title |
✅ Already working via I18nLabel | — |
| 14 | Dashboard description | {app}.dashboard.description |
P2 | |
| 15 | Widget title | {app}.dashboard.widgets.{widgetId} |
✅ Already working via I18nLabel | — |
| 16 | Page title/content | {app}.pages.{pageName}.* |
crm.pages.gettingStarted.title |
P2 |
Resolution Priority
- Explicit I18nLabel — if metadata already uses
{ key, defaultValue }, resolve it directly (backward compatible) - Convention key — auto-construct key from metadata identifiers, look up in translation bundle
- Plain string fallback — if no translation found, render the original string label
Implementation Plan
Step 1: New useMetadataI18n hook (packages/i18n)
// packages/i18n/src/useMetadataI18n.ts
export function useMetadataI18n(appName: string = 'crm') {
const { t } = useObjectTranslation();
const resolve = (key: string, fallback: string): string => {
const translated = t(key, { defaultValue: '' });
return (translated && translated !== key && translated !== '') ? translated : fallback;
};
return {
// P0: Object & Fields
objectLabel: (obj) => resolve(`${appName}.objects.${obj.name}.label`, obj.label),
objectDescription: (obj) => obj.description ? resolve(`${appName}.objects.${obj.name}.description`, obj.description) : undefined,
fieldLabel: (objectName, fieldName, fallback) => resolve(`${appName}.fields.${objectName}.${fieldName}`, fallback),
selectOptionLabel: (objectName, fieldName, value, fallback) => resolve(`${appName}.fieldOptions.${objectName}.${fieldName}.${value}`, fallback),
// P1: Views & Actions
viewLabel: (objectName, viewId, fallback) => resolve(`${appName}.views.${objectName}.${viewId}.label`, fallback),
formSectionTitle: (objectName, sectionId, fallback) => resolve(`${appName}.views.${objectName}.${sectionId}`, fallback),
actionLabel: (actionName, fallback) => resolve(`${appName}.actions.${actionName}.label`, fallback),
actionConfirmText: (actionName, fallback) => resolve(`${appName}.actions.${actionName}.confirmText`, fallback),
actionSuccessMsg: (actionName, fallback) => resolve(`${appName}.actions.${actionName}.successMessage`, fallback),
// P2: Reports & Pages
reportLabel: (reportName, fallback) => resolve(`${appName}.reports.${reportName}.label`, fallback),
reportDescription: (reportName, fallback) => resolve(`${appName}.reports.${reportName}.description`, fallback),
reportColumnLabel: (columnKey, fallback) => resolve(`${appName}.reports.columns.${columnKey}`, fallback),
dashboardDesc: (fallback) => resolve(`${appName}.dashboard.description`, fallback),
pageTitle: (pageName, field, fallback) => resolve(`${appName}.pages.${pageName}.${field}`, fallback),
};
}Step 2: Wire into Console components
| File | What to change | Lines |
|---|---|---|
apps/console/src/components/ObjectView.tsx L701, L705, L707 |
Breadcrumb + title + description → objectLabel() / objectDescription() |
~3 |
packages/plugin-list/src/ListView.tsx (column builder) |
Field column headers → fieldLabel() |
~10 |
packages/plugin-grid/src (column inference) |
Grid column headers → fieldLabel() |
~5 |
packages/fields/src/index.tsx (SelectCellRenderer) |
Select display values → selectOptionLabel() |
~5 |
apps/console/src/components/ObjectView.tsx (view tabs) |
View tab labels → viewLabel() |
~3 |
packages/plugin-detail/src (form sections) |
Section titles → formSectionTitle() |
~3 |
| Action bar / SchemaRenderer action components | Action button labels → actionLabel() |
~5 |
packages/plugin-report/src |
Report title + column headers | ~5 |
packages/layout/src/NavigationRenderer.tsx |
Object name in nav items (non-navigation type) | ~3 |
Step 3: Spec documentation (cross-repo → objectstack-ai/spec)
Add i18n-convention.md to packages/spec/src/ui/ documenting the key convention table. No type changes needed — FieldSchema.label stays z.string() in Data Protocol, I18nLabelSchema stays string | I18nObject in UI Protocol.
Spec Protocol Evaluation
| Spec Layer | Current label type |
Change needed? | Rationale |
|---|---|---|---|
Data Protocol (field.zod.ts) |
z.string() |
❌ No change | Data layer must remain UI-agnostic; translation is a UI concern |
UI Protocol (view.zod.ts, action.zod.ts) |
I18nLabelSchema (string | I18nLabel) |
❌ No change | Already supports both modes; convention resolution allows plain strings |
UI Protocol (i18n.zod.ts) |
I18nLabelSchema union |
❌ No change | Union design already accommodates plain string backward compatibility |
| Spec Docs | — | ✅ Add convention doc | Standardize key patterns for all app implementors |
Files Changed Summary
| Location | Change | Effort |
|---|---|---|
packages/i18n/src/useMetadataI18n.ts |
New — convention-based resolver hook | 1 file |
packages/i18n/src/index.ts |
Export new hook | 1 line |
apps/console/src/components/ObjectView.tsx |
Use hook for title/breadcrumb/description | ~6 lines |
packages/plugin-list/src/ |
Use hook for column headers | ~10 lines |
packages/plugin-grid/src/ |
Use hook for grid columns | ~5 lines |
packages/fields/src/index.tsx |
Use hook for select options | ~5 lines |
packages/plugin-detail/src/ |
Use hook for form section titles | ~3 lines |
| Action bar components | Use hook for action labels | ~5 lines |
packages/plugin-report/src/ |
Use hook for report labels | ~5 lines |
packages/layout/src/NavigationRenderer.tsx |
Use hook for object nav items | ~3 lines |
examples/crm/src/objects/*.object.ts |
No change ✅ | 0 |
examples/crm/src/i18n/*.ts |
No change ✅ | 0 |
| Total | ~10 files, ~45 lines | vs ~200 manual I18nLabel conversions |
Cross-repo: objectstack-ai/spec
- Add
i18n-convention.mddocumenting the key convention table - No type definition changes needed
Acceptance Criteria
- P0 — Object/Field/Select option labels:
- Switching to
zhshows Chinese object names in ObjectView breadcrumb, title, description - Switching to
zhshows Chinese field names in data table column headers - Switching to
zhshows Chinese select option labels in table cells and form dropdowns
- Switching to
- P1 — Views/Actions/Reports:
- View tab labels translated
- Form section titles translated
- Action button labels translated
- Report titles translated
- P2 — Remaining:
- Action confirmText/successMessage translated
- Report column headers and descriptions translated
- Page titles translated
- Dashboard description translated
- Compatibility:
- Explicit
I18nLabelobjects (dashboard/navigation) still work (no regression) - English locale continues to display English labels
- All existing tests pass
- New unit tests for
useMetadataI18nhook
- Explicit
- Spec:
- Convention documentation added to
objectstack-ai/spec
- Convention documentation added to
- Metadata files:
- Zero changes to
examples/crm/src/objects/*.object.ts - Zero changes to
examples/crm/src/i18n/*.ts
- Zero changes to