Skip to content

Convention-based Metadata i18n Auto-resolution #942

@hotlong

Description

@hotlong

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):

image1

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

  1. Explicit I18nLabel — if metadata already uses { key, defaultValue }, resolve it directly (backward compatible)
  2. Convention key — auto-construct key from metadata identifiers, look up in translation bundle
  3. 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 neededFieldSchema.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.md documenting the key convention table
  • No type definition changes needed

Acceptance Criteria

  • P0 — Object/Field/Select option labels:
    • Switching to zh shows Chinese object names in ObjectView breadcrumb, title, description
    • Switching to zh shows Chinese field names in data table column headers
    • Switching to zh shows Chinese select option labels in table cells and form dropdowns
  • 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 I18nLabel objects (dashboard/navigation) still work (no regression)
    • English locale continues to display English labels
    • All existing tests pass
    • New unit tests for useMetadataI18n hook
  • Spec:
    • Convention documentation added to objectstack-ai/spec
  • Metadata files:
    • Zero changes to examples/crm/src/objects/*.object.ts
    • Zero changes to examples/crm/src/i18n/*.ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions