随着应用出海和多元化用户需求增长,国际化(i18n)已成为现代移动应用的标配。uni-app 作为跨平台框架,天然需要一套能同时运行在 H5、小程序和 App 上的多语言方案。本文将手把手带你集成 vue-i18n,通过一个完整的商城应用案例,从语言包设计、动态切换、状态持久化到各平台兼容处理,构建可维护的国际化体系。
uni-app 国际化方案选型分析
在 uni-app 中实现国际化,有几种可选路径:
- 纯手工方案:在组件中通过计算属性根据当前语言返回不同文本。适合非常小的项目,但维护成本高,无法复用。
- uni-app 内置的
uni.getLocale():可以获取系统语言,但没有提供翻译函数和模板语法支持,需自行封装。 - vue-i18n:Vue 生态中最成熟的国际化库,提供
$t()翻译函数、指令、复数处理、日期格式化等完整功能,与 Vue 3 组合式 API 完美配合。
本文将采用 vue-i18n v9+(适配 Vue 3),并针对 uni-app 的多平台特性进行适配封装。这个方案已在 H5、微信小程序和 App 端验证通过。
vue-i18n 安装与初始化配置
首先安装 vue-i18n(确保使用 v9 及以上版本以支持 Vue 3):
npm install vue-i18n@9
在项目根目录创建 locale/ 文件夹,结构如下:
locale/
index.js # 主入口,创建 i18n 实例
messages/
zh-CN.js # 中文语言包
en.js # 英文语言包
utils.js # 工具函数(获取系统语言、持久化等)
先编写中文和英文语言包。以商城场景为例,locale/messages/zh-CN.js:
export default {
app: {
name: '优选商城',
slogan: '精选好物,品质生活'
},
tabbar: {
home: '首页',
category: '分类',
cart: '购物车',
profile: '我的'
},
product: {
addToCart: '加入购物车',
buyNow: '立即购买',
price: '价格',
stock: '库存'
},
user: {
login: '登录',
register: '注册',
settings: '设置',
language: '语言'
},
common: {
confirm: '确定',
cancel: '取消',
loading: '加载中...'
}
}
locale/messages/en.js:
export default {
app: {
name: 'BestMart',
slogan: 'Quality Goods, Better Life'
},
tabbar: {
home: 'Home',
category: 'Category',
cart: 'Cart',
profile: 'Me'
},
product: {
addToCart: 'Add to Cart',
buyNow: 'Buy Now',
price: 'Price',
stock: 'Stock'
},
user: {
login: 'Login',
register: 'Register',
settings: 'Settings',
language: 'Language'
},
common: {
confirm: 'Confirm',
cancel: 'Cancel',
loading: 'Loading...'
}
}
然后在 locale/index.js 中创建 i18n 实例:
// locale/index.js
import { createI18n } from 'vue-i18n'
import zhCN from './messages/zh-CN'
import en from './messages/en'
// 获取初始语言
function getInitialLocale() {
// 1. 优先读取用户手动选择的语言
const stored = uni.getStorageSync('app_language')
if (stored && (stored === 'zh-CN' || stored === 'en')) {
return stored
}
// 2. 其次根据系统语言推断
const systemLocale = uni.getLocale()
if (systemLocale && systemLocale.startsWith('en')) {
return 'en'
}
// 3. 默认中文
return 'zh-CN'
}
const i18n = createI18n({
legacy: false, // 使用组合式 API 模式
globalInjection: true, // 全局注入 $t
locale: getInitialLocale(),
fallbackLocale: 'zh-CN',
messages: {
'zh-CN': zhCN,
'en': en
}
})
export default i18n
注意 legacy: false 是关键配置,它让 vue-i18n 工作在 Vue 3 的组合式 API 模式下,支持 useI18n() 钩子。
最后在 main.js 中注册:
// main.js
import App from './App.vue'
import { createSSRApp } from 'vue'
import i18n from './locale/index'
export function createApp() {
const app = createSSRApp(App)
app.use(i18n)
return { app }
}
语言包模块化设计与热加载
当应用规模增长,单一语言文件会变得难以维护。推荐按业务模块拆分语言包,然后在入口文件中合并。修改 locale/messages/zh-CN.js 为聚合文件:
// locale/messages/zh-CN.js
import home from './modules/zh-CN/home'
import category from './modules/zh-CN/category'
import cart from './modules/zh-CN/cart'
import user from './modules/zh-CN/user'
import common from './modules/zh-CN/common'
export default {
...home,
...category,
...cart,
...user,
...common
}
同理处理英文包。这种模块化结构便于团队协作,每个业务模块独立维护自己的翻译。
对于更大型的项目,还可以实现按需加载语言包。例如,当用户切换语言时才异步加载对应的语言文件,减少初始包体积。以下是简单的异步加载实现:
// locale/asyncLoader.js
export async function loadLocaleMessages(locale) {
switch (locale) {
case 'zh-CN':
return (await import('./messages/zh-CN.js')).default
case 'en':
return (await import('./messages/en.js')).default
default:
return (await import('./messages/zh-CN.js')).default
}
}
// 在切换语言时调用
async function setLocaleAsync(locale) {
const messages = await loadLocaleMessages(locale)
i18n.global.setLocaleMessage(locale, messages)
i18n.global.locale.value = locale
uni.setStorageSync('app_language', locale)
}
这种方式对小程序尤其友好,因为小程序对包体积有严格限制。不过对于大多数中小型应用,直接全量加载语言包也完全没问题。
动态语言切换与全局响应
vue-i18n 的核心优势在于切换语言后,所有使用 $t() 的地方会自动更新,无需手动刷新页面。我们需要一个语言切换的入口页面,并在切换后持久化用户选择。
创建语言切换页面 pages/settings/language.vue:
<template>
<view class="language-page">
<view
v-for="lang in languages"
:key="lang.code"
class="language-item"
@click="changeLanguage(lang.code)"
>
<text>{{ lang.nativeName }}</text>
<text class="lang-sub">{{ lang.name }}</text>
<checkbox :checked="currentLocale === lang.code" />
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
const currentLocale = ref(locale.value)
const languages = [
{ code: 'zh-CN', name: 'Chinese', nativeName: '中文' },
{ code: 'en', name: 'English', nativeName: 'English' }
]
function changeLanguage(code) {
locale.value = code
currentLocale.value = code
uni.setStorageSync('app_language', code)
uni.showToast({
title: code === 'zh-CN' ? '已切换为中文' : 'Switched to English',
icon: 'success'
})
}
</script>
切换完成后,所有页面的翻译文本会立即更新。这是因为 locale.value 是响应式的,vue-i18n 内部会重新计算所有 $t() 调用的返回值。
在页面的 navigationBarTitleText 等配置中也希望使用多语言,可以在 pages.json 中使用运行时标题设置:
// 在页面 onShow 或 setup 中
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
uni.setNavigationBarTitle({
title: t('tabbar.home')
})
语言偏好持久化与初始化加载
我们已经在前面的 getInitialLocale() 函数中处理了启动时的语言恢复。但还有一个细节:当用户首次打开应用时,如何优雅地跟随系统语言?
扩展 locale/utils.js:
// locale/utils.js
export function getSystemLocale() {
const locale = uni.getLocale()
// uni.getLocale() 在不同平台返回值可能不同
// 小程序返回 'zh_CN' 格式,需转换
if (locale) {
const normalized = locale.replace('_', '-')
if (normalized.startsWith('zh')) return 'zh-CN'
if (normalized.startsWith('en')) return 'en'
}
return 'zh-CN'
}
export function setUserLanguage(locale) {
uni.setStorageSync('app_language', locale)
}
export function getUserLanguage() {
return uni.getStorageSync('app_language') || null
}
另外,uni-app 的 tabBar 和 navigationBar 中的文本也需要国际化。由于这些配置在 pages.json 中是静态的,推荐做法是使用 自定义导航栏 和 自定义 tabBar,这样就能在组件中使用 $t() 了。
如果必须使用原生导航栏,可以在 App.vue 的 onLaunch 中动态设置 tabBar 文本:
// App.vue
import i18n from '@/locale/index'
onLaunch(() => {
const t = i18n.global.t
uni.setTabBarItem({
index: 0,
text: t('tabbar.home')
})
uni.setTabBarItem({
index: 1,
text: t('tabbar.category')
})
// ... 其他 tab
})
模板与脚本中的多语言使用技巧
vue-i18n 提供了多种使用方式:
模板中使用 $t()
<template>
<view>
<text>{{ $t('app.name') }}</text>
<text>{{ $t('product.price') }}: ¥99.00</text>
</view>
</template>
脚本中使用 useI18n()
<script setup>
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
function showConfirm() {
uni.showModal({
title: t('common.confirm'),
content: '确定要删除该商品吗?',
confirmText: t('common.confirm'),
cancelText: t('common.cancel')
})
}
</script>
带参数的翻译(插值)
语言包中可以使用 {name} 占位符:
// 语言包
export default {
product: {
stockRemaining: '剩余 {count} 件'
}
}
// 使用
<text>{{ $t('product.stockRemaining', { count: 5 }) }}</text>
// 输出: 剩余 5 件
复数处理
vue-i18n 支持复数规则,但中文没有复数变化,英文可以使用管道符:
// 英语语言包
messages: {
cart: {
itemCount: 'no items | one item | {count} items'
}
}
// 使用
$t('cart.itemCount', { count: 0 }) // "no items"
$t('cart.itemCount', { count: 1 }) // "one item"
$t('cart.itemCount', { count: 5 }) // "5 items"
小程序端兼容处理与常见坑点
微信小程序在支持 vue-i18n 时有一些特殊之处:
- 存储限制:单个 key 最大 1MB,语言包通常不会超出,但要注意不要在语言包中存储大段 HTML 文本。
- uni.getLocale() 返回值:小程序返回的是
'zh_CN'(下划线格式),需要转换为'zh-CN'(横线格式)才能与 vue-i18n 的语言代码匹配。 - 异步加载:小程序不支持动态
require,但支持import()动态导入,因此按需加载方案可行。 - this 指向:在选项式 API 中使用
this.$t()正常;在 setup 中要使用useI18n()。
另外,支付宝和百度小程序对 ES Module 的支持略有差异。如果你的项目需要兼容这些平台,建议将语言包文件改为 CommonJS 格式(module.exports),并在 vue.config.js 中配置条件编译。
一个通用适配技巧:在 locale/index.js 中使用条件编译:
// #ifdef MP-WEIXIN || MP-BAIDU || MP-ALIPAY
// 小程序平台特殊逻辑
const isMiniProgram = true
// #endif
// #ifdef H5 || APP-PLUS
const isMiniProgram = false
// #endif
总结
本文通过一个完整的商城国际化案例,从 vue-i18n 安装配置、语言包模块化设计、动态切换、状态持久化到各平台兼容处理,系统讲解了 uni-app 的多语言实现方案。关键要点回顾:
- 使用 vue-i18n v9 配合 Vue 3 组合式 API 是最优解。
- 语言包按业务模块拆分,支持按需加载以优化包体积。
- 通过
useI18n()钩子在脚本中获取t()和locale,实现动态切换。 - 利用
uni.getStorageSync持久化语言偏好,启动时自动恢复。 - 注意小程序平台的语言代码格式和存储限制。
现在,你可以将这套方案应用到自己的 uni-app 项目中,让应用轻松走向国际化。

