Vue i18n 国际化全套适配实战:一步一步从零到工程化落地

文章目录

    • 一图看懂全流程
    • [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联动)
    • [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 多语言方案汇总(内置语言包 + 外置磁盘语言包 + 异常增强版))

标签:Vue 3 · vue-i18n · 国际化 · TypeScript

出海项目开发中,前端多语言是绕不开的需求。很多开发者最开始简单认为国际化只是把页面中文替换成英文,真正落地才会发现这是一套完整的工程问题。本文基于 Vue3 + vue-i18n + TS,从基础安装配置、语言包管理,到组件内外调用、语言持久化,再到插值复数、日期数字格式化、语言包懒加载、路由接口联动、RTL布局适配完整落地,附带可直接运行的业务代码,同时补充工程化配套方案与踩坑总结。

一图看懂全流程

流程图逻辑:安装依赖 → 目录与Key规范 → 编写语言包 → 创建i18n实例 → 挂载应用 → 组件内使用 → 语言切换持久化,再延伸多项进阶能力

  1. 安装依赖:vue-i18n + unplugin-vue-i18n
  2. 设计目录与key命名规范:src/locales目录,按业务模块分层管理
  3. 编写多语言包:zh-CN / en-US,保证不同语言对象键结构保持一致
  4. 初始化i18n实例:使用createI18n完成基础配置
  5. 挂载至Vue应用实例
  6. 组件中使用翻译能力:$t模板语法、useI18n组合式API
  7. 语言切换与持久化: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,则是基于这套国际化框架,针对特定地区做内容适配,比如文本翻译、调整时间展示格式、切换货币符号。

一套完整前端国际化方案,需要覆盖下面这些场景:

  1. 页面文案翻译与动态切换,这是最基础功能;
  2. 日期、时间、数字、货币本地化展示规则。例如国内习惯 2026/03/15,美式格式 03/15/2026
  3. 文本排版方向适配,阿拉伯语、希伯来语这类语言是从右向左RTL布局;
  4. 语言包文件管理,多语种场景需要考虑按需加载,控制首屏资源体积;
  5. 用户语言偏好持久存储,页面刷新之后语言选择不会丢失;
  6. 和后端接口联动:接口错误提示、枚举值、时间信息都支持多语言;
  7. 路由元信息、页面标题、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 选型重点关注四个维度

  1. 响应式更新:切换语言后,页面所有翻译文案自动更新,不需要刷新页面,vue-i18n原生支持;自研方案要手动做依赖收集,极易遗漏。
  2. 懒加载能力:语种较多场景,全部语言包打包进主包会增大首屏JS体积,支持按语言动态加载是必备能力。
  3. 插值语法支持:不同语种语序差别巨大,英文、俄语还有复杂复数规则,需要框架原生支持ICU消息语法。
  4. 团队上手成本,优先选择团队成员熟悉的库,相比功能更强大但学习成本高的方案,稳定落地更重要。

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语义化命名,禁止msg1text2这类无意义标识;
  • 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方向、图标方向、边距对齐都需要适配。

落地建议:

  1. html根节点动态设置dir属性 document.documentElement.dir = 'rtl'
  2. CSS优先使用逻辑属性:margin-inline-start替代margin-leftpadding-inline-end替代padding-right
  3. 箭头类图标做水平镜像 transform: scaleX(-1)
  4. 字体选型需要适配阿拉伯文字体。

项目初期不考虑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一致性。可以借助自动化工具辅助开发,减少手动工作量。

  1. 新项目初始化:一键完成vue-i18n环境搭建,生成目录、语言包、基础配置,完成语言持久化切换能力。
  2. 存量项目改造:扫描代码内硬编码中文,提取文案,生成key清单,替换原有字符串为翻译函数,重点把拼接字符串重构为带占位符完整句子。
  3. 批量翻译语言包:基于中文语言包,生成英文、日文等语种文件,保持key结构不变,占位符原样保留,人工复核不确定翻译内容。
  4. 代码检查:扫描代码遗留硬编码文本,检查多语言包key是否对齐,排查错误写法,输出问题清单。
  5. 封装语言选择组件:支持动态判断语言包是否加载,未加载时异步请求语言资源,同步更新页面语言、本地存储和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
方案说明
  1. 内置语言包方案

    • 语言文件打包进jar,适合语言版本固定,很少修改的场景
    • ReloadableResourceBundleMessageSource 支持热刷新;不需要热更新可替换为 ResourceBundleMessageSource
    • 前端可通过请求头 Accept-Language 传递语言标识,后端使用 LocaleContextHolder 获取当前语言,不用手动传lang参数
  2. 外置磁盘语言包方案

    • Linux路径示例:/opt/app/i18n/;Windows路径示例:D:/app/i18n/,路径分隔符使用 /
    • 应用进程需要该目录读权限,否则读取失败
    • 热更新依靠 cacheSeconds 配置定时检查文件修改时间;生产环境不建议设置为0,会频繁IO,推荐30~60秒
    • 语言文件完全独立于Jar包,运维可直接修改语言文本,无需重新打包发布
    • 启动阶段校验目录状态,读取翻译时捕获异常,打印日志,兜底返回key,防止接口异常
  3. 依赖说明

    • Spring Context,SpringBoot项目自带,无需额外引入依赖
    • 代码中不使用Lombok,兼容所有SpringBoot项目

如果你需要,我可以再追加一小节:统一Locale解析工具类(从Http请求头自动解析Locale),直接整合进这份文档。

参考资料

Vue I18n 官方文档


相关推荐
志尊宝7 小时前
Vue3 零基础每日笔记(035):什么是 Composables——mixin 之死与逻辑复用新方案
笔记·vue·html·前端开发·软件开发
志尊宝9 小时前
Vue3 零基础每日笔记(030):动态组件与 keep-alive——切换组件与页面缓存
笔记·vue·html·前端开发·软件开发
志尊宝10 小时前
Vue3 零基础每日笔记(029):插槽 slot 三连——默认、具名、作用域一次讲透
前端·javascript·vue.js·vue·前端开发
尾善爱看海20 小时前
Vue 面试收官篇:SSR、性能优化落地、30 道高频面试题精讲(附标准答案)
前端·javascript·vue.js·面试·vue
caoerzhong1 天前
JeeWMS 开源仓库管理系统 GPL-3.0 合规指南:Java WMS 二次开发前必须弄清的授权边界
java·开发语言·开源·vue
caoerzhong2 天前
JeeWMS 开源仓库管理系统权限与域验证解析:Java WMS 如何把数据权限管到仓、货主与人
java·python·开源·vue
caoerzhong2 天前
JeeWMS 开源仓库管理系统多租户架构解析:一套 Java WMS 如何同时服务多个货主与多个仓库
java·架构·开源·vue
ynchyong2 天前
VUE 中 不能将类型“R[]”分配给类型“UnwrapRefSimple<R>[]”
typescript·vue·ts·unwraprefsimple
Leaderxin2 天前
还在用 Electron?6 种跨平台桌面方案横评:Rust + Vue 把安装包从 224MB 干到 4.7MB
rust·vue·跨平台·tauri·桌面应用