当前位置: 首页 > news >正文

Vue 3国际化实战:vue-i18n核心原理与工程化落地

1. 项目概述:为什么 Vue 应用必须认真对待国际化(i18n)

“Implementing i18n in Vue.js Using vue-i18n”——这个标题看似只是技术栈的简单组合,但背后是一条绝大多数 Vue 开发者迟早要踩的深坑。我带过 7 个中大型 Vue 项目,其中 4 个在上线前 2 周才被产品经理紧急叫停:“用户反馈界面全是英文,东南亚市场根本没法用。”结果团队通宵改文案、硬编码语言切换逻辑、手动维护多套 JSON,最后上线延迟 5 天,测试漏掉 3 个语言包里的日期格式错误,导致印尼用户投诉“订单时间显示为 NaN/NaN/NaN”。这不是危言耸听,而是真实发生在我上一个电商 SaaS 项目里的事故。

所谓 i18n(internationalization 的简写,i 和 n 之间有 18 个字母),本质不是“加个下拉框切语言”,而是重构整个应用的文本生命周期管理方式。它要求你把所有可读文字从组件模板、JS 逻辑、甚至路由元信息里彻底剥离,交由统一的语言资源系统调度;它强制你思考日期、数字、货币、复数、性别词形变化等本地化细节;它还倒逼你设计可扩展的插件加载机制、异步语言包按需加载策略、以及浏览器语言自动探测的容错边界。vue-i18n 不是锦上添花的装饰库,而是 Vue 生态中唯一经过生产环境千锤百炼、与 Vue 3 Composition API 深度融合、且被 Vite 官方推荐的国际化解决方案。它不依赖任何外部构建工具,原生支持 SFC<i18n>块、JSON/YAML/JS 多格式资源定义、运行时热更新,甚至能和 Pinia 状态管理无缝协同。如果你正在用 Vue CLI 或 Vite 构建面向全球用户的 Web 应用,跳过 vue-i18n 直接手写 i18n 逻辑,就像用螺丝刀拧紧火箭发动机的螺栓——理论上可行,但没人敢签验收单。

这个项目的核心价值,远不止于“让按钮显示中文或英文”。它解决的是产品出海的第一道合规门槛:当你的应用需要适配德语(名词首字母大写)、阿拉伯语(RTL 布局翻转)、日语(年号纪年+汉字混排)、越南语(声调符号渲染)时,vue-i18n 提供的datetimenumberplural等内置函数,直接调用浏览器 Intl API,省去你手动实现千行格式化逻辑的精力。更重要的是,它把“语言切换”从 UI 交互层下沉为应用状态层——语言变更会触发所有绑定$t的响应式文本自动刷新,无需手动this.$forceUpdate(),也不用担心子组件未监听事件。我见过太多团队用localStorage.setItem('lang', 'zh')+ 全局事件总线的方式做切换,结果在嵌套路由、动态组件、SSR 渲染场景下频繁出现语言不同步、服务端渲染语言与客户端不一致的诡异 bug。而 vue-i18n 的useI18n()Hook,配合createI18n()实例的全局注入,天然规避了这些陷阱。所以,别再把它当成“后期补丁”,从createApp()的第一行代码开始,就该把它当作 Vue 应用的呼吸系统一样设计。

2. 整体架构设计与方案选型深度拆解

2.1 为什么不是自己封装?vue-i18n 的不可替代性在哪?

有人会问:“不就是替换字符串吗?写个const t = (key) => locales[lang][key]就完事了?”我试过。2019 年一个内部管理后台,团队用 3 天写了轻量级 i18n 工具,支持 JSON 配置和简单 key 替换。上线后第 2 周,财务同事反馈:德语版报表里的“€1,234.56”显示成了“€1.234,56”,但导出 Excel 时又变回英文格式,导致审计对不上。我们才发现,数字分组符和小数点在德语区是反过来的。第 3 周,HR 提出需求:法语版员工姓名要按“名+姓”显示,但加拿大法语区要求“姓+名”,而系统里只存了一个fullName字段。我们不得不加字段、改接口、重写所有用户卡片组件。第 4 周,测试发现阿拉伯语菜单栏文字从右往左排,但图标位置没跟着翻转,整个导航栏布局错乱……最后,我们花了 11 天重构成 vue-i18n,所有问题迎刃而解。

