Internationalization (I18N)
Yue is a component library consumed by applications, not an application-level translation platform. This page documents Yue's own locale contract: where the text inside components comes from, how language packs are loaded on demand, how a subtree inherits them, and why Yue does not depend on any translation engine directly.
The complete decision record (the stage 0 RFC) lives in the repository at docs/04-yue-i18n-rfc.md — this page is the version written for users.
Ownership: whose text is whose
| String | Owner | Mechanism |
|---|---|---|
| Accessible name of the clear button | Yue | catalog key input.clear |
| Visible copy of buttons, groups, segmented controls | Consumer | default slot |
| "Submitting" during loading | Consumer | default slot (the component only outputs aria-busy / is-loading) |
| Labels, helper text, validation and business errors | Consumer | the consumer's own DOM or application layer |
placeholder, aria-label, group names | Consumer | attribute passthrough |
One hard rule: do not add a Prop to a component in order to translate it. A prop such as clearLabel serves only one component, has to be repeated at every call site, and turns "translation" into "changing markup". Text that belongs to Yue goes into the catalog; text that belongs to you stays in your template.
Quick start
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')Four entry points, each responsible for one thing:
| Entry point | What it does |
|---|---|
@yue-ui/vue/plugin | Registers all components and installs size and locale together on the application |
@yue-ui/vue/locale | useLocale() / provideLocale() / createYueLocale() / YueLocaleProvider |
@yue-ui/vue/locale/en-US | The default (fallback) language pack; the root entry point already brings it along |
@yue-ui/vue/locale/zh-CN | The Chinese language pack; it only ends up in the build output if you import it |
You can also skip the plugin:
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') // '清空'Language packs
- Every language pack is a partial override of the same tree, not a whole-table replacement: write only the keys you want to change;
- Types are inferred from the catalog (
satisfies YueLocaleMessages); missing a leaf or misspelling a key is a compile error; - A language pack holds only Yue's own component copy, not business page text;
- A language pack is its own subpath: an application that only shows English does not download Chinese, and vice versa;
- Translation values cannot contain HTML, Vue templates, or component names — a message is one sentence.
key list
| key | en-US | zh-CN | Parameters | Accessibility |
|---|---|---|---|---|
input.clear | Clear | 清空 | None | Yes (aria-label) |
Keys are named by function, not by source location or language: input.clear, not clearButtonText. Every key registers its purpose, its parameters, and whether it enters the accessibility tree in YUE_MESSAGE_META in packages/vue/src/locale/catalog.ts, and pnpm audit:i18n checks that the two sides correspond one-to-one.
Normalization and the fallback chain
locale is always BCP 47: en-US, zh-CN, zh-Hans-CN. en_US is normalized to en-US and receives a warning (an underscore is not BCP 47).
When a language is requested, the runtime tries in order:
zh-Hans-CN → zh-CN → zh → fallback (default en-US) → en-USIt drops script first, then region, and finally falls back to the default language. Therefore:
- Providing only
zh-CNcan still serve a request forzh-Hans-CN; - When only
en-USis provided, switching tofr-FRdoes not leave the interface blank — what it gets is stillen-US; - The chain only decides "which language pack to use", not whether a key exists: a key missing from every pack on the chain is a genuine missing key.
Subtree inheritance
provideLocale() is an override, not a reset: options you do not name are inherited from the level above.
// application level: Chinese
app.use(YueUI, { locale: 'zh-CN', packs: { 'zh-CN': ZhCN } })
// the subtree changes only one string: language, language pack, and all other keys are kept
provideLocale({ messages: { input: { clear: 'Effacer' } } })The component version is <YueLocaleProvider>:
<YueLocaleProvider locale="zh-CN" :packs="{ 'zh-CN': zhCN }">
<YueInput v-model="keyword" clearable />
</YueLocaleProvider>locale and fallback are reactive: changing locale immediately switches the language of components already mounted in the subtree, with no remount needed. packs / adapter / direction are read only once, at creation time — a language pack is a module import, not render state.
Language and size are two independent axes: changing the language of a subtree does not also change size, and vice versa.
Diagnosing missing keys
A missing key is not rendered as an empty string, and it does not pass silently:
| Situation | Rendered result | Diagnostic reason |
|---|---|---|
| The key is not on the chain | the key itself (input.clear) | missing-key |
| The value is a blank string | the key itself | empty-value |
A plural message has no count | the other form | missing-param |
| A plural message lacks the current category | the other form | missing-plural-form |
{name} has no matching parameter | {name} is kept verbatim | missing-param |
| locale is not valid BCP 47 | used as-is and processing continues | invalid-locale |
Diagnostics are recorded in a readable queue, so tests and tooling can assert against them directly:
import { consumeLocaleDiagnostics } from '@yue-ui/vue/locale'
locale.t('input.missing') // 'input.missing' (not '')
consumeLocaleDiagnostics() // [{ reason: 'missing-key', key: 'input.missing', … }]The same diagnostic is printed to the console only once, but it enters the queue every time — a missing translation will not flood a render loop, and tests can still see it.
Numbers, dates, and plurals
Do not hand-write thousands separators, date formats, or plural branches. The locale instance hands all three to the platform:
locale.n(1234567.89) // Intl.NumberFormat
locale.n(1234, { style: 'currency', currency: 'EUR' }) // currency
locale.d(new Date(), { dateStyle: 'medium' }) // Intl.DateTimeFormatPlural messages are written as an object categorized by Intl.PluralRules, and must contain other:
// in the language pack
selection: {
items: { one: '{count} 项', other: '{count} 项' },
}
// in the component — there is no count === 1 branch
locale.t('selection.items', { count: 3 })The categories are determined by the language: Arabic has six forms, Japanese has only one. A component does not judge English grammar; it only hands the number to Intl.PluralRules.
The v1 catalog does not have plural keys yet: the runtime capability is implemented and covered by unit tests, but no key is added without a consumer (a translation nobody can verify is not a translation). Keys and language packs will be added when the first component that needs counting lands.
Direction (RTL)
direction is derived from the locale (ar / he / fa / ur … are rtl), and can also be explicitly overridden in the options:
createYueLocale({ locale: 'ar-EG' }).direction.value // 'rtl'
createYueLocale({ locale: 'ar-EG', direction: 'ltr' }).direction.value // 'ltr'Component CSS uses logical properties (padding-inline, border-start-start-radius, margin-block-start…), so a whole control mirrors itself under dir="rtl" with no need for a second set of styles:
<div dir="rtl">
<YueInput v-model="value" clearable />
</div>RTL does not have a real language pack yet
The runtime and the CSS are ready, and what the browser gate verifies is exactly "logical properties + dir passthrough", but there is not a single RTL language pack in the repository. Until one appears, RTL will not be claimed as complete.
External translation engines (adapter)
An application does not have to switch to Yue's mechanism. The adapter contract corresponds one-to-one with the three things on the instance itself:
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, // already a ref
t: (key, params) => i18n.global.t(key, params ?? {}),
// when n / d are omitted, Yue's own Intl implementation is used
},
})- The adapter provides
currentandt;n/dare optional; - The core types do not import any third-party types;
vue-i18nwill not appear in the base exports of@yue-ui/vue; - Unimplemented adapters (Intlayer / Paraglide / Tolgee) are not on the "supported" list — they need an application-side build process or plugin, and will only be built once a real consumption scenario appears.
The reference vue-i18n implementation is in this repository: apps/docs/.vitepress/theme/adapters/vue-i18n.ts (13 lines, the same shape as above). Two pieces of evidence back it: tests/i18n/vue-i18n-adapter.test.ts drives a real YueInput with a real composer, and verify:tarball asserts, inside an installed temporary project, that switching the engine's language re-renders a component that is already mounted.
Gates
| Command | What it checks |
|---|---|
corepack pnpm test | runtime unit tests for the hooks (inheritance, fallback, missing keys, interpolation, plurals, Intl, SSR without browser objects, Symbol consistency), unit tests for the vue catalog (key consistency, parameter consistency, no empty values, no orphan keys), SSR and hydration (renderToString output per locale and through the fallback chain, createSSRApp hydrating server HTML without replacing the nodes, zero hydration mismatches) and the vue-i18n adapter tests |
corepack pnpm audit:i18n | key parity, parameter parity, empty values, orphan keys, one-to-one metadata correspondence; hard-coded user-facing copy is found structurally: template text nodes, user-visible attributes such as aria-label / title / alt / placeholder, literal bindings, and text in scripts — in English as well as Chinese, without flagging developer warnings or selectors |
corepack pnpm audit:docs:i18n | Chinese/English page mirroring, heading structure, code blocks, component tags and attributes, internal links and anchors |
corepack pnpm verify:dist | every page carries lang, a canonical URL and hreflang alternates (including x-default), each pointing at the same page in the other tree; the sitemap covers both languages; and each locale's built page already carries its own copy (correct before hydration, not only after) |
corepack pnpm verify:visual | switching language in a real browser, document.documentElement.lang, the fallback chain (a regional tag with no pack of its own), subtree inheritance, RTL mirroring, long copy not overflowing |
corepack pnpm verify:treeshaking | the root entry point does not carry zh-CN; the language pack subpath does not carry component code |
corepack pnpm verify:tarball | after installing into a temporary project: language packs install separately, the adapter is optional, SSR imports do not touch the browser |
What we deliberately do not do
- We do not hard-code Intlayer, Paraglide, vue-i18n, or Tolgee as the only option;
- We do not add one component Prop per string;
- We do not express cross-language sentences with string concatenation;
- We do not stuff HTML / Vue templates into translation values;
- We do not silently treat missing translations as done;
- We do not duplicate large amounts of English documentation before the underlying locale is complete.