Skip to content

Internationalization

One "Saved successfully" string costs two files: a key in the Chinese dictionary, a key in the English one. The view itself contains nothing but t('common.saveOk'), not a single hardcoded character. Switching languages therefore becomes switching dictionaries, and every string you write from here on owes that debt twice.

How it hangs together

The kernel package's locales/index.ts wires up vue-i18n:

ts
export const i18n = createI18n({
  legacy: false,
  locale: 'zh-CN',
  fallbackLocale: 'en-US',
  messages: {
    'zh-CN': zhCN as Messages,
    'en-US': enUS as Messages,
  },
})

/** A translator for non-setup contexts (utility functions). */
export const t = i18n.global.t

Composition API mode (legacy: false), default locale zh-CN, falling back to en-US for any key missing from the active locale. Say zh-CN.ts is missing a key: the UI doesn't throw and doesn't show the raw key name either, it just displays whatever English string sits at that key in en-US.ts. The built-in translation surface is two files — locales/zh-CN.ts and locales/en-US.ts — with no per-component splitting and no lazy loading.

Those two live in the package: an app can't edit them, and doesn't need to. Put your own text in the app's src/locales/ext/<locale>/<module>.ts; main.ts hands them to the kernel with createSmartAdmin({ locales: import.meta.glob('./locales/ext/*/*.ts', { eager: true }) }), and registerLocales deep-merges them into the dictionaries by namespace. The filename becomes the top-level namespace, and overriding a built-in string touches only the one key you write. See the README in the template's copy of that directory.

App.vue keeps Naive UI's own locale (which drives its built-in strings — date pickers, pagination, and so on) in sync with the app locale:

ts
const naiveLocale = computed(() => (app.locale === 'en-US' ? enUS : zhCN))
const naiveDateLocale = computed(() => (app.locale === 'en-US' ? dateEnUS : dateZhCN))

watch(
  () => app.locale,
  (l) => {
    i18n.global.locale.value = l
  },
  { immediate: true },
)

So there are really two locale surfaces kept in lockstep from one source (app.locale): vue-i18n's i18n.global.locale (which drives t()) and Naive UI's n-config-provider :locale/:date-locale (which drives its internal components' text).

Adding a translation key

Both message files are grouped by feature namespace — common, app, settings, login, module, menu, user, config, dict, org, file, notice, role, log, profile, error, recycle, and more. Two real examples:

ts
// zh-CN.ts
user: {
  title: '用户管理',
  account: '账号',
  deleteConfirm: '确定删除用户「{name}」?',
}

// en-US.ts
user: {
  title: 'Users',
  account: 'Account',
  deleteConfirm: 'Delete user "{name}"?',
}

To add a key:

  1. Pick the right namespace (only open a new top-level one for a genuinely new feature area).
  2. Add it to both languages at once, at the same dot-path, with matching interpolation placeholders ({name}, {count}, …). Kernel keys go in the package's zh-CN.ts and en-US.ts; app keys go in ext/zh-CN/<module>.ts and ext/en-US/<module>.ts.
  3. Use it inside a component with t('namespace.key'); outside setup (e.g. functions in utils/), use the exported t helper, which an app imports from smart-admin-web.

Frontend Standards makes this a hard rule: all visible text in views goes through t('...') — no hardcoded Chinese/English string literals.

Switching language at runtime

stores/app.ts owns the locale as a persisted UI preference:

ts
export type Locale = 'zh-CN' | 'en-US'

state: () => ({ ...DEFAULTS, collapsed: false, locale: 'zh-CN' as Locale }),

actions: {
  setLocale(l: Locale) {
    this.locale = l
  },
}

Calling useAppStore().setLocale('en-US') updates app.locale; the whole store persists to localStorage (key app) via Pinia's persistence plugin, so the choice survives a reload. App.vue's watch(() => app.locale, ...) (shown above) then pushes that value into i18n.global.locale.value, and the naiveLocale/naiveDateLocale computed properties derive from the same app.locale — so vue-i18n text and Naive UI's built-in text switch together, in one render.

How backend error codes land on the text in error.* (the numeric-code → msgKey → translation-key mapping) isn't on this page — it's bound up with the response contract, in Adapting to the Backend.

Released under the Apache License 2.0