文章目录
-
- 一图看懂全流程
- [01 · 国际化是什么:先想清楚再动手](#01 · 国际化是什么:先想清楚再动手)
- [02 · 方案选型:为什么选择 vue-i18n](#02 · 方案选型:为什么选择 vue-i18n)
-
- [2.1 自研方案看似简单,长期维护成本很高](#2.1 自研方案看似简单,长期维护成本很高)
- [2.2 主流国际化库横向对比](#2.2 主流国际化库横向对比)
- [2.3 选型重点关注四个维度](#2.3 选型重点关注四个维度)
- [03 · 版本与安装:版本匹配是高频踩坑点](#03 · 版本与安装:版本匹配是高频踩坑点)
- [04 · 目录结构与 key 命名规范](#04 · 目录结构与 key 命名规范)
- [05 · 编写语言包](#05 · 编写语言包)
- [06 · 创建 i18n 实例(逐项拆解配置)](#06 · 创建 i18n 实例(逐项拆解配置))
- [07 · 挂载到应用入口](#07 · 挂载到应用入口)
- [08 · 在组件中使用(t / useI18n)](#08 · 在组件中使用(t / useI18n))
-
- [8.1 组合式API(推荐,TS类型友好)](#8.1 组合式API(推荐,TS类型友好))
- [8.2 独立语言切换组件](#8.2 独立语言切换组件)
- [09 · 非组件环境使用(Pinia / 工具函数)](#09 · 非组件环境使用(Pinia / 工具函数))
- [10 · 语言切换与持久化完整闭环](#10 · 语言切换与持久化完整闭环)
-
- [10.1 首次访问语言识别优先级](#10.1 首次访问语言识别优先级)
- [10.2 html lang属性的意义](#10.2 html lang属性的意义)
- [11 · 进阶:消息语法全掌握(插值 / 复数 / 链接文案)](#11 · 进阶:消息语法全掌握(插值 / 复数 / 链接文案))
-
- [11.1 命名插值](#11.1 命名插值)
- [11.2 列表插值与字面量转义](#11.2 列表插值与字面量转义)
- [11.3 复数规则(Pluralization)](#11.3 复数规则(Pluralization))
- [11.4 消息链接与文本修饰符](#11.4 消息链接与文本修饰符)
- [11.5 HTML渲染与XSS安全提醒](#11.5 HTML渲染与XSS安全提醒)
- [12 · 进阶:日期、数字、货币本地化](#12 · 进阶:日期、数字、货币本地化)
-
- [12.1 日期时间格式化 datetimeFormats + d](#12.1 日期时间格式化 datetimeFormats + d)
- [12.2 数字与货币 numberFormats + n](#12.2 数字与货币 numberFormats + n)
- [13 · 进阶:语言包懒加载](#13 · 进阶:语言包懒加载)
- [14 · 进阶:路由、接口、SEO联动](#14 · 进阶:路由、接口、SEO联动)
-
- [14.1 路由标题本地化](#14.1 路由标题本地化)
- [14.2 和后端接口语言协商](#14.2 和后端接口语言协商)
- [14.3 时区处理](#14.3 时区处理)
- [15 · RTL 布局适配(阿拉伯语等从右向左语言)](#15 · RTL 布局适配(阿拉伯语等从右向左语言))
- [16 · 工程化配套与常见坑清单](#16 · 工程化配套与常见坑清单)
-
- [16.1 VSCode 插件 i18n-ally](#16.1 VSCode 插件 i18n-ally)
- [16.2 踩坑清单](#16.2 踩坑清单)
- [17 · 工程化自动化辅助方案](#17 · 工程化自动化辅助方案)
- [18 · 总结](#18 · 总结)
- [19. SpringBoot 多语言方案汇总(内置语言包 + 外置磁盘语言包 + 异常增强版)](#19. SpringBoot 多语言方案汇总(内置语言包 + 外置磁盘语言包 + 异常增强版))
-
-
- [一、classpath 内置语言包加载(打包进Jar)](#一、classpath 内置语言包加载(打包进Jar))
- 二、外置磁盘语言包加载(不打包进Jar,异常处理增强版)
-
- 外置语言包调用示例
- [application.yml 配置](#application.yml 配置)
- 外置磁盘语言包文件示例
- 方案说明
- 参考资料
-
标签:Vue 3 · vue-i18n · 国际化 · TypeScript
出海项目开发中,前端多语言是绕不开的需求。很多开发者最开始简单认为国际化只是把页面中文替换成英文,真正落地才会发现这是一套完整的工程问题。本文基于 Vue3 + vue-i18n + TS,从基础安装配置、语言包管理,到组件内外调用、语言持久化,再到插值复数、日期数字格式化、语言包懒加载、路由接口联动、RTL布局适配完整落地,附带可直接运行的业务代码,同时补充工程化配套方案与踩坑总结。
一图看懂全流程
流程图逻辑:安装依赖 → 目录与Key规范 → 编写语言包 → 创建i18n实例 → 挂载应用 → 组件内使用 → 语言切换持久化,再延伸多项进阶能力
- 安装依赖:vue-i18n + unplugin-vue-i18n
- 设计目录与key命名规范:src/locales目录,按业务模块分层管理
- 编写多语言包:zh-CN / en-US,保证不同语言对象键结构保持一致
- 初始化i18n实例:使用createI18n完成基础配置
- 挂载至Vue应用实例
- 组件中使用翻译能力:$t模板语法、useI18n组合式API
- 语言切换与持久化:localStorage存储语言偏好,同步html lang属性
进阶扩展能力:语言包懒加载、日期/数字/货币本地化格式化、路由标题与页面SEO适配、后端接口多语言协商、RTL从右向左布局适配
flowchart LR
A[安装依赖] --> B[制定目录与Key命名规范]
B --> C[准备语言包]
C --> D[创建i18n实例并配置]
D --> E[挂载Vue应用]
E --> F[组件中调用翻译API]
F --> G[语言切换+本地持久化]
G --> H{扩展能力}
H --> H1[语言包懒加载]
H --> H2[日期/数字/货币本地化]
H --> H3[路由&SEO多语言适配]
H --> H4[后端接口多语言对接]
H --> H5[RTL双向布局支持]
01 · 国际化是什么:先想清楚再动手
项目要面向海外用户时,很多团队第一反应就是做翻译。但国际化不等于单纯文本翻译,翻译仅仅是整个体系里很小的一环。
国际化英文全称Internationalization,缩写i18n,指代用一套代码,兼容不同国家、地区、文化习惯,不需要为每个地区维护独立代码分支。本地化l10n,则是基于这套国际化框架,针对特定地区做内容适配,比如文本翻译、调整时间展示格式、切换货币符号。
一套完整前端国际化方案,需要覆盖下面这些场景:
- 页面文案翻译与动态切换,这是最基础功能;
- 日期、时间、数字、货币本地化展示规则。例如国内习惯
2026/03/15,美式格式03/15/2026; - 文本排版方向适配,阿拉伯语、希伯来语这类语言是从右向左RTL布局;
- 语言包文件管理,多语种场景需要考虑按需加载,控制首屏资源体积;
- 用户语言偏好持久存储,页面刷新之后语言选择不会丢失;
- 和后端接口联动:接口错误提示、枚举值、时间信息都支持多语言;
- 路由元信息、页面标题、SEO相关meta标签的多语言处理。
小型内部工具,只需要实现基础文案切换即可。但跨境SaaS、电商出海项目,上面所有能力基本都需要实现。本文示例基于Vue3 + vue-i18n + TypeScript,整套设计思路,也可以迁移到React、原生JS项目参考。
02 · 方案选型:为什么选择 vue-i18n
2.1 自研方案看似简单,长期维护成本很高
项目初期,部分团队会选择自研简单多语言方案:全局对象存放key-value文本,封装简易t()函数。短期开发速度快,但随着项目迭代,各类问题会陆续暴露:
- 语言文件不断增多,缺少统一规范,key命名混乱,难以维护;
- 切换语言无法自动更新视图,需要手动触发页面更新甚至刷新页面;
- 缺少插值、复数、上下文语法支持,只能手动拼接字符串,不同语种语序差异会造成文案错乱;
- 日期数字格式化,需要自己封装Intl接口,兼容各种浏览器边界场景;
- 语言包懒加载、分包策略需要自行实现;
- HTML转义、XSS安全防护需要额外开发。
自研方案并非不可行,但本质是重复造轮子。vue-i18n、i18next这类成熟库,已经沉淀大量线上踩坑经验,直接使用更加稳定高效。
2.2 主流国际化库横向对比
| 库 | 适用生态 | 核心优势 | 注意点 |
|---|---|---|---|
| vue-i18n | Vue 2 / Vue 3 | 和Vue响应式深度整合,切换语言自动更新视图 | 仅适配Vue生态 |
| i18next | 原生JS / React / Vue / Node | 生态庞大,插件丰富,前后端可复用 | Vue项目需要额外桥接层 |
| react-i18next | React | 基于i18next,Hooks开发体验好 | 依赖i18next核心 |
| FormatJS / react-intl | React | ICU消息语法,格式化能力强大 | 上手门槛偏高 |
新建Vue3项目,优先选择vue-i18n。属于Vue生态配套方案,坑少,和响应式系统天然结合。
2.3 选型重点关注四个维度
- 响应式更新:切换语言后,页面所有翻译文案自动更新,不需要刷新页面,vue-i18n原生支持;自研方案要手动做依赖收集,极易遗漏。
- 懒加载能力:语种较多场景,全部语言包打包进主包会增大首屏JS体积,支持按语言动态加载是必备能力。
- 插值语法支持:不同语种语序差别巨大,英文、俄语还有复杂复数规则,需要框架原生支持ICU消息语法。
- 团队上手成本,优先选择团队成员熟悉的库,相比功能更强大但学习成本高的方案,稳定落地更重要。
03 · 版本与安装:版本匹配是高频踩坑点
vue-i18n版本和Vue版本严格绑定,版本装错是项目初期最常见的问题。
| Vue版本 | vue-i18n版本 | 说明 |
|---|---|---|
| Vue 2 | vue-i18n@8.x | 采用VueI18n类,new实例的旧Options写法 |
| Vue 3 | vue-i18n@9+ | 9/10版本已经停止维护,使用createI18n组合式API |
| Vue 3(推荐) | vue-i18n@11稳定版 | 本文所有示例均基于该版本 |
包管理器安装命令,pnpm、npm、yarn任选其一
# pnpm推荐,磁盘占用更小,安装速度更快
pnpm install vue-i18n
# npm
npm install vue-i18n --save
# yarn
yarn add vue-i18n
# bun
bun add vue-i18n
Vite项目推荐配套安装@intlify/unplugin-vue-i18n开发依赖。构建阶段提前预编译语言包,运行时无需解析JSON,还支持SFC单文件组件内<i18n>块,提升运行性能。
# npm
npm install @intlify/unplugin-vue-i18n -D
# pnpm
pnpm add @intlify/unplugin-vue-i18n -D
# yarn
yarn add @intlify/unplugin-vue-i18n -D
# bun
bun add @intlify/unplugin-vue-i18n -D
04 · 目录结构与 key 命名规范
提前定义标准化目录,避免后续文件散乱,增加维护成本。推荐项目目录结构:
src/
├── locales/ # 多语言包根目录
│ ├── zh-CN.ts # 中文语言包
│ ├── en-US.ts # 英文语言包
│ └── modules/ # 可选:按业务模块拆分语言包
│ ├── common.ts
│ ├── login.ts
│ └── order.ts
├── plugins/
│ └── i18n.ts # i18n实例初始化配置
├── utils/
│ └── i18nUtils.ts # 语言切换、非组件翻译工具函数
└── main.ts # 项目入口,挂载i18n
Key命名规范
- 使用点号分层,按照业务模块划分,例如
order.status.pending; - key语义化命名,禁止
msg1、text2这类无意义标识; - key统一使用英文,文案内容放在value字段,方便编辑器提示,缺失key的回退逻辑也更友好。
05 · 编写语言包
语言包可以用TS、JS或者JSON文件。建议按照页面/组件模块拆分,避免键名冲突,不同语言文件,对象层级结构必须完全一致,只修改value文案。
// src/locales/zh-CN.ts 中文语言包,采用模块.key分层结构
export default {
header: {
index: '首页',
about: '关于我们',
contact: '联系我们'
},
home: {
welcome: '欢迎访问我的网站',
desc: '这是一个 Vue i18n 国际化示例',
greeting: '你好,{name}!'
},
common: {
confirm: '确认',
cancel: '取消',
save: '保存',
networkError: '网络异常,请稍后重试'
}
} as const
// src/locales/en-US.ts 英文语言包,保持和中文完全相同的key结构
export default {
header: {
index: 'Home',
about: 'About Us',
contact: 'Contact Us'
},
home: {
welcome: 'Welcome to My Website',
desc: 'This is a Vue i18n Demo',
greeting: 'Hello, {name}!'
},
common: {
confirm: 'Confirm',
cancel: 'Cancel',
save: 'Save',
networkError: 'Network error, please try again later'
}
} as const
TS项目增加
as const,配合unplugin-vue-i18n类型声明,编辑器输入t('home.')时自动提示可用key,减少手写key拼写错误。
06 · 创建 i18n 实例(逐项拆解配置)
在src/plugins/i18n.ts初始化i18n实例,同时处理默认语言识别、本地缓存读取等逻辑。
// src/plugins/i18n.ts
import { createI18n } from 'vue-i18n'
import zhCN from '@/locales/zh-CN'
import enUS from '@/locales/en-US'
/** 获取默认语言:优先级 本地存储 > 浏览器语言 */
function getDefaultLanguage(): 'zh-CN' | 'en-US' {
const saved = localStorage.getItem('app_language')
if (saved === 'zh-CN' || saved === 'en-US') return saved
const browserLang = navigator.language.toLowerCase()
if (browserLang.includes('zh')) return 'zh-CN'
return 'en-US'
}
const i18n = createI18n({
legacy: false, // 开启组合式API模式,核心配置
globalInjection: true, // 全局注入$t,模板内直接调用
locale: getDefaultLanguage(),
fallbackLocale: 'zh-CN', // key缺失时,回退使用中文文案
messages: {
'zh-CN': zhCN,
'en-US': enUS
},
availableLocales: ['zh-CN', 'en-US'], // 项目支持的语言列表
silentTranslationWarn: true, // 关闭翻译缺失警告,开发阶段可改为false排查问题
silentFallbackWarn: true // 关闭回退文案警告
})
export default i18n
配置项说明
| 配置项 | 作用 |
|---|---|
legacy: false |
启用组合式API模式,组件内使用useI18n,locale为响应式变量,切换语言自动更新页面 |
globalInjection: true |
全局注入t / d / $n,模板无需额外导入即可使用 |
locale |
当前生效语言,响应式变量,修改后自动触发页面重新渲染 |
fallbackLocale |
兜底语言,当前语种缺少对应key,使用该语种文案,避免页面直接展示原始key字符串 |
messages |
语言包映射,key为语言标识,value是语言包对象 |
availableLocales |
项目支持语种列表,用于语言选择器遍历、语种合法性校验 |
silentTranslationWarn |
控制缺失key警告,开发环境建议关闭,快速定位缺失文案 |
Vite项目,在vite.config.ts注册插件:
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
export default defineConfig({
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
plugins: [
VueI18nPlugin({
runtimeOnly: true,
compositionOnly: true,
include: [fileURLToPath(new URL('./src/locales/**', import.meta.url))]
})
]
})
07 · 挂载到应用入口
main.ts导入i18n实例并注册,全局生效。
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import i18n from '@/plugins/i18n'
const app = createApp(App)
app.use(i18n)
app.mount('#app')
08 · 在组件中使用($t / useI18n)
Vue3推荐组合式API写法useI18n,模板也可以直接使用全局注入的$t。
8.1 组合式API(推荐,TS类型友好)
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { ref, computed } from 'vue'
// locale响应式当前语言;t翻译函数;availableLocales支持语种列表
const { locale, t, availableLocales } = useI18n()
// 使用computed包裹,切换语言自动重新计算菜单文本
const navMenu = computed(() => [
{ label: t('header.index'), path: '/' },
{ label: t('header.about'), path: '/about' }
])
const handleSwitchLang = () => {
locale.value = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
}
</script>
<template>
<h1>{{ $t('home.welcome') }}</h1>
<p>{{ $t('home.greeting', { name: '张三' }) }}</p>
<nav>
<a v-for="item in navMenu" :key="item.path" :href="item.path">
{{ item.label }}
</a>
</nav>
<button @click="handleSwitchLang">
{{ locale === 'zh-CN' ? 'Switch to English' : '切换为中文' }}
</button>
</template>
模板
$t('home.welcome')和script内部t('home.welcome')能力一致;$t依赖globalInjection: true,业务组件内优先使用useI18n,作用域隔离,TS提示更好。
8.2 独立语言切换组件
<!-- components/LanguageSwitcher.vue -->
<template>
<select :value="locale" @change="changeLanguage($event)">
<option v-for="lang in availableLocales" :key="lang" :value="lang">
{{ lang === 'zh-CN' ? '中文' : 'English' }}
</option>
</select>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { switchLanguage } from '@/utils/i18nUtils'
const { locale, availableLocales } = useI18n()
const changeLanguage = (e: Event) => {
const lang = (e.target as HTMLSelectElement).value
switchLanguage(lang as 'zh-CN' | 'en-US')
}
</script>
09 · 非组件环境使用(Pinia / 工具函数)
工具类、Pinia状态库、路由守卫等场景,不能调用useI18n,需要直接使用i18n.global。封装工具函数统一处理,减少重复异常捕获代码。
// src/utils/i18nUtils.ts
import i18n from '@/plugins/i18n'
/** 切换语言:更新实例 + 本地持久化 */
export const switchLanguage = (newLocale: 'zh-CN' | 'en-US') => {
try {
i18n.global.locale.value = newLocale
localStorage.setItem('app_language', newLocale)
document.documentElement.lang = newLocale // 同步html lang,利于屏幕阅读器、浏览器翻译与SEO
} catch (error) {
console.error('语言切换失败:', error)
}
}
/** 非组件环境翻译函数,异常返回原始key,避免页面空白 */
export const t = (key: string, values?: Record<string, unknown>): string => {
try {
return i18n.global.t(key, values)
} catch (error) {
console.error(`翻译失败(键:${key}):`, error)
return key
}
}
Pinia Store中调用示例:
// src/store/menu.ts
import { defineStore } from 'pinia'
import { t } from '@/utils/i18nUtils'
export const useMenuStore = defineStore('menu', {
state: () => ({
menuList: [
{ label: t('header.index'), path: '/', icon: 'home' },
{ label: t('header.about'), path: '/about', icon: 'info' }
]
}),
actions: {
// 语言切换后,重新加载菜单文案
refreshMenu() {
this.menuList = [
{ label: t('header.index'), path: '/', icon: 'home' },
{ label: t('header.about'), path: '/about', icon: 'info' }
]
}
}
})
重要提醒:useI18n仅能在setup组件内部调用,非组件环境直接调用会抛出异常。记住规则:组件内部useI18n,组件外部i18n.global。
10 · 语言切换与持久化完整闭环
一套完整的语言切换逻辑包含三件事:更新i18n实例locale、写入localStorage、修改html根标签lang属性。上文switchLanguage已经实现,补充两个工程细节。
10.1 首次访问语言识别优先级
推荐顺序:用户本地存储 > 浏览器语言 > 项目默认语言。读取navigator.language,兼容zh-CN、zh-TW、ja等不同浏览器返回值。
function getInitialLocale(): string {
const saved = localStorage.getItem('app_language')
if (saved) return saved
const browserLang = navigator.language
if (browserLang.startsWith('zh')) return 'zh-CN'
if (browserLang.startsWith('ja')) return 'ja-JP'
return 'en-US'
}
10.2 html lang属性的意义
document.documentElement.lang = newLocale不仅仅是规范,会影响浏览器翻译插件、屏幕阅读器朗读,对页面SEO也有正向作用,很多教程会忽略这个配置。
修改locale是响应式变量,所有使用$t、t()的地方自动更新视图,这也是vue-i18n最核心优势,自研方案很难做到。
11 · 进阶:消息语法全掌握(插值 / 复数 / 链接文案)
vue-i18n消息语法能力远不止简单字符串替换,下面介绍ICU消息格式常用能力。
11.1 命名插值
语言包内{name}占位符,调用时传入对象参数。
// 语言包
common: {
welcomeUser: '欢迎您,{name}!',
orderDetail: '您的订单 {orderNo} 已提交,预计 {date} 送达。'
}
// 使用
t('common.welcomeUser', { name: '张三' })
t('common.orderDetail', { orderNo: 'NO123456', date: '2026-03-15' })
建议使用语义命名占位符,不要用{0}{1}下标形式,翻译人员查看语言包无法理解参数含义。
11.2 列表插值与字面量转义
// 列表插值:{0} {1} 对应数组下标
// message: { hello: '{0} world' }
t('message.hello', ['hello'])
// 字面量插值,转义特殊符号如@ {}
// address: "{account}{'@'}{domain}"
t('address', { account: 'foo', domain: 'domain.com' })
11.3 复数规则(Pluralization)
中文量词变化少,英文、俄语存在复杂复数形式。vue-i18n使用竖线|分割不同复数分支,内置{count}隐式参数。
// en-US语言包
apple: 'no apples | one apple | {count} apples',
car: 'car | cars'
// 使用
t('car', 1)
t('car', 2)
t('apple', 0)
t('apple', 1)
t('apple', { count: 10 })
斯拉夫语种(俄语、乌克兰语)复数规则更复杂,可以自定义复数处理函数。
function russianRule(choice: number, choicesLength: number) {
if (choice === 0) return 0
const teen = choice > 10 && choice < 20
const endsWithOne = choice % 10 === 1
if (!teen && endsWithOne) return 1
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) return 2
return choicesLength < 4 ? 2 : 3
}
const i18n = createI18n({
locale: 'ru',
pluralizationRules: { ru: russianRule },
messages: {
ru: { car: '0 машин | {n} машина | {n} машины | {n} машин' }
}
})
11.4 消息链接与文本修饰符
部分文案和已有文案完全一致,使用@:key引用,搭配大小写修饰符@.upper / @.lower / @.capitalize。
// en-US
message: {
homeAddress: 'Home address',
missingHomeAddress: 'Please provide @.lower:message.homeAddress',
linked: '@:message.homeAddress'
}
// t('message.missingHomeAddress') 输出: Please provide home address
11.5 HTML渲染与XSS安全提醒
重点风险:v-html渲染翻译文本容易引发XSS攻击。
v-html="$t(...)"只能渲染静态可信文案,绝对不能处理用户输入内容 。Vue提供escapeParameter全局开启参数转义,防止恶意代码注入。优先使用<i18n-t>组件做文本插值,避免v-html。
const i18n = createI18n({
locale: 'en',
escapeParameter: true, // 组合式API全局开启参数转义
messages: {
en: { welcome: 'Welcome <strong>{name}</strong>!' }
}
})
// 用户输入内容会自动转义,不会执行恶意脚本
12 · 进阶:日期、数字、货币本地化
除文本外,时间、金额、数字展示也要适配地区习惯。后端统一返回ISO时间或者时间戳,前端借助Intl能力展示,vue-i18n封装$d日期函数、$n数字货币函数,切换语言自动联动格式。
12.1 日期时间格式化 datetimeFormats + $d
配置遵循ECMA-402 Intl.DateTimeFormat标准。
const datetimeFormats = {
'en-US': {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric' }
},
'ja-JP': {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric', hour12: true }
}
}
const i18n = createI18n({ datetimeFormats })
模板使用$d渲染日期
<p>{{ $d(new Date(), 'short') }}</p>
<p>{{ $d(new Date(), 'long', 'ja-JP') }}</p>
注意配置项名称
datetimeFormats,拼写错误会造成格式化静默失效。
12.2 数字与货币 numberFormats + $n
const numberFormats = {
'en-US': {
currency: { style: 'currency', currency: 'USD', notation: 'standard' },
decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', useGrouping: false }
},
'ja-JP': {
currency: { style: 'currency', currency: 'JPY', useGrouping: true, currencyDisplay: 'symbol' }
}
}
const i18n = createI18n({ numberFormats })
模板调用$n
<p>{{ $n(10000, 'currency') }}</p>
<p>{{ $n(10000, 'currency', 'ja-JP') }}</p>
<p>{{ $n(987654321, 'currency', { notation: 'compact' }) }}</p>
<p>{{ $n(0.99123, 'percent') }}</p>
也可以直接使用原生Intl API,功能一致,但不会跟随i18n的locale自动切换。
new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }).format(date)
new Intl.NumberFormat('de-DE').format(1234567.89)
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(99.9)
13 · 进阶:语言包懒加载
语种数量少,直接全量打包没问题。如果项目支持十几种语言,全部打包进主包,首屏体积会明显增大。推荐动态import按需加载语言包。
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
export const SUPPORT_LOCALES = ['en', 'ja', 'zh-CN']
export function setupI18n(options = { locale: 'en' }) {
const i18n = createI18n(options)
setI18nLanguage(i18n, options.locale)
return i18n
}
export function setI18nLanguage(i18n, locale) {
if (i18n.mode === 'legacy') {
i18n.global.locale = locale
} else {
i18n.global.locale.value = locale
}
document.querySelector('html').setAttribute('lang', locale)
// 可在此统一设置请求头,用于后端接口语言协商
// axios.defaults.headers.common['Accept-Language'] = locale
}
export async function loadLocaleMessages(i18n, locale) {
// 动态导入语言包,按需加载
const messages = await import(`./locales/${locale}.json`)
i18n.global.setLocaleMessage(locale, messages.default)
return nextTick()
}
结合vue-router路由守卫使用,进入页面前先加载对应语种包。
router.beforeEach(async (to, from, next) => {
const locale = to.params.locale || i18n.global.locale.value
if (!SUPPORT_LOCALES.includes(locale)) return next('/en')
if (!i18n.global.availableLocales.includes(locale)) {
await loadLocaleMessages(i18n, locale)
}
setI18nLanguage(i18n, locale)
next()
})
语言切换设计为异步动作,await加载完成再赋值locale,避免页面短暂闪烁显示原始key。
14 · 进阶:路由、接口、SEO联动
14.1 路由标题本地化
路由meta只存储key,不在路由写死翻译文本,路由守卫内动态翻译。
const routes = [
{ path: '/login', name: 'Login', component: () => import('@/views/Login.vue'),
meta: { titleKey: 'menu.login' } },
{ path: '/dashboard', name: 'Dashboard', component: () => import('@/views/Dashboard.vue'),
meta: { titleKey: 'menu.dashboard' } }
]
router.afterEach((to) => {
const titleKey = to.meta?.titleKey
if (titleKey) document.title = i18n.global.t(titleKey)
})
切换语言后document.title不会自动更新,需要在switchLanguage函数内,重新翻译当前路由标题。
14.2 和后端接口语言协商
后端返回错误信息、枚举值同样需要多语言。通用方案:请求头携带Accept-Language,或者请求参数传入locale。Axios统一在请求拦截器添加。
import axios from 'axios'
import i18n from '@/plugins/i18n'
const service = axios.create({ baseURL: '/api' })
service.interceptors.request.use((config) => {
config.headers['Accept-Language'] = i18n.global.locale.value
return config
})
后端返回JSON语言包动态接入方式
很多业务场景会把多语言文案存储在服务端数据库,前端不再全部打包本地语言文件,由后端接口直接返回完整JSON结构,前端加载后注入i18n实例。适合后台可在线编辑文案、多租户平台场景。
后端返回JSON示例:
{
"header": {
"index": "首页",
"about": "关于我们"
},
"common": {
"confirm": "确认",
"cancel": "取消"
}
}
封装接口拉取并注入语言包函数:
// src/utils/i18nUtils.ts
import axios from 'axios'
import i18n from '@/plugins/i18n'
/**
* 从后端接口拉取对应语种JSON语言包,注入i18n实例
* @param locale 语言标识 zh-CN / en-US
*/
export async function fetchLocaleFromBackend(locale: string) {
const res = await axios.get(`/api/locale/${locale}`)
const remoteMessages = res.data
// 将后端返回的json对象设置为当前语种的翻译消息
i18n.global.setLocaleMessage(locale, remoteMessages)
// 更新可用语种列表
if (!i18n.global.availableLocales.includes(locale)) {
i18n.global.availableLocales.push(locale)
}
}
使用示例,切换语言时优先请求后端语言资源:
export const switchLanguage = async (newLocale: string) => {
try {
// 判断该语种是否已经加载,未加载则请求后端
if (!i18n.global.availableLocales.includes(newLocale)) {
await fetchLocaleFromBackend(newLocale)
}
i18n.global.locale.value = newLocale
localStorage.setItem('app_language', newLocale)
document.documentElement.lang = newLocale
} catch (error) {
console.error('语言切换失败:', error)
}
}
方案特点:文案在后端管理,运营可直接修改文字,不需要前端打包发布;缺点是每次新增语种需要发起网络请求,可搭配本地缓存,把拉取到的语言JSON存入localStorage,减少重复请求。
后端返回枚举code,前端匹配翻译key
接口列表、下拉选项、状态标签,后端一般返回固定编码,前端维护编码和i18n key的映射表,再调用t()渲染。
后端返回数据示例:
{
"orderStatus": "pending"
}
前端维护映射:
const statusKeyMap: Record<string, string> = {
pending: 'order.status.pending',
paid: 'order.status.paid',
cancelled: 'order.status.cancelled'
}
// 渲染
const getStatusText = (code: string) => {
const key = statusKeyMap[code]
return key ? i18n.global.t(key) : code
}
后端直接返回带占位符的模板文案
后端返回带{xxx}占位符的字符串,直接传入t函数进行插值渲染,适用于动态业务文案。
// backendMsg 后端返回:"订单 {orderNo} 已成功提交"
const backendMsg = res.data.msg
const text = i18n.global.t(backendMsg, { orderNo: 'NO123456' })
注意安全风险:后端返回文案同样要警惕XSS,禁止直接v-html渲染。
14.3 时区处理
跨境项目时区是常见坑,推荐规范:后端统一存储UTC时间,前端读取浏览器本地时区渲染展示。
function formatDateTime(isoString, locale) {
const date = new Date(isoString)
return new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
}).format(date)
}
15 · RTL 布局适配(阿拉伯语等从右向左语言)
面向中东市场,RTL从右向左排版是必须实现的能力。并不是简单设置dir="rtl"就可以完成,flex方向、图标方向、边距对齐都需要适配。
落地建议:
- html根节点动态设置dir属性
document.documentElement.dir = 'rtl'; - CSS优先使用逻辑属性:
margin-inline-start替代margin-left,padding-inline-end替代padding-right; - 箭头类图标做水平镜像
transform: scaleX(-1); - 字体选型需要适配阿拉伯文字体。
项目初期不考虑RTL,后期改造成本极高。如果产品需要覆盖中东地区,在国际化阶段就同步规划布局方案。
16 · 工程化配套与常见坑清单
16.1 VSCode 插件 i18n-ally
VS Code安装i18n-ally插件,在.vscode/settings.json配置语言目录。编辑器内预览翻译、跳转key、自动补全,大幅提升开发效率。
{
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.enabledParsers": ["ts", "json"],
"i18n-ally.displayLanguage": "zh"
}
16.2 踩坑清单
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 版本不匹配 | Vue3项目引入vue-i18n@8,报错,$t无法使用 | Vue3使用9+版本,推荐v11;Vue2使用8.x |
| 缺少legacy:false | useI18n警告,locale非响应式 | createI18n显式设置legacy: false |
| key缺失 | 页面直接展示key字符串 | 配置fallbackLocale,开发环境打开翻译警告 |
| 非组件调用useI18n | Pinia、工具函数直接报错 | 使用i18n.global.t,封装公共工具函数 |
| 切换语言页面标题不变 | document.title保留旧语种文本 | 切换语言时,重新翻译当前路由标题 |
| 首包体积过大 | 多语种全部打包进主js | 动态import懒加载语言包,路由守卫预加载 |
| 字符串拼接文案 | 订单+编号+已提交,翻译语序错乱 |
使用完整句子+占位符,禁止字符串拼接 |
| v-html渲染用户内容 | 存在XSS安全漏洞 | 优先<i18n-t>组件,开启escapeParameter转义 |
| 复数、语序硬编码 | 英文单复数、俄语文案展示错误 | 使用竖线复数语法,自定义pluralRules |
| 后端语言包加载异常 | 切换语种页面全部不显示文案 | 增加接口异常捕获,加载失败自动回退到默认语种 |
17 · 工程化自动化辅助方案
多语言项目存在大量重复性工作,例如提取硬编码文案、批量翻译语言包、检查key一致性。可以借助自动化工具辅助开发,减少手动工作量。
- 新项目初始化:一键完成vue-i18n环境搭建,生成目录、语言包、基础配置,完成语言持久化切换能力。
- 存量项目改造:扫描代码内硬编码中文,提取文案,生成key清单,替换原有字符串为翻译函数,重点把拼接字符串重构为带占位符完整句子。
- 批量翻译语言包:基于中文语言包,生成英文、日文等语种文件,保持key结构不变,占位符原样保留,人工复核不确定翻译内容。
- 代码检查:扫描代码遗留硬编码文本,检查多语言包key是否对齐,排查错误写法,输出问题清单。
- 封装语言选择组件:支持动态判断语言包是否加载,未加载时异步请求语言资源,同步更新页面语言、本地存储和html属性。
工程小技巧:
- 将项目i18n规范写入项目文档,约定目录、key命名、禁止字符串拼接;
- 先确认方案目录结构,再编写业务代码,分模块完成验收;
- 批量生成语言包后,单独列出不确定翻译条目,人工校对;
- 可编写简单脚本在CI阶段校验不同语种key一致性,缺失key直接拦截合并。
18 · 总结
整套国际化体系,不只是替换页面文字,包含语言包规范、响应式语言切换、插值复数语法、日期数字格式化、语言包懒加载、路由接口SEO联动、RTL布局适配。按照完整流程落地,搭配工程化工具辅助,可以稳定完成出海项目多语言需求。
最终验收清单
- ✅ 版本匹配:Vue3项目使用vue-i18n v11
- ✅ 基础配置:legacy: false + globalInjection: true
- ✅ 兜底文案:配置fallbackLocale,防止页面展示原始key
- ✅ 持久化:切换语言写入localStorage,更新html lang属性
- ✅ 本地化展示:日期数字货币使用 ( d / ) (d/) (d/)n,不硬编码格式
- ✅ 性能优化:多语种场景启用语言包懒加载
- ✅ 调用规范:非组件环境使用i18n.global,不直接调用useI18n
- ✅ 安全防护:v-html仅渲染可信静态文案,开启参数转义
- ✅ 后端对接:支持远程JSON语言包、枚举code映射、后端模板文案插值
19. SpringBoot 多语言方案汇总(内置语言包 + 外置磁盘语言包 + 异常增强版)
一、classpath 内置语言包加载(打包进Jar)
语言包放置在 resources/i18n/,打包时一起打入jar。
java
import org.springframework.context.MessageSource;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
/**
* 多语言语言包加载工具
* 语言包文件放置位置:resources/i18n/
* 文件名示例:
* messages_zh_CN.properties
* messages_en_US.properties
*/
@Component
public class I18nMessageLoader {
private final MessageSource messageSource;
public I18nMessageLoader() {
ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
// 语言包基础名称,不带后缀
source.setBasename("classpath:i18n/messages");
// 编码
source.setDefaultEncoding(StandardCharsets.UTF_8.name());
// 缓存刷新时间(单位秒,0=每次都重新加载,生产建议加大)
source.setCacheSeconds(60);
// 默认语言
source.setDefaultLocale(Locale.CHINA);
messageSource = source;
}
/**
* 获取翻译文本
* @param code 语言key
* @param args 占位参数
* @param locale 地区语言
* @return 翻译后的字符串
*/
public String getMsg(String code, Object[] args, Locale locale) {
return messageSource.getMessage(code, args, code, locale);
}
/**
* 简化重载:不带参数
*/
public String getMsg(String code, Locale locale) {
return getMsg(code, null, locale);
}
/**
* 简化重载:不带参数,使用默认语言
*/
public String getMsg(String code) {
return getMsg(code, null, Locale.CHINA);
}
}
内置语言包调用示例
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Locale;
@RestController
public class DemoController {
@Autowired
private I18nMessageLoader i18nMessageLoader;
@GetMapping("/test-i18n")
public String testI18n(@RequestParam String lang) {
Locale locale = "en".equals(lang) ? Locale.US : Locale.CHINA;
// 获取语言包内 key=welcome 的文本
return i18nMessageLoader.getMsg("welcome", locale);
}
}
内置语言包文件示例
resources/i18n/messages_zh_CN.properties
properties
welcome=欢迎使用系统
msg.hello=你好,{0}
resources/i18n/messages_en_US.properties
properties
welcome=Welcome to the system
msg.hello=Hello, {0}
二、外置磁盘语言包加载(不打包进Jar,异常处理增强版)
语言包放在服务器磁盘独立目录,修改语言文件无需重新打包发布;增加目录校验、异常捕获、日志输出,异常时返回key兜底,避免接口报错。
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Locale;
/**
* 外置磁盘语言包加载器
* 语言包目录:由配置 i18n.external-base-path 指定
* 文件命名:messages_zh_CN.properties / messages_en_US.properties
* 降级策略:读取异常/文件缺失时,直接返回key,不抛出500异常
*/
@Component
public class ExternalI18nMessageLoader {
private static final Logger log = LoggerFactory.getLogger(ExternalI18nMessageLoader.class);
private final MessageSource messageSource;
private final String baseDir;
public ExternalI18nMessageLoader(@Value("${i18n.external-base-path:/opt/app/i18n/}") String baseDir) {
this.baseDir = baseDir;
ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
String basename = "file:" + baseDir + "messages";
source.setBasename(basename);
source.setDefaultEncoding(StandardCharsets.UTF_8.name());
// 缓存刷新秒数,生产建议30~60,0为每次读取
source.setCacheSeconds(60);
source.setDefaultLocale(Locale.CHINA);
source.setFallbackToSystemLocale(false);
// 启动时校验目录是否存在
try {
Path dirPath = Paths.get(baseDir);
if (!Files.exists(dirPath)) {
log.warn("[I18N] 外部语言包目录不存在,path={}", baseDir);
} else if (!Files.isReadable(dirPath)) {
log.error("[I18N] 外部语言包目录无读取权限,path={}", baseDir);
} else {
log.info("[I18N] 外部语言包目录校验成功,path={}", baseDir);
}
} catch (Exception e) {
log.error("[I18N] 校验外部语言包目录发生异常", e);
}
this.messageSource = source;
}
/**
* 获取翻译文本
* @param code 语言key
* @param args 占位参数
* @param locale 语言地区
* @return 翻译文本;异常时返回key原值
*/
public String getMsg(String code, Object[] args, Locale locale) {
try {
return messageSource.getMessage(code, args, code, locale);
} catch (Exception e) {
log.warn("[I18N] 获取语言文本失败,key={}, locale={}", code, locale, e);
return code;
}
}
/**
* 重载:无占位参数
*/
public String getMsg(String code, Locale locale) {
return getMsg(code, null, locale);
}
/**
* 重载:默认中文
*/
public String getMsg(String code) {
return getMsg(code, null, Locale.CHINA);
}
}
外置语言包调用示例
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Locale;
@RestController
public class I18nDemoController {
private static final Logger log = LoggerFactory.getLogger(I18nDemoController.class);
@Autowired
private ExternalI18nMessageLoader externalI18nMessageLoader;
@GetMapping("/test-external-i18n")
public String test(@RequestParam(defaultValue = "zh") String lang) {
Locale locale = "en".equals(lang) ? Locale.US : Locale.CHINA;
String text = externalI18nMessageLoader.getMsg("welcome", locale);
log.info("[I18N-TEST] result={}", text);
return text;
}
}
application.yml 配置
yaml
i18n:
external-base-path: /opt/app/i18n/
外置磁盘语言包文件示例
/opt/app/i18n/messages_zh_CN.properties
properties
welcome=欢迎使用系统
msg.hello=你好,{0}
msg.tip=外部磁盘语言包,修改无需重启服务
/opt/app/i18n/messages_en_US.properties
properties
welcome=Welcome to the system
msg.hello=Hello, {0}
msg.tip=External language file, no restart required
方案说明
-
内置语言包方案
- 语言文件打包进jar,适合语言版本固定,很少修改的场景
- ReloadableResourceBundleMessageSource 支持热刷新;不需要热更新可替换为 ResourceBundleMessageSource
- 前端可通过请求头 Accept-Language 传递语言标识,后端使用 LocaleContextHolder 获取当前语言,不用手动传lang参数
-
外置磁盘语言包方案
- Linux路径示例:
/opt/app/i18n/;Windows路径示例:D:/app/i18n/,路径分隔符使用/ - 应用进程需要该目录读权限,否则读取失败
- 热更新依靠 cacheSeconds 配置定时检查文件修改时间;生产环境不建议设置为0,会频繁IO,推荐30~60秒
- 语言文件完全独立于Jar包,运维可直接修改语言文本,无需重新打包发布
- 启动阶段校验目录状态,读取翻译时捕获异常,打印日志,兜底返回key,防止接口异常
- Linux路径示例:
-
依赖说明
- Spring Context,SpringBoot项目自带,无需额外引入依赖
- 代码中不使用Lombok,兼容所有SpringBoot项目
如果你需要,我可以再追加一小节:统一Locale解析工具类(从Http请求头自动解析Locale),直接整合进这份文档。