Skip to content

国际化(I18N) ​

Yue 是被应用消费的组件库,不是应用级翻译平台。这一页说明 Yue 自己的 locale 契约:组件内部文案怎么来、语言包怎么按需加载、子树怎么继承、以及为什么 Yue 不直接依赖任何翻译引擎。

完整决策记录(阶段 0 的 RFC)在仓库的 docs/04-yue-i18n-rfc.md——本页是给使用者看的版本。

所有权:谁的文字是谁的 ​

字符串所有者机制
清空按钮的可访问名称Yuecatalog key input.clear
按钮、分组、分段控件的可见文案消费者default 插槽
loading 期间的「提交中」消费者默认插槽(组件只输出 aria-busy / is-loading)
标签、帮助文案、校验与业务错误消费者消费者自己的 DOM 或应用层
placeholder、aria-label、分组名称消费者属性透传

一条硬性规则:不要为了翻译给组件加 Prop。 clearLabel 这样的 prop 只服务一个组件、要在每个调用点重复,还会把「翻译」变成「改标记」。属于 Yue 的文字进 catalog,属于你的文字留在你的模板里。

快速开始 ​

ts
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/localeuseLocale() / provideLocale() / createYueLocale() / YueLocaleProvider
@yue-ui/vue/locale/en-US默认(fallback)语言包,根入口已经带上了它
@yue-ui/vue/locale/zh-CN中文语言包,只有导入才会进包

不用插件也可以:

ts
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 清单 ​

keyen-USzh-CN参数无障碍
input.clearClear清空无是(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)。

请求一个语言时,运行时会依次尝试:

text
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() 是覆盖,不是重置:未点名的选项沿用上层。

ts
// 应用级:中文
app.use(YueUI, { locale: 'zh-CN', packs: { 'zh-CN': ZhCN } })

// 子树只改一个字符串:语言、语言包、其余 key 全部保留
provideLocale({ messages: { input: { clear: 'Effacer' } } })

组件版本是 <YueLocaleProvider>:

vue
<YueLocaleProvider locale="zh-CN" :packs="{ 'zh-CN': zhCN }">
  <YueInput v-model="keyword" clearable />
</YueLocaleProvider>

locale 与 fallback 是响应式的:改 locale 会让子树里已经挂载的组件立刻换语言,不需要重新挂载。packs / adapter / direction 只在创建时读取一次——语言包是模块导入,不是渲染状态。

语言与尺寸是两条独立的轴:子树换语言不会顺手改掉 size,反之亦然。

缺失 key 的诊断 ​

缺失的 key 不会渲染成空字符串,也不会静默通过:

情况渲染结果诊断 reason
链上没有这个 keykey 本身(input.clear)missing-key
值是空白字符串key 本身empty-value
复数消息没有 countother 形式missing-param
复数消息缺当前分类other 形式missing-plural-form
{name} 没有对应参数保留 {name} 原文missing-param
locale 不是合法 BCP 47原样使用并继续invalid-locale

诊断会记录在一个可读的队列里,测试与工具可以直接断言:

ts
import { consumeLocaleDiagnostics } from '@yue-ui/vue/locale'

locale.t('input.missing')            // 'input.missing'(不是 '')
consumeLocaleDiagnostics()            // [{ reason: 'missing-key', key: 'input.missing', … }]

同一条诊断只会在控制台打印一次,但每次都会进队列——一个缺失的翻译在渲染循环里不会刷屏,测试也仍然看得见。

数字、日期与复数 ​

不要手写千分位、日期格式或复数分支。locale 实例把这三件事都交给平台:

ts
locale.n(1234567.89)                                  // Intl.NumberFormat
locale.n(1234, { style: 'currency', currency: 'EUR' }) // 货币
locale.d(new Date(), { dateStyle: 'medium' })          // Intl.DateTimeFormat

复数消息写成按 Intl.PluralRules 分类的对象,必须含 other:

ts
// 语言包里
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),也可以在选项里显式覆盖:

ts
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" 下会自己镜像,不需要第二套样式:

html
<div dir="rtl">
  <YueInput v-model="value" clearable />
</div>

RTL 还没有真实的语言包

运行时与 CSS 已经就绪,浏览器门禁验证的也是「逻辑属性 + dir 透传」,但仓库里没有一个 RTL 语言包。在出现之前不会声称 RTL 已完成。

外部翻译引擎(adapter) ​

应用不必改用 Yue 的机制。adapter 的契约与实例本身的三件事一一对应:

ts
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 testhooks 的运行时单测(继承、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:i18nkey 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 完成前复制大量英文文档。