精通 Vue 应用多语言:使用 vue-i18n 的全面指南
在构建现代 Web 应用时,考虑到全球用户是至关重要的。这意味着你的应用需要能够支持多种语言,并根据用户的偏好或浏览器的设置来显示相应的文本内容。这个过程称为国际化(Internationalization,简称 i18n),而使其适应特定语言和文化的过程称为本地化(Localization,简称 l10n)。
对于 Vue.js 开发者而言,vue-i18n 是实现应用多语言功能的首选库。它功能强大、易于使用,并且与 Vue.js 的生态系统紧密集成。本文将带你深入了解 vue-i18n,从安装、基本配置,到处理文本、复数、日期、数字,以及高级主题如异步加载和与其他库的集成。
第一部分:理解国际化(i18n)与本地化(l10n)
在开始技术实现之前,先快速回顾一下 i18n 和 l10n 的基本概念:
- 国际化 (i18n):是使应用具备支持多种语言和文化习惯的能力的过程。它通常涉及从代码中抽象出所有的文本内容,以便它们可以被翻译,并确保应用能够处理不同的日期格式、数字格式、货币符号等。
- 本地化 (l10n):是根据特定地区或语言(称为“区域设置”或“locale”)调整国际化应用的过程。这包括翻译文本、调整日期/时间/数字格式、使用正确的货币符号,甚至调整布局、颜色或图像,以适应当地文化。
i18n 是基础,l10n 是在 i18n 基础上的具体实现。vue-i18n 库主要帮助我们实现 i18n 的核心部分——文本内容的抽象和管理,并提供本地化所需的格式化工具。
第二部分:为什么选择 vue-i18n?
市场上有许多 i18n 库,为什么 vue-i18n 是 Vue 应用的推荐选择?
- 官方推荐和深度集成: vue-i18n 是 Vue.js 核心团队成员 Kazuya Kawaguchi (kazupon) 开发和维护的。它与 Vue 的响应式系统、组件生命周期、单文件组件 (
.vue
文件) 等特性完美集成。 - 功能丰富: 它提供了处理文本翻译、复数、日期、数字、货币等多种本地化需求的功能。
- 使用方便: 提供了简洁的模板语法 (
$t
,$tc
,$d
,$n
) 和组件 (<i18n>
) 来进行翻译。 - 性能优化: 支持异步加载翻译消息,可以显著减小初始包体积。
- 活跃的社区和良好的文档: 作为官方推荐库,它拥有活跃的社区支持和详细的文档。
综上所述,对于 Vue 应用来说,vue-i18n 是一个强大、可靠且与框架高度契合的解决方案。
第三部分:vue-i18n 的基本使用
让我们从零开始,在一个 Vue 项目中集成并使用 vue-i18n。
步骤 1:安装 vue-i18n
首先,你需要在你的 Vue 项目中安装 vue-i18n。根据你的 Vue 版本,选择相应的 vue-i18n 版本:
- Vue 3: 使用
vue-i18n@^9.x
- Vue 2: 使用
vue-i18n@^8.x
本文主要以 Vue 3 和 vue-i18n v9+ 为例进行讲解,但大部分核心概念和用法也适用于 Vue 2 版本。
使用 npm 或 yarn 安装:
“`bash
npm install vue-i18n@next # For Vue 3
或者
yarn add vue-i18n@next # For Vue 3
如果是 Vue 2
npm install vue-i18n@^8
或者
yarn add vue-i18n@^8
“`
步骤 2:创建翻译文件
接下来,你需要存放不同语言的翻译消息。通常,我们会创建一个单独的目录(例如 locales
或 lang
),并在其中为每种语言创建一个文件。这些文件通常使用 JSON 格式,但也可以使用 JavaScript 对象、YAML 等。
例如,创建一个 locales
目录,并在其中创建 en.json
(英语)和 zh.json
(中文)文件:
src/locales/en.json
:
json
{
"message": {
"hello": "Hello, world!",
"welcome": "Welcome, {name}!",
"greeting": "Hello, <strong>{name}</strong>!"
},
"button": {
"submit": "Submit",
"cancel": "Cancel"
},
"menu": {
"home": "Home",
"about": "About"
}
}
src/locales/zh.json
:
json
{
"message": {
"hello": "你好,世界!",
"welcome": "欢迎,{name}!",
"greeting": "你好,<strong>{name}</strong>!"
},
"button": {
"submit": "提交",
"cancel": "取消"
},
"menu": {
"home": "首页",
"about": "关于我们"
}
}
这些文件包含按层级组织的键值对。键(如 message.hello
)是唯一的标识符,值是对应语言的翻译文本。
步骤 3:初始化并配置 vue-i18n 实例
现在,在你的 Vue 应用入口文件(通常是 src/main.js
或 src/main.ts
)中创建并配置 vue-i18n
实例。
“`javascript
// src/main.js
import { createApp } from ‘vue’;
import { createI18n } from ‘vue-i18n’; // 导入 createI18n
import App from ‘./App.vue’;
// 导入语言文件
import en from ‘./locales/en.json’;
import zh from ‘./locales/zh.json’;
// 1. 创建 i18n 实例
const i18n = createI18n({
// 遗留模式设置为 false 以兼容 Composition API 和新的 API
legacy: false,
// 全局注入 $t 等方法到 Options API,如果只用 Composition API 可以不设置或设置为 false
globalInjection: true,
// 设置默认语言
locale: ‘en’,
// 设置备用语言,当当前语言找不到对应翻译时,会尝试使用备用语言
fallbackLocale: ‘en’,
// 导入所有语言消息
messages: {
en,
zh
}
});
const app = createApp(App);
// 2. 将 i18n 实例挂载到 Vue 应用实例上
app.use(i18n);
app.mount(‘#app’);
“`
配置说明:
legacy: false
: Vue 3 版本推荐使用legacy: false
以获得 Composition API 的全面支持和更现代的 API。如果需要兼容 Vue 2 的 Options API$t
等方法,可以将其设置为true
,但globalInjection: true
是更推荐的方式来在 Options API 中使用这些方法。globalInjection: true
: 如果设置为true
,则会在 Vue 模板和 Options API 中自动注入$t
,$tc
,$d
,$n
等全局方法。这非常方便,但如果你更倾向于 Composition API 的明确导入,可以将其设置为false
。locale
: 设置当前应用的语言。这里我们初始化为en
。你可以根据用户的浏览器语言、用户设置等来动态设置这个值。fallbackLocale
: 设置备用语言。当当前语言(locale
)中找不到某个键的翻译时,vue-i18n 会尝试在fallbackLocale
中查找。这有助于避免出现空白文本。messages
: 一个对象,其中键是语言代码(如en
,zh
),值是对应语言的翻译消息对象(通常从 JSON 文件导入)。
步骤 4:在组件中使用翻译消息
配置完成后,你就可以在 Vue 组件的模板或 JavaScript 中使用翻译消息了。vue-i18n 提供了几种方式来访问这些消息。
在模板中使用 $t
函数
$t
是一个全局函数,用于在模板中根据当前语言和提供的键获取翻译文本。
src/components/HelloWorld.vue
:
“`vue
{{ $t(‘message.hello’) }}
{{ $t(‘message.welcome’, { name: userName }) }}
“`
解释:
{{ $t('message.hello') }}
: 直接通过键'message.hello'
获取翻译文本。{{ $t('message.welcome', { name: userName }) }}
: 如果翻译文本中包含占位符(如{name}
),可以通过第二个参数传入一个对象来替换这些占位符。键是占位符的名称(不带花括号),值是要插入的数据。
在模板中使用 <i18n>
组件
<i18n>
组件提供了一种更结构化的方式来处理包含 HTML 或需要复杂插值(如将一个 Vue 组件插入到翻译文本中)的翻译文本。
src/components/GreetingMessage.vue
:
“`vue
{{ userName }}
“`
解释:
<i18n path="message.greeting" tag="p">
: 指定使用message.greeting
键的翻译,并将其渲染为一个<p>
标签。<template #name>
: 通过具名插槽 (#name
) 来提供占位符{name}
的内容。插槽的名称与翻译文本中的占位符名称一致。这种方式比使用v-html
更安全,因为它允许你在占位符位置插入 Vue 模板内容,而不是直接注入原始 HTML。
在 JavaScript 中使用 $t
函数
在组件的 JavaScript 代码中,你可以通过 this.$t
来访问翻译函数。
“`vue
“`
解释:
this.$t('key')
: 在 methods, computed, mounted 等选项中使用$t
。this.$t('key', { params })
: 同样支持参数替换。
在 Composition API 中使用:
在 Vue 3 的 Composition API 中,你可以使用 useI18n
函数来获取 i18n 实例,然后使用其中的 t
函数。
“`vue
{{ t(‘message.hello’) }}
{{ t(‘message.welcome’, { name: userName }) }}
“`
解释:
import { useI18n } from 'vue-i18n';
: 从vue-i18n
中导入useI18n
函数。const { t } = useI18n();
: 在setup
函数中调用useI18n()
来获取 i18n 相关的函数和属性,包括t
。- 在模板和
setup
函数中直接使用t()
函数。
第四部分:处理复数、日期和数字
vue-i18n 不仅处理简单的文本翻译,还提供了处理复数形式、日期和数字格式化的功能,这些都是本地化中常见的需求。
处理复数 ($tc
)
不同语言的复数规则不同(例如,英语有单数和复数,而有些语言可能有更多形式)。vue-i18n 使用管道符 (|
) 和特定的语法来定义复数规则。$tc
函数用于根据给定的数量选择合适的复数形式。
src/locales/en.json
:
json
{
"apple": "no apples | one apple | {count} apples",
"banana": "{count} banana | {count} bananas",
"car": "zero cars | one car | {count} cars"
}
src/locales/zh.json
:
json
{
"apple": "{count} 个苹果",
"banana": "{count} 个香蕉",
"car": "{count} 辆车"
}
解释:
- 英语 (
en.json
): 管道符分隔不同的复数形式。0
或no
对应第一部分,1
或one
对应第二部分,其他数量对应第三部分。{count}
是一个特殊的占位符,会自动被传入的数字替换。"no apples"
对应数量 0。"one apple"
对应数量 1。"{count} apples"
对应数量 > 1。
- 中文 (
zh.json
): 中文没有复杂的复数规则,通常只有一种形式,直接包含{count}
占位符即可。
在组件中使用 $tc
:
“`vue
{{ $tc(‘apple’, appleCount, { count: appleCount }) }}
{{ $tc(‘banana’, bananaCount, { count: bananaCount }) }}
{{ $tc(‘car’, carCount, { count: carCount }) }}
“`
解释:
$tc('key', count, { params })
: 第一个参数是翻译键,第二个参数是决定复数形式的数量,第三个参数是用于替换占位符的对象(特别是{count}
)。即使翻译文本不使用{count}
占位符,也建议传入{ count: countValue }
作为第三个参数,以确保行为一致。
处理日期和时间 ($d
)
不同地区有不同的日期和时间格式(例如,月/日/年 vs 日/月/年)。vue-i18n 可以帮助你格式化日期和时间。你需要先定义格式,然后在 $d
函数中使用。
在创建 i18n 实例时配置 dateTimeFormats
:
“`javascript
// src/main.js (部分代码)
const i18n = createI18n({
// …其他配置
dateTimeFormats: {
‘en-US’: {
short: {
year: ‘numeric’, month: ‘short’, day: ‘numeric’
},
long: {
year: ‘numeric’, month: ‘long’, day: ‘numeric’,
weekday: ‘long’, hour: ‘numeric’, minute: ‘numeric’, second: ‘numeric’,
timeZoneName: ‘short’
}
},
‘zh-CN’: {
short: {
year: ‘numeric’, month: ‘short’, day: ‘numeric’
},
long: {
year: ‘numeric’, month: ‘long’, day: ‘numeric’,
weekday: ‘long’, hour: ‘numeric’, minute: ‘numeric’, second: ‘numeric’
}
}
}
});
“`
解释:
dateTimeFormats
: 一个对象,键是区域设置(如en-US
,zh-CN
),值是该区域设置下的格式定义对象。- 格式定义对象:键是格式名称(如
short
,long
),值是符合 Intl.DateTimeFormat 选项的对象。Intl.DateTimeFormat
是 JavaScript 内置的日期格式化 API,vue-i18n 内部使用了它。
在组件中使用 $d
:
“`vue
Short Date (Default Locale): {{ $d(now, ‘short’) }}
Long Date (Default Locale): {{ $d(now, ‘long’) }}
Long Date (zh-CN): {{ $d(now, ‘long’, ‘zh-CN’) }}
“`
解释:
$d(value, formatName, locale?)
: 第一个参数是要格式化的 Date 对象或时间戳,第二个参数是你在dateTimeFormats
中定义的格式名称,第三个可选参数是指定使用的区域设置(如果不提供,则使用当前应用的locale
)。
处理数字和货币 ($n
)
类似于日期,不同地区对数字(小数点、千位分隔符)和货币有不同的格式。vue-i18n 利用 Intl.NumberFormat API 来实现数字和货币格式化。
在创建 i18n 实例时配置 numberFormats
:
“`javascript
// src/main.js (部分代码)
const i18n = createI18n({
// …其他配置
numberFormats: {
‘en-US’: {
currency: {
style: ‘currency’, currency: ‘USD’, minimumFractionDigits: 2
},
percent: {
style: ‘percent’, minimumFractionDigits: 1
}
},
‘zh-CN’: {
currency: {
style: ‘currency’, currency: ‘CNY’, minimumFractionDigits: 2
},
percent: {
style: ‘percent’, minimumFractionDigits: 1
}
}
}
});
“`
解释:
numberFormats
: 一个对象,键是区域设置(如en-US
,zh-CN
),值是该区域设置下的格式定义对象。- 格式定义对象:键是格式名称(如
currency
,percent
),值是符合 Intl.NumberFormat 选项的对象。
在组件中使用 $n
:
“`vue
Price (en-US): {{ $n(price, ‘currency’, ‘en-US’) }}
Price (zh-CN): {{ $n(price, ‘currency’, ‘zh-CN’) }}
Discount (Percent): {{ $n(discount, ‘percent’) }}
“`
解释:
$n(value, formatName, locale?)
: 第一个参数是要格式化的数字,第二个参数是你在numberFormats
中定义的格式名称,第三个可选参数是指定使用的区域设置。
第五部分:动态切换语言
应用通常需要提供一个界面供用户切换语言。实现这一点非常简单,只需要更新 i18n 实例的 locale
属性即可。由于 Vue 的响应式特性,所有依赖于当前 locale
的翻译都会自动更新。
创建一个简单的语言切换器组件:
“`vue
Current Locale: {{ locale }}
“`
解释:
useI18n()
返回的对象中包含了locale
的一个Ref
引用。直接修改locale.value
就可以改变当前语言。- Options API 中通过
this.$i18n.locale
直接访问和修改。 - 将用户的语言偏好存储在
localStorage
中是一个好习惯,这样用户下次访问时,你可以读取这个值并设置为初始locale
。可以在main.js
初始化时读取localStorage
中的值来设置初始locale
。
在 main.js
中初始化时读取用户偏好:
“`javascript
// src/main.js (部分代码)
// 获取用户存储的语言偏好,如果没有则默认为浏览器语言或指定默认语言 ‘en’
const initialLocale = localStorage.getItem(‘user-locale’) || navigator.language.slice(0, 2) || ‘en’;
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: initialLocale, // 使用读取到的语言作为初始语言
fallbackLocale: ‘en’,
messages: {
en,
zh
}
});
// …其他代码
“`
第六部分:异步加载(Lazy Loading)翻译消息
当你的应用支持的语言越来越多时,将所有语言的翻译消息一次性打包到主文件中会导致包体积过大,影响加载速度。异步加载允许你只在需要时(例如,用户切换到某种语言时)加载对应语言的翻译文件。
vue-i18n 支持异步加载,你需要调整初始化和语言切换的逻辑。
步骤 1:组织翻译文件(按需导入)
将语言文件改为单独导入,而不是在 main.js
中全部导入。
src/locales/index.js
(创建一个文件用于组织语言导入和加载逻辑):
“`javascript
import { nextTick } from ‘vue’;
import { createI18n } from ‘vue-i18n’;
// 定义支持的语言和对应的文件路径
const SUPPORTED_LOCALES = {
en: ‘English’,
zh: ‘中文’
// 添加更多语言…
// fr: ‘Français’,
// es: ‘Español’
};
// 创建 i18n 实例 (不加载所有 messages)
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: ‘en’, // 初始语言可以先设一个默认值
fallbackLocale: ‘en’,
messages: {} // 初始 messages 为空对象
});
let loadedLocales = []; // 记录已加载的语言
// 异步加载语言文件的函数
async function loadLocaleMessages(locale) {
// 如果该语言已加载,则直接返回
if (loadedLocales.includes(locale)) {
return nextTick(); // 确保所有 DOM 更新完成后再返回
}
try {
// 动态导入对应的语言文件
const messages = await import(./${locale}.json
);
// 设置该语言的消息到 i18n 实例中
i18n.global.setLocaleMessage(locale, messages.default);
loadedLocales.push(locale); // 标记该语言已加载
return nextTick(); // 确保所有 DOM 更新完成后再返回
} catch (e) {
console.error(Failed to load locale messages for ${locale}:
, e);
// 处理加载失败的情况,例如回退到默认语言或显示错误信息
return Promise.reject(e); // 抛出错误以便调用方处理
}
}
// 设置语言并异步加载
async function setLocale(locale) {
// 如果当前语言已经是目标语言,且已加载,则不进行任何操作
if (i18n.global.locale.value === locale && loadedLocales.includes(locale)) {
return nextTick();
}
await loadLocaleMessages(locale); // 异步加载语言文件
i18n.global.locale.value = locale; // 设置当前语言
// 可选:将语言偏好存储到 localStorage
localStorage.setItem(‘user-locale’, locale);
// 可选:设置文档的 lang 属性,有助于 SEO 和辅助功能
document.querySelector(‘html’).setAttribute(‘lang’, locale);
return nextTick(); // 确保所有 DOM 更新完成后再返回
}
// 初始化加载语言
async function initI18n() {
// 尝试从 localStorage 读取用户偏好
const savedLocale = localStorage.getItem(‘user-locale’);
// 获取浏览器语言(只取前两位)
const browserLocale = navigator.language.slice(0, 2);
// 确定初始语言:用户偏好 > 浏览器语言 > 默认语言
let initialLocale = ‘en’; // 默认语言
if (savedLocale && SUPPORTED_LOCALES[savedLocale]) {
initialLocale = savedLocale;
} else if (browserLocale && SUPPORTED_LOCALES[browserLocale]) {
initialLocale = browserLocale;
}
// 加载并设置初始语言
await setLocale(initialLocale);
}
export { i18n, setLocale, initI18n, SUPPORTED_LOCALES };
“`
步骤 2:在 main.js
中使用异步加载逻辑
修改 main.js
,使用上面定义的 initI18n
函数来初始化和加载语言。
“`javascript
// src/main.js
import { createApp } from ‘vue’;
import App from ‘./App.vue’;
import { i18n, initI18n } from ‘./locales’; // 从新的 locales/index.js 导入
async function bootstrap() {
// 初始化并加载初始语言
await initI18n();
const app = createApp(App);
// 挂载 i18n 实例
app.use(i18n);
// 挂载应用
app.mount(‘#app’);
}
// 启动应用
bootstrap();
“`
步骤 3:修改语言切换器使用 setLocale
函数
修改你的语言切换组件,调用 setLocale
函数来切换语言。
“`vue
Current Locale: {{ locale }}
“`
解释:
import(
./${locale}.json)
: 这是 webpack 或 Vite 支持的动态导入语法,它会将每个语言文件拆分成一个独立的 chunk,只在运行时按需加载。i18n.global.setLocaleMessage(locale, messages.default)
: 使用setLocaleMessage
方法将异步加载的语言消息添加到 i18n 实例中。await setLocale(lang)
: 在切换语言时,调用setLocale
函数,它会先异步加载语言文件(如果尚未加载),然后设置当前语言。由于是异步操作,使用await
确保加载完成后再继续。nextTick()
: 在设置语言消息或切换语言后,使用nextTick
可以确保 DOM 已经更新,这在某些场景下可能需要。
通过这种方式,你的应用初始加载时只会包含默认语言的翻译,其他语言的翻译会在用户第一次切换到该语言时才加载,从而优化了初始加载性能。
第七部分:与其他库的集成
集成 Vue Router
你可能希望在 URL 中包含语言信息,例如 /en/about
或 /zh/about
。你可以结合 Vue Router 和 vue-i18n 来实现。
1. 在路由配置中添加语言参数:
“`javascript
// src/router/index.js
import { createRouter, createWebHistory } from ‘vue-router’;
import HomeView from ‘../views/HomeView.vue’;
import AboutView from ‘../views/AboutView.vue’;
// 导入之前定义的 setLocale 和 SUPPORTED_LOCALES
import { setLocale, SUPPORTED_LOCALES } from ‘@/locales’;
const routes = [
// 重定向根路径到带默认语言的首页
{
path: ‘/’,
redirect: /${Object.keys(SUPPORTED_LOCALES)[0]}/
},
// 动态语言参数路径
{
path: ‘/:locale/’,
// 子路由
children: [
{
path: ”, // /:locale/ 路径,通常是首页
name: ‘home’,
component: HomeView
},
{
path: ‘about’, // /:locale/about 路径
name: ‘about’,
component: AboutView
}
// 添加更多路由…
]
},
// 可选:处理无效的语言路径或 404
{
path: ‘/:pathMatch(.)‘,
name: ‘NotFound’,
component: () => import(‘../views/NotFound.vue’) // 创建一个 404 页面
}
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
// 导航守卫:在每次路由切换前根据 URL 参数设置语言
router.beforeEach(async (to, from, next) => {
const locale = to.params.locale;
// 检查 URL 中的语言参数是否是我们支持的语言
if (locale && SUPPORTED_LOCALES[locale]) {
// 如果是支持的语言,则设置应用语言并继续导航
if (locale !== i18n.global.locale.value) { // 避免重复设置
await setLocale(locale); // 异步加载并设置语言
}
// 更新 localStorage 中的语言偏好 (可选,setLocale 中已包含)
// localStorage.setItem(‘user-locale’, locale);
return next();
} else if (locale) {
// 如果 URL 中有语言参数但不被支持,可以重定向到带默认语言的 404 页面
// 例如,重定向到 /en/404 或 /en/notfound
// 更简单的处理是重定向到带默认语言的首页
console.warn(Unsupported locale in URL: ${locale}
);
// 使用 replace 避免创建历史记录
return next({ path: /${i18n.global.locale.value}/
, replace: true });
} else {
// 如果 URL 中没有语言参数 (例如,用户直接访问 /),则重定向到带默认语言的路径
// 这通常在根路径重定向中处理了,但这里作为补充
// 检查根路径重定向是否已处理,避免死循环
if (to.path === ‘/’) {
// 重定向到带默认语言的根路径
// 注意:如果根路径重定向到 /:locale/,这里可能不需要额外处理
// 为了安全起见,可以检查目标路径是否已经是带语言的路径
if (!routes.some(route => route.path === /:locale/
&& to.path.startsWith(/${i18n.global.locale.value}/
))) {
return next({ path: /${i18n.global.locale.value}${to.fullPath === '/' ? '/' : to.fullPath}
, replace: true });
}
}
// 如果不是根路径且没有语言参数,可能需要根据用户偏好或浏览器语言重定向
// 或者直接加载应用默认语言并继续
// 为了简化,这里假设前面已处理了根路径重定向,其他无语言路径可能不常见或由服务器处理
return next();
}
});
export default router;
“`
解释:
- 我们在路由路径中添加了
:locale
参数。 - 在
router.beforeEach
导航守卫中,我们检查当前路由的params.locale
。 - 如果
locale
参数存在且是支持的语言,我们就调用setLocale
来设置应用语言。 - 如果
locale
参数存在但不支持,或者不存在,我们可以根据需要进行重定向(例如,重定向到默认语言的对应页面或首页)。 - 需要在
main.js
中将 router 挂载到 Vue 应用实例上。
2. 在组件中使用带语言的路由链接:
你可以通过编程导航或 <router-link>
来生成带语言的 URL。
“`vue
“`
解释:
- 通过
params: { locale: locale.value }
将当前语言作为参数传递给路由。
集成状态管理 (Vuex / Pinia)
你可以将当前语言存储在你的状态管理库中,以便在应用中的任何地方访问和修改。
使用 Pinia (Vue 3 推荐):
src/stores/app.js
:
“`javascript
import { defineStore } from ‘pinia’;
import { setLocale, SUPPORTED_LOCALES } from ‘@/locales’; // 导入语言设置函数和支持的语言列表
export const useAppStore = defineStore(‘app’, {
state: () => ({
currentLocale: localStorage.getItem(‘user-locale’) || navigator.language.slice(0, 2) || ‘en’,
}),
actions: {
async changeLocale(locale) {
if (SUPPORTED_LOCALES[locale]) {
await setLocale(locale); // 调用异步加载和设置语言的函数
this.currentLocale = locale; // 更新 Pinia state
} else {
console.warn(Unsupported locale attempted: ${locale}
);
}
}
}
});
“`
在 main.js
中初始化时使用 store 的值:
“`javascript
// src/main.js
import { createApp } from ‘vue’;
import { createPinia } from ‘pinia’;
import App from ‘./App.vue’;
import { i18n } from ‘./locales’; // 导入 i18n 实例,但不再由 locales 初始化语言
import router from ‘./router’; // 导入 router
async function bootstrap() {
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(i18n); // 挂载 i18n 实例
app.use(router); // 挂载 router
// 在 router 的 beforeEach 守卫中根据路由参数设置语言
// Pinia store 的初始化值应与路由守卫中的逻辑保持一致
// 或者,在 Pinia action 中触发路由导航,确保两者同步
// 注意:这里不再直接调用 initI18n,因为路由守卫会根据 URL 设置语言
// Pinia state 中的 initialLocale 只是一个初始值,实际使用的语言由路由守卫决定和更新
app.mount(‘#app’);
}
bootstrap();
“`
修改路由守卫使用 store 的 action:
“`javascript
// src/router/index.js (部分修改)
import { createRouter, createWebHistory } from ‘vue-router’;
// …其他导入
import { useAppStore } from ‘@/stores/app’; // 导入你的 Pinia store
const router = createRouter({ / …配置 / });
router.beforeEach(async (to, from, next) => {
const appStore = useAppStore(); // 获取 store 实例
const locale = to.params.locale;
const supportedLocales = Object.keys(SUPPORTED_LOCALES); // 从 locales 导入或直接使用 store 中的列表
if (locale && supportedLocales.includes(locale)) {
// 如果 URL 语言有效,使用 store action 设置语言 (会同步更新 i18n 和 localStorage)
if (appStore.currentLocale !== locale) { // 避免不必要的设置
await appStore.changeLocale(locale);
}
return next();
} else if (locale) {
// URL 中有语言参数但不支持
console.warn(Unsupported locale in URL: ${locale}
);
// 重定向到带当前语言的首页 (或者默认语言的首页)
return next({ path: /${appStore.currentLocale}/
, replace: true });
} else {
// URL 中没有语言参数
// 重定向到带当前语言 (store 中) 的路径
// 确保重定向目标路径不已经是带语言的,避免死循环
const currentLocale = appStore.currentLocale;
const targetPath = /${currentLocale}${to.fullPath === '/' ? '/' : to.fullPath}
;
// 避免重定向到当前已经在的带语言路径
if (to.path !== targetPath && !to.path.startsWith(`/${currentLocale}/`)) {
console.log(`Redirecting to ${targetPath}`);
return next({ path: targetPath, replace: true });
}
// 如果已经在带语言的路径,或者不需要重定向,则继续
return next();
}
});
export default router;
“`
修改语言切换器使用 store 的 action:
“`vue
Current Locale (from Store): {{ appStore.currentLocale }}
“`
解释:
- Pinia (或 Vuex) store 持有当前语言状态,并且
changeLocale
action 负责调用setLocale
函数来实际加载和设置语言到vue-i18n
实例。 - Vue Router 的导航守卫根据 URL 参数来获取目标语言,并调用 store 的 action 来进行语言切换和状态更新。
- 所有需要知道当前语言的组件都可以从 store 中获取
currentLocale
。
这种集成方式使得语言状态在整个应用中集中管理,并且与路由、vue-i18n 实例保持同步。
第八部分:高级用法与最佳实践
<i18n>
自定义块
在单文件组件 (.vue
文件) 中,你可以使用 <i18n>
自定义块来直接在组件内部定义该组件特有的翻译消息。这有助于将组件的逻辑、模板和翻译消息组织在一起。
“`vue
{{ $t(‘welcome_component’) }}
{{ $t(‘greeting_user’, { name: userName }) }}
{
“en”: {
“welcome_component”: “Welcome from the component!”,
“greeting_user”: “Hello, {name}!”
},
“zh”: {
“welcome_component”: “来自组件的欢迎!”,
“greeting_user”: “你好,{name}!”
}
}
“`
解释:
<i18n>
块中的 JSON 对象会作为该组件的本地消息。- 在组件内部使用
$t
(或 Composition API 中的t
) 时,会优先查找组件的本地消息,如果找不到,则会查找全局消息。 - 这需要配置 Vue Loader 或 Vite 插件来处理
<i18n>
块。对于 Vue CLI 项目,安装vue-loader
通常已经支持。对于 Vite,需要安装@intlify/vite-plugin-vue-i18n
并进行配置。
Vite 配置示例:
vite.config.js
:
“`javascript
import { fileURLToPath, URL } from ‘node:url’;
import { defineConfig } from ‘vite’;
import vue from ‘@vitejs/plugin-vue’;
import vueI18n from ‘@intlify/vite-plugin-vue-i18n’; // 导入插件
import path from ‘node:path’; // 导入 path 模块
export default defineConfig({
plugins: [
vue(),
vueI18n({
// 需要指定国际化资源的目录
include: path.resolve(__dirname, ‘./src/locales/**’),
// 如果你的 .vue 文件使用了
// 这通常不需要额外配置,插件会自动处理 .vue 文件
})
],
resolve: {
alias: {
‘@’: fileURLToPath(new URL(‘./src’, import.meta.url))
}
}
});
“`
翻译键的组织
良好的翻译键组织方式能够提高可维护性:
- 按功能或模块分组: 例如
user.profile.name
,product.details.price
。 - 按组件分组: 可以在全局消息中为每个组件创建一个命名空间,或者使用上面提到的
<i18n>
块。 - 使用有意义的键: 避免使用
key1
,stringA
这样的键。使用能够描述内容或位置的键。 - 扁平 vs 嵌套: 嵌套结构(如 JSON)更清晰,但在某些工具中可能处理更复杂。扁平结构(如
user_profile_name
)简单,但不够直观。选择适合你项目和团队的方式。
处理丢失的翻译
当某个键在当前语言和备用语言中都找不到时,vue-i18n 会发出警告(在开发模式下)并显示原始键。你可以通过 missing
选项来自定义行为,例如返回一个特定的字符串,或者记录到日志服务。
性能考虑
- 异步加载: 这是处理大型应用翻译消息最重要的性能优化手段。
- 避免不必要的
$t
调用: 在计算属性或 Watcher 中频繁调用$t
可能导致性能问题,特别是在复杂的组件树中。如果翻译内容不经常变化,考虑将其结果缓存起来。 - 使用
v-once
: 如果某个部分的翻译文本在整个组件生命周期内都不会改变,可以使用v-once
指令来避免其响应性更新,优化性能。但要注意,一旦使用v-once
,这部分 DOM 将不再响应任何数据变化,包括语言切换。通常不与需要响应语言切换的内容一起使用。
给翻译人员提供上下文
仅仅提供一个 JSON 文件给翻译人员是不足够的。好的 i18n 实践应该为翻译人员提供每个键的使用位置和上下文信息,以便他们能够做出准确的翻译。这通常需要借助专门的 i18n 管理平台或工具来辅助。
第九部分:总结
恭喜你!通过阅读本文,你应该已经全面了解了如何在 Vue 应用中使用 vue-i18n 实现多语言功能。我们从基础安装和配置开始,学习了如何在模板和 JavaScript 中使用 $t
和 <i18n>
组件,掌握了处理复数、日期和数字的技巧。更进一步,我们探讨了如何实现异步加载以优化性能,以及如何将 vue-i18n 与 Vue Router 和状态管理库(如 Pinia)集成。
国际化是一个持续的过程,不仅仅是翻译文本。它涉及到对用户体验、文化习惯和可访问性的深入思考。vue-i18n 为我们提供了强大的工具来应对这些挑战,让你能够更轻松地构建面向全球用户的 Vue.js 应用。
记住最佳实践:抽象文本内容、使用有意义的键、考虑异步加载、为翻译人员提供清晰的上下文。持续学习和实践,你的多语言 Vue 应用将会越来越完善!