Skip to content

i18n: label 应为纯字符串,key 由框架自动生成 #1054

@hotlong

Description

@hotlong

问题背景

当前 I18nLabelSchema 定义为 string | { key, defaultValue, params } 联合类型,允许元数据的 label/description 字段以两种形态存在。这个设计导致了一系列连锁问题。

问题清单

1. React 渲染崩溃 (P0 — 已修复)

{ key, defaultValue } 对象被直接渲染为 React 子节点,触发 React error #31

受影响文件:

  • apps/studio/src/components/MetadataInspector.tsxlabeldescription 渲染
  • apps/studio/src/components/app-sidebar.tsx — 元数据列表项 label 渲染

临时修复: 添加 resolveLabel() helper 将 i18n 对象降级为字符串。但这是 workaround,不是正解。

2. 两套翻译系统并行,职责不清

当前存在两套互不关联的 i18n 机制:

机制 存储位置 用于 示例
TranslationBundle 翻译文件 en.ts, zh-CN.ts 对象 field label、选项值 { objects: { account: { label: 'Account', fields: { ... } } } }
I18nObject 内嵌在元数据定义中 app/view/page 的 label { key: 'setup.app.label', defaultValue: 'Setup' }
  • TranslationBundle 按 objects.<name>.fields.<field>.label 结构化组织,由 I18nServicePlugin 加载并通过 REST API 提供
  • I18nObject 散落在代码中,key 由开发者手写,没有对应的翻译文件,也没有被任何翻译服务管理

3. i18n key 命名无标准化

当前手写的 key 各自为政:

setup.app.label           → SetupPlugin
setup.app.description     → SetupPlugin
setup.nav.users           → AuthPlugin
setup.nav.roles           → SecurityPlugin
setup.nav.audit_logs      → AuditPlugin

area 的 key:

setup.area.administration → setup-areas.ts
setup.area.platform       → setup-areas.ts

这些 key 没有注册到翻译系统中,没有对应的翻译文件,defaultValue 永远不会被翻译覆盖。

4. 客户端没有 i18n resolution 能力

Studio 没有 i18n provider、没有 useTranslation hook、没有加载翻译资源。即使有翻译文件,客户端也无法根据 locale 解析 { key, defaultValue } — 只能退化为取 defaultValue。

5. API 响应格式不一致

同一个 GET /api/v1/meta/apps 接口:

  • CRM app: label: "Enterprise CRM" (字符串)
  • Setup app: label: { key: "setup.app.label", defaultValue: "Setup" } (对象)

客户端必须对每个 label 做类型判断,增加了所有消费端的复杂度。

6. 使用 I18nLabelSchema 的 schema 范围广

以下 spec 中的 schema 都使用了 I18nLabelSchema,意味着所有这些元数据的 label 都可能是对象:

  • AppSchema.label, .description
  • NavigationArea.label, .description
  • BaseNavItemSchema.label
  • PageSchema.label, .description
  • DashboardWidgetSchema.title, .description
  • ReportSchema.label, .description
  • ChartSchema.title, .subtitle
  • NotificationSchema.title, .message
  • KeyboardShortcutSchema.description
  • AriaPropsSchema.ariaLabel
  • 以及更多 UI schema

建议方案

核心原则:label 字段只接受 string,i18n key 由框架自动生成

Step 1: 标准化 i18n key 命名规则

<type>.<packageId>.<name>.label
<type>.<packageId>.<name>.description
<type>.<packageId>.<name>.navigation.<navId>.label

示例:

apps.com.objectstack.setup.setup.label           → "Setup"
apps.com.objectstack.setup.setup.description      → "Platform settings..."
apps.com.example.crm.crm_app.label               → "Enterprise CRM"
objects.com.objectstack.system.sys__user.label    → "User"
objects.com.objectstack.system.sys__user.fields.email.label → "Email"

Step 2: 注册时自动生成 key 并写入翻译服务

SchemaRegistry.registerApp() / registerItem() 中:

  1. label 存储为纯字符串(即 defaultValue)
  2. 自动生成标准 key,以 defaultValue 作为 fallback 写入 I18nService

Step 3: API 响应始终返回已解析的字符串

metadata REST endpoints 根据请求的 Accept-Language header 或 ?locale= 参数,调用 i18nService.t(key, locale) 解析后再返回。

Step 4: 废弃 I18nLabelSchema 联合类型

  • label, description 等字段改为 z.string()
  • 移除 I18nObjectSchema 在 metadata schema 中的使用
  • 开发者只需写 label: 'Setup'
  • 覆盖翻译通过翻译文件完成,而非内嵌 i18n 对象

迁移兼容性

  • 现有使用字符串 label 的配置(CRM、Todo 等)无需任何改动
  • 使用 i18n 对象的配置(SetupPlugin、AuthPlugin nav items)改为纯字符串
  • resolveLabel() workaround 可在迁移完成后移除

受影响的包

  • @objectstack/spec — I18nLabelSchema 定义和所有 UI schema
  • @objectstack/objectql — SchemaRegistry 注册 + Protocol API 响应
  • @objectstack/plugin-setup — setup-app.ts, setup-areas.ts
  • @objectstack/plugin-auth — nav item labels
  • @objectstack/plugin-security — nav item labels
  • @objectstack/plugin-audit — nav item labels
  • @objectstack/studio — MetadataInspector, app-sidebar 等所有渲染组件
  • @objectstack/rest — metadata endpoint 响应格式

Metadata

Metadata

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