Skip to content

composeStacksi18n 仍是 last-wins —— #5005 裁决否掉的那个形状,只剩这一个键还在用 #5051

Description

@xuyushun441-sys

发现于 #5005 实现过程,与本单修复无关(#5005 的主题是被丢掉的键,i18n 没被丢),按 Prime Directive #10 单独立单,未认领

事实

#5005 落地后,composeStacks 的非数组顶层键统一为同值放行 / 冲突报错。唯一的例外是 i18n:

// packages/spec/src/stack.zod.ts — composeStacks 第 2 步
// 2. i18n — last-wins (single object, not array). Pre-existing strategy,
//    deliberately untouched by #5005; see COMPOSE_KEY_DISPOSITIONS.
for (let i = stacks.length - 1; i >= 0; i--) {
  if (stacks[i].i18n) {
    composed.i18n = stacks[i].i18n;
    break;
  }
}

实测(#5005 的控制用例钉住了现状):

const a = defineStack({ manifest: A, i18n: { defaultLocale: 'en'    } });
const b = defineStack({ manifest: B, i18n: { defaultLocale: 'zh-CN' } });
composeStacks([a, b]).i18n   // → { defaultLocale: 'zh-CN' } —— a 的声明无声消失

为什么值得单独看一眼

维护者 2026-08-04 在 #5005 上裁决 ⛔ 不做 last-wins,理由是「静默安全降级 —— 先声明的栈被后组合者无声覆盖」。i18n 就是字面意义上的 last-wins:一个装了 CRM(defaultLocale: 'zh-CN')和某个附加包(defaultLocale: 'en')的组合栈,谁在数组里靠后谁定语言,作者不会收到任何提示。

后果比 api / server 轻(是本地化而非授权),这也是 #5005 明确把它留在范围外的原因 —— 它是这里唯一本来就有明确、可工作策略的键,改它会打断今天真在依赖它的组合,而 #5005 修的是「键消失」。但改完之后它成了整个顶层键面上唯一的不一致,值得单独定一次。

待裁决(不要猜)

  • A. 与其余非数组键对齐 —— 同值放行、冲突报错带处方。一致、响亮;代价是今天靠 last-wins 覆盖语言配置的组合会开始报错(仓内实测无真实调用点,外部未知)。
  • B. 保留 last-wins,但冲突时 warn 一次并点名 —— 不破坏现有组合,把静默变成有声。比 A 弱,但零破坏。
  • C. 维持现状 —— 认定本地化配置的后者优先是刻意语义,并把这个理由写进 docblock,免得下一个人再提一次。

ComposeStacksOptionsSchema 上不建议为此加旋钮:#5005 已裁决显式覆盖机制留给定制故事真拉动时统一设计,单给 i18n 开一个口子会预支那个设计。

关联:#5005(合成语义裁决与实现)、#4910

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions