读完这篇,你能从零搭出一套能上线运行的 Vue 3 多语言方案:语言包怎么组织、怎么切换并记住用户选择、怎么按需加载,以及几个几乎人人都会踩的坑。
第一步:安装并创建 i18n 实例
Vue 3 必须用 vue-i18n 的 9.x 版本(8.x 是给 Vue 2 的,混用会直接报错)。
npm install vue-i18n@9
新建 src/i18n/index.js:
import { createI18n } from 'vue-i18n'
import zhCN from './locales/zh-CN'
import enUS from './locales/en-US'
const i18n = createI18n({
legacy: false, // 关键:启用 Composition API 模式
globalInjection: true, // 关键:模板里才能直接用 $t
locale: localStorage.getItem('locale') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: { 'zh-CN': zhCN, 'en-US': enUS }
})
export default i18n
在 main.js 挂载:
import { createApp } from 'vue'
import App from './App.vue'
import i18n from './i18n'
createApp(App).use(i18n).mount('#app')
注意:legacy: false 是必选项。不写这一行,useI18n() 在组件里拿不到实例,会报 "Not available in legacy mode"。
第二步:组织语言包文件
按模块拆文件,别堆成一个几百行的巨无霸。src/i18n/locales/zh-CN.js:
export default {
common: { confirm: '确定', cancel: '取消', loading: '加载中...' },
login: { title: '登录', username: '用户名', password: '密码' },
message: { welcome: '欢迎,{name}', count: '共 {n} 条 | 共 {n} 条' }
}
en-US.js 保持完全相同的键结构:
export default {
common: { confirm: 'Confirm', cancel: 'Cancel', loading: 'Loading...' },
login: { title: 'Sign In', username: 'Username', password: 'Password' },
message: { welcome: 'Welcome, {name}', count: 'No items | One item | {n} items' }
}
注意:两种语言的 key 必须一一对应,少一个不会报错,只会在页面上静默显示成 key 字符串,这类问题在上线后才被发现,返工成本很高。建议写个脚本对比两边的 key 集合。
第三步:在组件里使用
<script setup>
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>
<template>
<h1>{{ $t('login.title') }}</h1>
<p>{{ t('message.welcome', { name: '阿乐' }) }}</p>
<button @click="locale = 'en-US'">English</button>
</template>
模板里用 $t(依赖 globalInjection: true),JS 里用 t()。
第四步:切换语言并持久化
语言状态要全局共享,最好放到 Pinia 或一个 composable 里:
import { useI18n } from 'vue-i18n'
import { watch } from 'vue'
export function useLocale() {
const { locale } = useI18n()
const setLocale = (lang) => {
locale.value = lang
localStorage.setItem('locale', lang)
document.documentElement.lang = lang
}
return { locale, setLocale }
}
首次进入时按浏览器语言自动判断:
const navLang = navigator.language.toLowerCase()
const defaultLang = navLang.startsWith('zh') ? 'zh-CN' : 'en-US'
注意:切换后 document.title、路由 meta 标题、Element Plus / Ant Design Vue 这类 UI 库的语言包都不会自动跟着变,需要各自单独处理,否则会出现"页面中文、日期控件英文"的割裂感。
第五步:语言包按需加载
语言一多,首屏体积会爆炸。用动态引入拆成独立 chunk:
const loadMessages = async (lang) => {
const messages = await import(`./locales/${lang}.js`)
i18n.global.setLocaleMessage(lang, messages.default)
i18n.global.locale.value = lang
}
注意:Vite 打包时用变量拼接 import 路径会触发警告,路径尽量写成可静态分析的形式(如上),否则要配 import.meta.glob。
第六步:非组件环境(路由、请求拦截器)里怎么用
组件外拿不到 useI18n(),直接读全局实例:
import i18n from '@/i18n'
const t = i18n.global.t
router.beforeEach((to) => {
document.title = to.meta.titleKey ? t(to.meta.titleKey) : 'My App'
})
第七步:补上数字与日期格式化
i18n 内置了格式化能力,不要自己拼字符串:
const { n, d } = useI18n()
n(1234567) // 1,234,567
d(new Date(), 'long') // 2025年3月1日 / March 1, 2025
日期格式需要在创建实例时配置 datetimeFormats,否则 'long' 会找不到格式定义。
小结
- vue-i18n 必须用 9.x,配置里
legacy: false 和 globalInjection: true 是两大开关
- 语言包按模块拆分,多语言 key 必须严格对齐,否则静默显示 key
- 语言切换要写回 localStorage,并同步
html.lang
- 语言包用动态
import 拆 chunk,控制首屏体积
- 组件外用
i18n.global.t,UI 库、路由标题、日期格式都要单独跟随切换