vue-i18n 的核心优势,在于它把国际化拆解为语言资源管理格式化能力运行时状态同步工程化支持四个不可分割的维度:

  • 语言资源管理:支持嵌套 JSON 结构(如"user.profile.name": "姓名")、命名空间隔离(user.前缀避免 key 冲突)、缺失 key 的 fallback 机制(missing: (locale, key) => key)、以及关键的mergeLocaleMessage()动态合并能力——当你有公共语言包common.json和模块专属dashboard.json时,无需手动拼接,它自动 merge。

  • 格式化能力:不只是t('key')。它提供d('2023-01-01', { key: 'short' })格式化日期(自动适配en-US1/1/2023ja-JP2023/01/01);n(1234567.89, 'currency')格式化货币(en-US:$1,234,567.89de-DE:1.234.567,89 €);tc('message', 5)处理复数(英语5 messages,俄语需根据数字 5/6/7… 匹配不同词尾)。这些能力底层调用Intl.DateTimeFormatIntl.NumberFormat,浏览器原生支持,零额外包体积。

  • 运行时状态同步:vue-i18n 实例是响应式的。当你调用i18n.locale.value = 'zh-CN',所有使用useI18n()的组件会自动重新计算t()返回值。这得益于 Vue 3 的refcomputed深度集成——t函数本身就是一个computed,其依赖链包含localemessages、当前组件的scope。没有手动watch,没有emit事件,状态流干净得像一条山涧溪水。

  • 工程化支持:Vite 插件@intlify/vite-plugin-vue-i18n支持 SFC<i18n>块提取、JSON 文件自动导入、编译时静态分析(检测缺失 key)、甚至生成类型声明文件(TypeScript 用户狂喜)。Vue CLI 用户则可用@intlify/vue-i18n-loader实现同样效果。这意味着,你的语言包可以像组件一样被模块化管理,src/locales/en-US.jsonsrc/locales/zh-CN.jsonsrc/modules/dashboard/locales/ja-JP.json各司其职,构建时自动合并,开发时 IDE 还能智能提示 key。

对比其他方案:i18next功能强大但 Vue 集成较重,需额外配置i18next-vue插件,且对 Composition API 支持不如原生;vue-i18n-legacy(v8.x)已停止维护;纯IntlAPI 虽标准,但缺乏 Vue 响应式绑定、无便捷的 key 管理、无缺失处理、无工程化支持。所以,vue-i18n 不是“一个选项”,而是 Vue 生态中经过时间验证的事实标准

2.2 Vue 2 vs Vue 3:API 设计哲学的根本差异

很多老项目还在用 Vue 2,而新项目基本是 Vue 3。两者在 i18n 实现上,绝非简单替换Vue.use()app.use()。这是两种设计哲学的碰撞。

Vue 2 的vue-i18n(v8.x)基于 Options API,核心是VueI18n构造函数和全局 mixin:

// main.js (Vue 2) import Vue from 'vue' import VueI18n from 'vue-i18n' import en from './locales/en-US.json' import zh from './locales/zh-CN.json' Vue.use(VueI18n) const i18n = new VueI18n({ locale: 'en-US', messages: { 'en-US': en, 'zh-CN': zh } }) new Vue({ i18n, render: h => h(App) }).$mount('#app')

组件内使用$t('key')$d(date),一切顺滑。但问题在于:全局状态污染i18n.locale是全局变量,多个 Vue 实例(如微前端场景)会互相干扰;无法细粒度控制作用域,你想让某个组件只加载自己的语言包?难;Composition API 支持弱setup()中无法直接用this.$t,得靠getCurrentInstance().proxy.$t,丑陋且不推荐。

Vue 3 的vue-i18n(v9.x+)彻底拥抱 Composition API,核心是createI18n()工厂函数和useI18n()Hook:

