Skip to content

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 ​

StringOwnerMechanism
Accessible name of the clear buttonYuecatalog key input.clear
Visible copy of buttons, groups, segmented controlsConsumerdefault slot
"Submitting" during loadingConsumerdefault slot (the component only outputs aria-busy / is-loading)
Labels, helper text, validation and business errorsConsumerthe consumer's own DOM or application layer
placeholder, aria-label, group namesConsumerattribute 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 ​

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')

Four entry points, each responsible for one thing:

Entry pointWhat it does
@yue-ui/vue/pluginRegisters all components and installs size and locale together on the application
@yue-ui/vue/localeuseLocale() / provideLocale() / createYueLocale() / YueLocaleProvider
@yue-ui/vue/locale/en-USThe default (fallback) language pack; the root entry point already brings it along
@yue-ui/vue/locale/zh-CNThe Chinese language pack; it only ends up in the build output if you import it

You can also skip the plugin:

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') // '清空'

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 ​

keyen-USzh-CNParametersAccessibility
input.clearClear清空NoneYes (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:

text
zh-Hans-CN  →  zh-CN  →  zh  →  fallback (default en-US)  →  en-US

It drops script first, then region, and finally falls back to the default language. Therefore:

  • Providing only zh-CN can still serve a request for zh-Hans-CN;
  • When only en-US is provided, switching to fr-FR does not leave the interface blank — what it gets is still en-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.

ts
// 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>:

vue
<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:

SituationRendered resultDiagnostic reason
The key is not on the chainthe key itself (input.clear)missing-key
The value is a blank stringthe key itselfempty-value
A plural message has no countthe other formmissing-param
A plural message lacks the current categorythe other formmissing-plural-form
{name} has no matching parameter{name} is kept verbatimmissing-param
locale is not valid BCP 47used as-is and processing continuesinvalid-locale

Diagnostics are recorded in a readable queue, so tests and tooling can assert against them directly:

ts
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:

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

Plural messages are written as an object categorized by Intl.PluralRules, and must contain other:

ts
// 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:

ts
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:

html
<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:

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,          // 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 current and t; n / d are optional;
  • The core types do not import any third-party types; vue-i18n will 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 ​

CommandWhat it checks
corepack pnpm testruntime 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:i18nkey 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:i18nChinese/English page mirroring, heading structure, code blocks, component tags and attributes, internal links and anchors
corepack pnpm verify:distevery 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:visualswitching 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:treeshakingthe root entry point does not carry zh-CN; the language pack subpath does not carry component code
corepack pnpm verify:tarballafter 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.