第一章:为什么需要国际化?
1.1 全球化趋势
| 场景 | 需求 |
|---|---|
| SaaS 产品出海 | 支持英语、日语、德语等 |
| 跨境电商 | 商品描述、支付提示需本地化 |
| 多地区用户 | 自动识别浏览器语言并切换 |
注意:国际化 ≠ 翻译。它包含:
- 文本翻译(Translation)
- 日期/时间/数字格式(Localization)
- 文化适配(如右到左语言 RTL)
1.2 国际化 vs 本地化
| 概念 | 说明 |
|---|---|
| i18n(Internationalization) | 架构上支持多语言(预留占位符、分离文案) |
| l10n(Localization) | 为特定地区提供本地化内容(翻译、格式) |
原则 :先 i18n,再 l10n。
第二章:前端 i18n ------ Vue I18n 实战
2.1 安装与初始化
npm install vue-i18n@9
创建 i18n 实例:
// src/i18n/index.ts
import { createI18n } from 'vue-i18n'
import en from './locales/en.json'
import zh from './locales/zh.json'
import es from './locales/es.json'
const i18n = createI18n({
legacy: false,
locale: 'en', // 默认语言
fallbackLocale: 'en',
messages: { en, zh, es }
})
export default i18n
2.2 多语言资源结构
src/
└── i18n/
└── locales/
├── en.json
├── zh.json
└── es.json
en.json 示例:
{
"common": {
"save": "Save",
"cancel": "Cancel"
},
"profile": {
"title": "My Profile",
"welcome": "Hello, {name}!",
"lastLogin": "Last login: {date}"
}
}
zh.json:
{
"common": {
"save": "保存",
"cancel": "取消"
},
"profile": {
"title": "我的资料",
"welcome": "你好,{name}!",
"lastLogin": "上次登录:{date}"
}
}
2.3 在组件中使用
<template>
<h1>{{ $t('profile.title') }}</h1>
<p>{{ $t('profile.welcome', { name: user.name }) }}</p>
<button @click="changeLanguage('zh')">中文</button>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
const changeLanguage = (lang: string) => {
locale.value = lang
// 同时通知后端(见第四章)
saveUserLanguagePreference(lang)
}
</script>
2.4 日期与数字本地化
安装 @intlify/vue-i18n-loader(Vite 支持)并配置:
// src/i18n/index.ts(扩展)
import { createI18n } from 'vue-i18n'
const datetimeFormats = {
en: { short: { year: 'numeric', month: 'short', day: 'numeric' } },
zh: { short: { year: 'numeric', month: 'long', day: 'numeric' } }
}
const numberFormats = {
en: { currency: { style: 'currency', currency: 'USD' } },
zh: { currency: { style: 'currency', currency: 'CNY' } }
}
const i18n = createI18n({
locale: 'en',
datetimeFormats,
numberFormats,
messages: { /* ... */ }
})
在模板中:
{{ $d(new Date(), 'short') }} → "Jan 10, 2026" 或 "2026年1月10日"
{{ $n(99.9, 'currency') }} → "$99.90" 或 "¥99.90"
第三章:后端 i18n ------ Flask-Babel 实战
3.1 安装与配置
pip install Flask-Babel
初始化 Babel:
# app/extensions.py
from flask_babel import Babel
babel = Babel()
# app/__init__.py
def create_app():
app = Flask(__name__)
babel.init_app(app)
return app
3.2 提取翻译字符串
在代码中标记可翻译文本:
from flask_babel import _
@app.route('/api/profile')
@jwt_required()
def get_profile():
user = get_current_user()
if not user:
# 使用 _() 标记
abort(404, _("User not found"))
return jsonify(message=_("Hello, %(name)s!", name=user.name))
邮件模板(Jinja2):
<!-- templates/emails/welcome.html -->
<h1>{{ _('Welcome!') }}</h1>
<p>{{ _('Hello %(name)s,', name=user.name) }}</p>
3.3 生成翻译文件
创建 babel.cfg:
[python: app/**.py]
[jinja2: app/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
提取字符串:
pybabel extract -F babel.cfg -o messages.pot .
初始化语言目录(首次):
pybabel init -i messages.pot -d app/translations -l zh
pybabel init -i messages.pot -d app/translations -l es
更新已有翻译:
pybabel update -i messages.pot -d app/translations
编译翻译文件(部署前必须):
pybabel compile -d app/translations
3.4 动态设置语言
根据用户偏好或请求头切换语言:
from flask_babel import get_locale
@babel.localeselector
def get_locale():
# 优先级:1. 用户设置 2. Accept-Language 头 3. 默认
if current_user.is_authenticated:
return current_user.preferred_language
return request.accept_languages.best_match(['en', 'zh', 'es']) or 'en'
注意 :
current_user需从数据库加载语言偏好。
第四章:前后端语言状态同步
4.1 用户语言偏好存储
在用户表中新增字段:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
preferred_language = db.Column(db.String(5), default='en') # 'zh', 'en', 'es'
4.2 前端保存偏好到后端
// utils/language.ts
export const saveUserLanguagePreference = async (lang: string) => {
await axios.patch('/api/user/preferences', {
language: lang
})
// 同时存入 localStorage 用于未登录场景
localStorage.setItem('app-language', lang)
}
后端接口:
@app.route('/api/user/preferences', methods=['PATCH'])
@jwt_required()
def update_preferences():
data = request.get_json()
lang = data.get('language')
if lang not in ['en', 'zh', 'es']:
abort(400, "Invalid language")
current_user.preferred_language = lang
db.session.commit()
return {"message": "Preferences updated"}
4.3 初始加载语言
-
未登录用户 :读取
localStorage或浏览器navigator.language -
已登录用户 :调用
/api/user/me获取preferred_language,并设置 Vue I18n 的locale// main.ts
const app = createApp(App)// 先设默认值
i18n.global.locale.value = localStorage.getItem('app-language') || 'en'// 登录后覆盖
if (isLoggedIn()) {
const user = await fetchCurrentUser()
i18n.global.locale.value = user.preferred_language
localStorage.setItem('app-language', user.preferred_language)
}app.use(i18n).mount('#app')
第五章:SEO 友好多语言路由
5.1 路由设计原则
| 方案 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 子路径 | /en/about, /zh/about |
✅ SEO 友好,易管理 | 需重写路由逻辑 |
| 子域名 | en.example.com |
✅ 清晰 | ❌ 需额外 DNS/SSL 配置 |
| 查询参数 | /about?lang=zh |
❌ 不被搜索引擎推荐 | 简单但不专业 |
推荐 :子路径方案(Google 官方推荐)。
5.2 Vue Router 配置
动态生成多语言路由:
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/:lang/about', component: () => import('@/views/About.vue') },
{ path: '/:lang/profile', component: () => import('@/views/Profile.vue') }
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 导航守卫:校验语言
router.beforeEach((to, from, next) => {
const supportedLangs = ['en', 'zh', 'es']
const lang = to.params.lang as string
if (!supportedLangs.includes(lang)) {
// 重定向到默认语言
return next(`/en${to.path}`)
}
next()
})
5.3 服务端渲染(SSR)或静态站点?
若使用 Vue SPA + Flask API,Nginx 需重写规则:
# nginx.conf
location ~ ^/(en|zh|es)/ {
try_files $uri $uri/ /index.html;
}
确保所有多语言路径都返回 index.html,由前端路由接管。
5.4 HTML lang 属性与 hreflang
在 index.html 中动态设置:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 动态注入 -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
</head>
通过 Vue 插件动态更新:
// plugins/hreflang.ts
export default function setupHreflang(router: Router) {
router.afterEach((to) => {
const lang = to.params.lang
document.documentElement.setAttribute('lang', lang as string)
// 移除旧 link
document.querySelectorAll('link[rel="alternate"]').forEach(el => el.remove())
// 添加新 hreflang
['en', 'zh', 'es'].forEach(l => {
const link = document.createElement('link')
link.rel = 'alternate'
link.hreflang = l
link.href = `https://example.com${to.path.replace(`/${lang}`, `/${l}`)}`
document.head.appendChild(link)
})
})
}
第六章:翻译管理与协作
6.1 问题:开发人员不适合维护翻译
- 翻译频繁变更
- 非技术人员(PM、运营)无法直接编辑 JSON/PO 文件
6.2 解决方案:集成翻译平台
选项 A:开源自建(SimpleLocalize CLI)
-
注册 SimpleLocalize(免费 tier 支持 100 keys)
-
安装 CLI:
npm install -g @simplelocalize/cli -
导出前端翻译:
simplelocalize upload --apiKey YOUR_KEY --uploadPath "src/i18n/locales/{lang}.json" --languageKey "{lang}" -
运营在 Web 界面编辑,导出为 JSON:
simplelocalize download --apiKey YOUR_KEY --downloadFormat "single-language-json" --downloadPath "src/i18n/locales/{lang}.json"
选项 B:自建管理后台(轻量级)
创建内部页面 /admin/translations,读取并编辑 en.json 等文件(需权限控制)。
推荐:初期用 SimpleLocalize,后期自建。
6.3 自动化流程(CI/CD)
# .github/workflows/i18n.yml
name: Sync Translations
on:
schedule:
- cron: '0 2 * * 1' # 每周一凌晨同步
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download translations
run: |
npx @simplelocalize/cli download \
--apiKey ${{ secrets.SIMPLELOCALIZE_API_KEY }} \
--downloadFormat single-language-json \
--downloadPath "src/i18n/locales/{lang}.json"
- name: Create PR
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore(i18n): sync translations"
title: "Sync latest translations"
第七章:特殊场景处理
7.1 复数与性别(Pluralization & Gender)
Vue I18n 支持 ICU 消息格式:
{
"messageCount": "{count, plural, =0 {No messages} =1 {One message} other {# messages}}"
}
使用:
{{ $t('messageCount', { count: 5 }) }} → "5 messages"
7.2 右到左语言(RTL)
为阿拉伯语等添加 CSS 支持:
// composables/useRtl.ts
export function useRtl() {
const isRtl = computed(() => ['ar', 'he'].includes(i18n.global.locale.value))
watch(isRtl, (rtl) => {
document.body.dir = rtl ? 'rtl' : 'ltr'
document.body.classList.toggle('rtl', rtl)
}, { immediate: true })
}
全局 CSS:
.rtl .text-left { text-align: right !important; }
.rtl .ml-4 { margin-left: 0; margin-right: 1rem; }
注意:本项目暂不支持 RTL,但架构需预留。
7.3 时区本地化
用户时区应单独存储(非语言绑定):
class User:
timezone = db.Column(db.String, default='UTC') # 如 'Asia/Shanghai'
后端返回 UTC 时间,前端用 Intl.DateTimeFormat 转换:
new Intl.DateTimeFormat('zh-CN', {
timeZone: user.timezone,
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date('2026-01-14T10:00:00Z'))
第八章:测试与验证
8.1 前端测试(Vitest)
// tests/i18n.spec.ts
import { createI18n } from 'vue-i18n'
import en from '@/i18n/locales/en.json'
test('profile title is correct', () => {
const i18n = createI18n({ locale: 'en', messages: { en } })
expect(i18n.global.t('profile.title')).toBe('My Profile')
})
8.2 后端测试
def test_error_message_localized(client):
# 设置 Accept-Language
headers = {'Accept-Language': 'zh'}
resp = client.get('/api/non-existent', headers=headers)
assert "未找到" in resp.json['message'] # Chinese error
8.3 视觉回归测试
使用 Chromatic 或 Playwright 截图对比不同语言下的 UI 布局,防止文本溢出。
第九章:部署与性能优化
9.1 按需加载语言包
避免一次性加载所有语言:
// src/i18n/dynamic-loader.ts
const loadLocaleMessages = async (lang: string) => {
const messages = await import(`./locales/${lang}.json`)
return messages.default
}
const setI18nLanguage = async (lang: string) => {
if (!i18n.global.availableLocales.includes(lang)) {
const messages = await loadLocaleMessages(lang)
i18n.global.setLocaleMessage(lang, messages)
}
i18n.global.locale.value = lang
}
9.2 后端缓存翻译
Flask-Babel 默认每次请求解析 .mo 文件。高并发下可缓存:
# 使用 simplekv 缓存
from simplekv.memory import DictStore
babel = Babel(app, cache=DictStore())