// i18n.ts (Vue 3) import { createI18n } from 'vue-i18n' import en from './locales/en-US.json' import zh from './locales/zh-CN.json' export const i18n = createI18n({ legacy: false, // 关键!禁用 Vue 2 兼容模式 locale: 'en-US', messages: { 'en-US': en, 'zh-CN': zh } })
<!-- App.vue --> <script setup lang="ts"> import { useI18n } from 'vue-i18n' const { t, d, n, locale } = useI18n() // t() 是响应式 computed,locale 是 ref </script> <template> <h1>{{ t('app.title') }}</h1> <p>{{ d(new Date()) }}</p> <select v-model="locale.value"> <option value="en-US">English</option> <option value="zh-CN">中文</option> </select> </template>

这种设计带来三大质变:

  1. 实例隔离:每个createI18n()创建独立实例,微前端中App1App2可共存不同 i18n 实例,互不干扰。
  2. 作用域精准useI18n({ useScope: 'local' })可让组件只读取自身defineI18nLocale()定义的语言包,父组件语言变更不影响它——适合嵌入式组件、第三方 SDK。
  3. 类型安全:配合@intlify/vite-plugin-vue-i18n,生成i18n.d.ts类型文件,t('user.login')的 key 会在 TS 编译时报错,如果user.login不存在。这是 Vue 2 时代做梦都不敢想的体验。

所以,如果你的项目是 Vue 3,务必设置legacy: false。别被文档里“兼容 Vue 2”的描述迷惑——那只是给迁移留的后门,新项目用它,等于主动放弃 Composition API 的全部红利。

2.3 语言包组织策略:扁平化 vs 嵌套 vs 模块化

语言包怎么放?是全塞进一个locales.json,还是按模块拆?我见过三种典型失败案例:

  • 案例一:巨型单文件
    locales.json有 1200 行,key 是login.username.placeholderlogin.password.error.requireddashboard.chart.tooltip.sales……开发时找一个登录页文案,得 Ctrl+F 十几次。更糟的是,当运营提需求“把所有‘提交’按钮改成‘确认’”,你得全局搜索submit,结果把form.submit.success也改了,导致成功提示变成“确认成功”。

  • 案例二:过度拆分
    src/locales/login/en.jsonsrc/locales/login/zh.jsonsrc/locales/dashboard/en.json……每个模块都有一套完整语言包。但common.button.cancel这种通用文案,在每个模块里重复定义,一旦修改,得改 8 个文件。CI 流程里漏掉一个,线上就出现英文按钮混在中文页面里。

  • 案例三:无命名空间
    {"username": "用户名", "password": "密码"}。key 太短,username在用户管理、登录、注册、个人资料页都用,结果改一个地方,所有地方都变了。毫无可维护性。

我的实战推荐是:三层嵌套 + 命名空间 + 公共包合并

第一层:顶级命名空间,按功能域划分:

src/ ├── locales/ │ ├── common/ # 全局通用:按钮、提示、状态 │ │ ├── en-US.json │ │ └── zh-CN.json │ ├── user/ # 用户模块:登录、注册、资料 │ │ ├── en-US.json │ │ └── zh-CN.json │ └── dashboard/ # 仪表盘模块 │ ├── en-US.json │ └── zh-CN.json

第二层:JSON 内部嵌套结构,模拟文件路径:

// src/locales/user/en-US.json { "login": { "title": "Sign In", "username": { "label": "Username", "placeholder": "Enter your username" }, "password": { "label": "Password", "placeholder": "Enter your password", "error": { "required": "Password is required", "minLength": "Password must be at least 8 characters" } } } }

这样 key 就是user.login.username.label,清晰表明来源和层级,IDE 搜索user.login一键定位。

第三层:构建时合并。在i18n.ts中,用loadLocaleMessages()动态导入所有包,并用mergeLocaleMessage()合并:

// i18n.ts import { createI18n } from 'vue-i18n' import commonEn from './locales/common/en-US.json' import userEn from './locales/user/en-US.json' import dashboardEn from './locales/dashboard/en-US.json' // 合并所有 en-US 包 const enUS = mergeLocaleMessage(commonEn, userEn, dashboardEn) export const i18n = createI18n({ locale: 'en-US', messages: { 'en-US': enUS } })

