国际化(I18N)
Yue 是被应用消费的组件库,不是应用级翻译平台。这一页说明 Yue 自己的 locale 契约:组件内部文案怎么来、语言包怎么按需加载、子树怎么继承、以及为什么 Yue 不直接依赖任何翻译引擎。
完整决策记录(阶段 0 的 RFC)在仓库的 docs/04-yue-i18n-rfc.md——本页是给使用者看的版本。
所有权:谁的文字是谁的
| 字符串 | 所有者 | 机制 |
|---|---|---|
| 清空按钮的可访问名称 | Yue | catalog key input.clear |
| 按钮、分组、分段控件的可见文案 | 消费者 | default 插槽 |
| loading 期间的「提交中」 | 消费者 | 默认插槽(组件只输出 aria-busy / is-loading) |
| 标签、帮助文案、校验与业务错误 | 消费者 | 消费者自己的 DOM 或应用层 |
placeholder、aria-label、分组名称 | 消费者 | 属性透传 |
一条硬性规则:不要为了翻译给组件加 Prop。 clearLabel 这样的 prop 只服务一个组件、要在每个调用点重复,还会把「翻译」变成「改标记」。属于 Yue 的文字进 catalog,属于你的文字留在你的模板里。
快速开始
import { createApp } from 'vue'
import YueUI from '@yue-ui/vue/plugin'
import ZhCN from '@yue-ui/vue/locale/zh-CN'
import '@yue-ui/design-tokens/index.css'
import '@yue-ui/vue/style.css'
createApp(App)
.use(YueUI, { locale: 'zh-CN', packs: { 'zh-CN': ZhCN } })
.mount('#app')四个入口,各管一件事:
| 入口 | 作用 |
|---|---|
@yue-ui/vue/plugin | 全量注册组件,并把 size 与 locale 一起装到应用上 |
@yue-ui/vue/locale | useLocale() / provideLocale() / createYueLocale() / YueLocaleProvider |
@yue-ui/vue/locale/en-US | 默认(fallback)语言包,根入口已经带上了它 |
@yue-ui/vue/locale/zh-CN | 中文语言包,只有导入才会进包 |
不用插件也可以:
import { createYueLocale } from '@yue-ui/vue/locale'
import ZhCN from '@yue-ui/vue/locale/zh-CN'
const locale = createYueLocale({ locale: 'zh-CN', packs: { 'zh-CN': ZhCN } })
locale.t('input.clear') // '清空'语言包
- 每个语言包是同一棵树的部分覆盖,不是整表替换:只写你要改的键即可;
- 类型由 catalog 推导(
satisfies YueLocaleMessages),漏掉一个叶子或写错一个键都是编译错误; - 语言包只放 Yue 自己的组件文案,不放业务页面文本;
- 语言包是独立子路径:一个只显示英文的应用不会下载中文,反之亦然;
- 翻译值里不能有 HTML、Vue 模板或组件名——一条消息就是一句话。
key 清单
| key | en-US | zh-CN | 参数 | 无障碍 |
|---|---|---|---|---|
input.clear | Clear | 清空 | 无 | 是(aria-label) |
key 按功能命名,不按源码位置或语言:input.clear,不是 clearButtonText。每个 key 在 packages/vue/src/locale/catalog.ts 的 YUE_MESSAGE_META 里登记用途、参数与是否进入无障碍树,pnpm audit:i18n 会检查两边一一对应。
规范化与 fallback 链
locale 一律是 BCP 47:en-US、zh-CN、zh-Hans-CN。en_US 会被规范成 en-US 并收到一条警告(下划线不是 BCP 47)。
请求一个语言时,运行时会依次尝试:
zh-Hans-CN → zh-CN → zh → fallback(默认 en-US) → en-US先丢 script,再丢 region,最后落到默认语言。所以:
- 只提供
zh-CN也能服务zh-Hans-CN的请求; - 只提供
en-US时,切到fr-FR不会让界面变成空白——它拿到的仍是en-US; - 链只决定「用哪一套语言包」,不决定 key 是否存在:链上任何一套都没有这个 key,才是真正的缺失 key。
子树继承
provideLocale() 是覆盖,不是重置:未点名的选项沿用上层。
// 应用级:中文
app.use(YueUI, { locale: 'zh-CN', packs: { 'zh-CN': ZhCN } })
// 子树只改一个字符串:语言、语言包、其余 key 全部保留
provideLocale({ messages: { input: { clear: 'Effacer' } } })组件版本是 <YueLocaleProvider>:
<YueLocaleProvider locale="zh-CN" :packs="{ 'zh-CN': zhCN }">
<YueInput v-model="keyword" clearable />
</YueLocaleProvider>locale 与 fallback 是响应式的:改 locale 会让子树里已经挂载的组件立刻换语言,不需要重新挂载。packs / adapter / direction 只在创建时读取一次——语言包是模块导入,不是渲染状态。
语言与尺寸是两条独立的轴:子树换语言不会顺手改掉 size,反之亦然。
缺失 key 的诊断
缺失的 key 不会渲染成空字符串,也不会静默通过:
| 情况 | 渲染结果 | 诊断 reason |
|---|---|---|
| 链上没有这个 key | key 本身(input.clear) | missing-key |
| 值是空白字符串 | key 本身 | empty-value |
复数消息没有 count | other 形式 | missing-param |
| 复数消息缺当前分类 | other 形式 | missing-plural-form |
{name} 没有对应参数 | 保留 {name} 原文 | missing-param |
| locale 不是合法 BCP 47 | 原样使用并继续 | invalid-locale |
诊断会记录在一个可读的队列里,测试与工具可以直接断言:
import { consumeLocaleDiagnostics } from '@yue-ui/vue/locale'
locale.t('input.missing') // 'input.missing'(不是 '')
consumeLocaleDiagnostics() // [{ reason: 'missing-key', key: 'input.missing', … }]同一条诊断只会在控制台打印一次,但每次都会进队列——一个缺失的翻译在渲染循环里不会刷屏,测试也仍然看得见。
数字、日期与复数
不要手写千分位、日期格式或复数分支。locale 实例把这三件事都交给平台:
locale.n(1234567.89) // Intl.NumberFormat
locale.n(1234, { style: 'currency', currency: 'EUR' }) // 货币
locale.d(new Date(), { dateStyle: 'medium' }) // Intl.DateTimeFormat复数消息写成按 Intl.PluralRules 分类的对象,必须含 other:
// 语言包里
selection: {
items: { one: '{count} 项', other: '{count} 项' },
}
// 组件里——没有 count === 1 分支
locale.t('selection.items', { count: 3 })分类由语言决定:阿拉伯语有六种形式,日语只有一种。组件不判断英文语法,它只把数字交给 Intl.PluralRules。
v1 的 catalog 还没有复数 key:运行时能力已实现并有单测覆盖,但没有消费方就不添加 key(没人能验证的翻译不是翻译)。第一个需要计数的组件落地时补 key 与语言包。
方向(RTL)
direction 由 locale 推导(ar / he / fa / ur … 为 rtl),也可以在选项里显式覆盖:
createYueLocale({ locale: 'ar-EG' }).direction.value // 'rtl'
createYueLocale({ locale: 'ar-EG', direction: 'ltr' }).direction.value // 'ltr'组件 CSS 使用逻辑属性(padding-inline、border-start-start-radius、margin-block-start…),所以整块控件在 dir="rtl" 下会自己镜像,不需要第二套样式:
<div dir="rtl">
<YueInput v-model="value" clearable />
</div>RTL 还没有真实的语言包
运行时与 CSS 已经就绪,浏览器门禁验证的也是「逻辑属性 + dir 透传」,但仓库里没有一个 RTL 语言包。在出现之前不会声称 RTL 已完成。
外部翻译引擎(adapter)
应用不必改用 Yue 的机制。adapter 的契约与实例本身的三件事一一对应:
import { createI18n } from 'vue-i18n'
import { createYueLocale } from '@yue-ui/vue/locale'
const i18n = createI18n({ legacy: false, locale: 'zh-CN', messages })
const locale = createYueLocale({
adapter: {
current: i18n.global.locale, // 已经是 ref
t: (key, params) => i18n.global.t(key, params ?? {}),
// n / d 省略时使用 Yue 自己的 Intl 实现
},
})- adapter 提供
current与t,n/d可选; - 核心类型不 import 任何第三方类型,
@yue-ui/vue的基础导出里不会出现vue-i18n; - 未实现的 adapter(Intlayer / Paraglide / Tolgee)不在「已支持」列表里——它们需要应用侧的构建流程或插件,只有出现真实消费场景才会做。
vue-i18n 的参考实现就在仓库里:apps/docs/.vitepress/theme/adapters/vue-i18n.ts(13 行,与上面同形)。它有两个证据支撑:tests/i18n/vue-i18n-adapter.test.ts 用真实的 composer 驱动真实的 YueInput,verify:tarball 则在装好的临时项目里断言「切换引擎语言后,已挂载的组件跟着换字」。
门禁
| 命令 | 检查什么 |
|---|---|
corepack pnpm test | hooks 的运行时单测(继承、fallback、缺失 key、插值、复数、Intl、SSR 无浏览器对象、Symbol 一致性)、vue 的 catalog 单测(key 一致性、参数一致性、无空值、无孤儿 key)、SSR 与 hydration(renderToString 按 locale 与 fallback 链输出、createSSRApp 在服务端 HTML 上水合且节点对象不变、0 条 hydration mismatch)与 vue-i18n adapter 测试 |
corepack pnpm audit:i18n | key parity、参数 parity、空值、孤儿 key、metadata 一一对应;按结构检查硬编码用户文案:模板文本节点、aria-label / title / alt / placeholder 这类用户可见属性、字面量绑定、脚本里的文本(英文与中文都查),开发者警告与选择器不误报 |
corepack pnpm audit:docs:i18n | 中英文页面镜像、标题结构、代码块、组件标签与属性、内部链接与锚点 |
corepack pnpm verify:dist | 每个页面都有 lang、canonical 与 hreflang(含 x-default),并且指向另一棵树的同一个页面;sitemap 收录两种语言;构建产物里每个 locale 的页面已经带着自己的文案(水合前就是对的,不只是水合后) |
corepack pnpm verify:visual | 真实浏览器里切换语言、document.documentElement.lang、fallback 链(无对应语言包的地区标签)、子树继承、RTL 镜像、长文案不溢出 |
corepack pnpm verify:treeshaking | 根入口不带 zh-CN;语言包子路径不带组件代码 |
corepack pnpm verify:tarball | 装进临时项目后:语言包单独安装、adapter 可选、SSR 导入不触碰浏览器 |
明确不做
- 不把 Intlayer、Paraglide、vue-i18n 或 Tolgee 硬编码为唯一方案;
- 不为每个字符串添加一个组件 Prop;
- 不用字符串拼接表达跨语言句子;
- 不把 HTML / Vue 模板塞进翻译值;
- 不把缺失翻译静默当成完成;
- 不在底层 locale 完成前复制大量英文文档。