好处是:开发时模块自治,构建时全局统一,既避免重复,又保持可维护性。CI 流程只需校验合并后的最终包,不用管中间文件。

3. 核心细节解析与实操要点

3.1 语言检测与初始化:从浏览器到 localStorage 的完整链路

语言初始化不是简单设个locale: 'zh-CN'。真实世界里,用户语言来自四层叠加:浏览器默认 > URL 参数 > localStorage 记忆 > 后备兜底。忽略任一层,都会导致“用户刚切到日语,刷新页面又变回英文”的挫败感。

第一步:浏览器语言探测(最可靠起点)
navigator.language是首选,但它返回的是zh-CNja-JPen-US这样的 BCP 47 标准标签。而你的语言包 key 可能是zhjaen。直接匹配会失败。正确做法是提取主语言码(zh-CNzh),并支持模糊匹配:

// utils/language.ts export function getBrowserLanguage(): string { const lang = navigator.language || navigator.userLanguage // 提取主语言码,如 'zh-CN' -> 'zh', 'pt-BR' -> 'pt' return lang.split('-')[0].toLowerCase() } // 检查是否支持该语言 export function isSupportedLanguage(lang: string): boolean { return ['en', 'zh', 'ja', 'ko', 'de', 'fr'].includes(lang) }

第二步:URL 参数覆盖(运营刚需)
产品经理常要求“发邮件给德国用户,链接带?lang=de-DE,点开直接德语”。这需要路由守卫拦截:

// router/index.ts import { createRouter, NavigationGuardNext } from 'vue-router' import { i18n } from '@/i18n' const router = createRouter({ /* ... */ }) router.beforeEach((to, from, next) => { const lang = to.query.lang as string if (lang && isSupportedLanguage(lang)) { i18n.locale.value = lang // 移除 URL 中的 lang 参数,避免污染后续路由 next({ ...to, query: { ...to.query, lang: undefined } }) } else { next() } })

第三步:localStorage 记忆(用户体验关键)
用户手动切换语言后,必须持久化。但注意:不能只存lang,还要存timestamp,用于过期清理(防止旧数据污染):

// composables/useLanguage.ts import { ref, onMounted } from 'vue' import { i18n } from '@/i18n' export function useLanguage() { const savedLang = localStorage.getItem('preferred-lang') const savedTime = localStorage.getItem('preferred-lang-time') // 24 小时过期 const isExpired = savedTime && Date.now() - parseInt(savedTime) > 24 * 60 * 60 * 1000 if (savedLang && !isExpired) { i18n.locale.value = savedLang } else { // 降级到浏览器语言 const browserLang = getBrowserLanguage() i18n.locale.value = isSupportedLanguage(browserLang) ? browserLang : 'en' } const setLanguage = (lang: string) => { i18n.locale.value = lang localStorage.setItem('preferred-lang', lang) localStorage.setItem('preferred-lang-time', String(Date.now())) } return { locale: i18n.locale, setLanguage } }

第四步:后备兜底(防崩溃底线)
即使以上全失败,也不能让t('key')返回undefinedcreateI18nmissing选项是最后一道保险:

export const i18n = createI18n({ locale: 'en', missing: (locale, key) => { console.warn(`[i18n] Missing translation for key '${key}' in locale '${locale}'`) // 返回 key 本身,或加前缀便于测试识别 return `[${key}]` } })

提示:missing回调在开发环境非常有用,它能帮你快速发现漏翻译的 key。上线前可关闭,或改为返回空字符串。

3.2 组件内文本绑定:从基础t()到高级listnamed插值

{{ $t('key') }}是入门,但真实业务中,90% 的文案需要动态参数。vue-i18n 提供三种插值方式,用错一种,就会埋下线上 bug。

1. 基础插值({}占位符)——最常用,但易出错

// en-US.json { "user.greeting": "Hello, {name}! You have {count} new messages." }
<script setup> const name = ref('Alice') const count = ref(5) </script> <template> <!-- ✅ 正确:传入对象 --> {{ t('user.greeting', { name: name.value, count: count.value }) }} <!-- ❌ 错误:传入数组,会报错 --> <!-- {{ t('user.greeting', [name.value, count.value]) }} --> </template>

风险点:如果namenullundefined,输出会是Hello, undefined!。必须做空值处理:

// 安全写法 const greetingParams = { name: name.value || 'Guest', count: count.value || 0 }

2. 列表插值([]数组)——适合顺序固定、无命名的场景

{ "order.status": "Order #{0} is {1} since {2}." }
<!-- ✅ 正确:传入数组 --> {{ t('order.status', ['12345', 'shipped', '2023-01-01']) }} <!-- 输出:Order #12345 is shipped since 2023-01-01. -->

适用场景:日志、调试信息、机器生成的提示。不推荐用于用户可见文案,因为翻译时顺序可能变化(如德语“since 2023-01-01”在句末,而英语在句中)。

3. 命名插值({name}+named选项)——最灵活、最安全

{ "user.profile": "{name} ({age} years old) joined on {date}." }
<script setup> const user = reactive({ name: 'Bob', age: 28, date: new Date('2022-05-10') }) </script> <template> <!-- ✅ 推荐:命名插值,顺序无关,可读性强 --> {{ t('user.profile', { name: user.name, age: user.age, date: d(user.date, 'short') // 嵌套格式化! }) }} </template>

高级技巧t()支持嵌套调用。上面例子中d(user.date, 'short')返回格式化后的字符串,直接作为date参数传入,t()内部不做二次处理,性能无损。

4. 复数处理(tc())——被严重低估的利器
英语只有单复数,但俄语、阿拉伯语有 6 种复数形式。tc()自动根据数字选择:

{ "message.count": { "one": "You have one message.", "other": "You have {count} messages." } }
<!-- tc() 第二个参数是数字,自动选 one/other --> <span>{{ tc('message.count', count) }}</span> <!-- count=1 → "You have one message." --> <!-- count=5 → "You have 5 messages." -->

注意tc()的 key 必须是对象,不能是字符串。否则会静默失败。

3.3 异步语言包加载:按需加载,减小首屏体积

一个完整应用的语言包可能达 500KB(含 10 种语言)。全量加载,首屏 JS 体积暴增,Lighthouse 评分惨不忍睹。vue-i18n 支持动态导入,只加载用户当前语言:

// i18n.ts import { createI18n } from 'vue-i18n' // 初始化时只加载默认语言 export const i18n = createI18n({ locale: 'en', messages: { en: {} } // 空对象,占位 }) // 动态加载函数 export async function loadLanguageAsync(lang: string) { if (i18n.availableLocales.includes(lang)) { return Promise.resolve() } const messages = await import(`./locales/${lang}.json`) i18n.setLocaleMessage(lang, messages.default) i18n.locale.value = lang }
<!-- LanguageSwitcher.vue --> <script setup> import { useI18n } from 'vue-i18n' import { loadLanguageAsync } from '@/i18n' const { locale } = useI18n() const changeLanguage = async (lang: string) => { try { await loadLanguageAsync(lang) } catch (e) { console.error('Failed to load language:', lang, e) // 加载失败,回退到 English locale.value = 'en' } } </script>

关键优化点

  • 预加载关键语言:在main.ts中,预加载enzh(覆盖 80% 用户),其他语言按需:
    // main.ts import('./i18n').then(({ loadLanguageAsync }) => { loadLanguageAsync('en') loadLanguageAsync('zh') })
  • Webpack/Vite 分包:确保import(./locales/${lang}.json)被识别为动态导入,生成独立 chunk。Vite 默认支持,Webpack 需配置optimization.splitChunks
  • 加载状态反馈:切换语言时显示 loading,避免白屏:
    <button @click="changeLanguage('ja')" :disabled="loading"> {{ loading ? 'Loading...' : '日本語' }} </button>

4. 实操过程与核心环节实现

4.1 从零搭建:Vue 3 + Vite + TypeScript 完整流程

我们以一个标准 Vue 3 + Vite + TypeScript 项目为例,一步步实现 i18n。假设项目已用npm create vite@latest my-app -- --template vue-ts创建。

步骤 1:安装依赖

npm install vue-i18n@^9.0.0 # 开发时类型支持(可选但强烈推荐) npm install -D @intlify/vite-plugin-vue-i18n

步骤 2:创建语言包目录结构

src/ ├── locales/ │ ├── en-US.json │ ├── zh-CN.json │ └── ja-JP.json ├── i18n.ts └── main.ts

步骤 3:编写基础语言包(en-US.json)

{ "app": { "title": "Vue I18n Demo", "description": "A demo of vue-i18n implementation." }, "user": { "login": { "title": "Sign In", "username": "Username", "password": "Password", "submit": "Sign In" } } }

步骤 4:创建i18n.ts(核心配置)

// src/i18n.ts import { createI18n } from 'vue-i18n' import en from './locales/en-US.json' import zh from './locales/zh-CN.json' import ja from './locales/ja-JP.json' // 类型声明,让 TS 知道 messages 结构 declare module 'vue-i18n' { export interface DefineLocaleMessage { app: { title: string; description: string } user: { login: { title: string; username: string; password: string; submit: string } } } } export const i18n = createI18n({ legacy: false, locale: 'en-US', fallbackLocale: 'en-US', messages: { 'en-US': en, 'zh-CN': zh, 'ja-JP': ja } })

步骤 5:在main.ts中挂载

// src/main.ts import { createApp } from 'vue' import { i18n } from './i18n' import App from './App.vue' const app = createApp(App) app.use(i18n) // 关键:挂载 i18n 插件 app.mount('#app')

步骤 6:在组件中使用(App.vue)

<!-- src/App.vue --> <script setup lang="ts"> import { useI18n } from 'vue-i18n' const { t, locale } = useI18n() </script> <template> <div id="app"> <h1>{{ t('app.title') }}</h1> <p>{{ t('app.description') }}</p> <div class="language-switcher"> <button @click="locale.value = 'en-US'">English</button> <button @click="locale.value = 'zh-CN'">中文</button> <button @click="locale.value = 'ja-JP'">日本語</button> </div> <LoginCard /> </div> </template>

步骤 7:为 LoginCard 组件添加 i18n

<!-- src/components/LoginCard.vue --> <script setup lang="ts"> import { useI18n } from 'vue-i18n' const { t } = useI18n() </script> <template> <div class="login-card"> <h2>{{ t('user.login.title') }}</h2> <div class="form-group"> <label>{{ t('user.login.username') }}</label> <input type="text" /> </div> <div class="form-group"> <label>{{ t('user.login.password') }}</label> <input type="password" /> </div> <button>{{ t('user.login.submit') }}</button> </div> </template>

步骤 8:Vite 插件配置(提升开发体验)

// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueI18n from '@intlify/vite-plugin-vue-i18n' export default defineConfig({ plugins: [ vue(), vueI18n({ // 指定语言包目录 include: path.resolve(__dirname, './src/locales/**') }) ] })

启用后,IDE 会为t('key')提供自动补全,且构建时会静态分析所有t()调用,报告缺失的 key。

4.2 SFC<i18n>块:模块化语言包的最佳实践

当组件逻辑复杂、语言文案较多时,把所有文案塞进locales/目录会破坏模块封装。SFC<i18n>块允许你在组件文件内定义专属语言包,且支持多格式:

<!-- src/components/ChartWidget.vue --> <template> <div class="chart-widget"> <h3>{{ t('title') }}</h3> <p>{{ t('description') }}</p> <canvas ref="chartCanvas"></canvas> </div> </template> <script setup lang="ts"> import { useI18n } from 'vue-i18n' const { t } = useI18n() </script> <i18n lang="yaml"> en: title: "Sales Chart" description: "Monthly sales performance" zh: title: "销售图表" description: "月度销售业绩" ja: title: "売上チャート" description: "月次売上実績" </i18n>

优势

  • 完全解耦ChartWidget.vue移动到新项目,语言包随行,无需额外配置。
  • 开发友好:文案和组件逻辑在同一文件,修改文案时无需切窗口。
  • 按需加载:Vite 插件会自动将<i18n>块提取为独立模块,仅当组件被引入时才加载对应语言。

注意事项

  • <i18n>块中的 key 是局部作用域,不会污染全局 messages。t('title')只查找本组件的<i18n>块,找不到才向上查找全局。
  • 支持jsonyamljson5格式,推荐yaml,缩进清晰,注释友好。
  • 如果同时存在全局包和<i18n>块,优先使用<i18n>块。

4.3 与 Vue Router 深度集成:路由级语言切换

URL 是语言状态的重要载体。理想情况下,/dashboard是英文,/zh/dashboard是中文,/ja/dashboard是日文。这需要路由和 i18n 协同。

步骤 1:定义带语言前缀的路由

// router/index.ts import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/:lang(en|zh|ja)?', component: () => import('@/layouts/DefaultLayout.vue'), children: [ { path: '', name: 'Home', component: () => import('@/views/Home.vue') }, { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/Dashboard.vue') } ] } ] const router = createRouter({ history: createWebHistory(), routes })

步骤 2:路由守卫同步语言

// router/index.ts import { i18n } from '@/i18n' router.beforeEach((to, from, next) => { const lang = to.params.lang as string || 'en' if (i18n.locale.value !== lang) { i18n.locale.value = lang } next() })

步骤 3:生成带语言的路由链接

<!-- 导航栏组件 --> <script setup> import { useRoute, useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' const route = useRoute() const router = useRouter() const { locale } = useI18n() const switchLanguage = (lang: string) => { // 保留当前路由,只改 lang 参数 router.push({ ...route, params: { ...route.params, lang } }) } </script> <template> <nav> <router-link :to="{ name: 'Home', params: { lang: 'en' }
http://www.jsqmd.com/news/1068695/

相关文章:

  • Weave Scope容器监控:实时拓扑可视化与交互式诊断实战指南
  • Postman自动化CSRF Token认证:环境变量与脚本实战指南
  • Java FutureTask 深度解析:状态机、超时控制与线程中断原理
  • 零样本学习在软件工程情感分析中的创新应用
  • 跨越LLM产品评估可操作性差距:从数据到行动的系统方法
  • DMXAPI+Qwen3.7-Max智能体实战:从PLC文档化看AI编程落地
  • Prisma + PostgreSQL 生产级落地指南:从连接配置到向量搜索
  • RTA广告技术解析:从实时API原理到电商金融实战部署
  • GLM-5.1代码能力跃迁:从SWE-Bench Pro登顶看大模型工程化落地
  • Qwen3.5+llama.cpp实测:216G显存跑262K上下文与120 tokens/s推理
  • SRC漏洞挖掘入门指南:从零到一掌握白帽子实战技能
  • FEC以太网控制器:缓冲区描述符机制与嵌入式网络驱动开发实战
  • Claude Opus 4.8 effort机制深度解析:成本与性能的临界点优化
  • 混元3.0编程能力跃迁:MoE架构与262K上下文如何重塑开发者工作流
  • Qwen3.5 Block在llama.cpp中的映射与优化原理
  • MC56F8455x SIM模块深度解析:复位、时钟与功耗管理实战指南
  • 飞书CLI实战指南:办公自动化从命令行开始
  • Spring 5:响应式架构与Kotlin原生支持的工程实践分水岭
  • DigitalOcean负载均衡器五大高频踩坑场景与配置避坑指南
  • OpenCV.js前端视觉开发:浏览器端图像处理实战指南
  • CentOS 8 安装 Node.js 三套可靠方案与避坑指南
  • 多Agent编排三要素:并行调度、视角隔离与运行时防护
  • DeepSeek-V4-Pro国产AI算力闭环实战解析
  • 数字取证实战:5大技巧高效破解加密电子证据
  • MySQL Query Profiling:精准定位SQL慢因的听诊器
  • React Props 封装机制:单向数据流与显式接口设计原理
  • Android应用反调试机制深度解析与Frida实战绕过方案
  • Gemini 3.1 Flash 计费逻辑深度解析:Token+推理强度双维定价
  • 从脚本小子到安全猎人:40个核心姿势构建体系化漏洞挖掘思维
  • Python中__str__和__repr__方法的核心区别与工程实践