标题: 国际化还在硬编码?i18n来救场!
副标题: 从资源文件到动态切换,打造全球化应用
🎬 开篇:一次国际化的尴尬事件
某SaaS平台拓展海外市场:
中国用户:登录成功! ✅
美国用户:登录成功! ❌(显示中文)
美国用户:What?我看不懂中文! 💀
开发紧急修复:
if (locale == "en") {
message = "Login successful!";
} else {
message = "登录成功!";
}
1个月后...
- 支持10种语言
- 代码里到处是if-else
- 维护噩梦 💀
引入i18n后:
- 配置文件管理翻译
- 自动语言切换
- 支持100+语言
- 维护轻松 ✅
老板:这才对嘛! 😊
教训:国际化要从架构层面设计,而不是硬编码!
🤔 什么是国际化(i18n)?
i18n = internationalization(首字母i + 中间18个字母 + 尾字母n)
核心概念:
- 语言包: 不同语言的翻译资源
- 区域设置: zh_CN(中文)、en_US(英语)、ja_JP(日语)
- 动态切换: 运行时切换语言
- 自动适配: 根据用户偏好自动选择
📚 知识地图
多语言国际化方案
├── 🎯 核心功能
│ ├── 资源文件管理
│ ├── 语言动态切换
│ ├── 区域格式化(日期、货币、数字)
│ ├── 翻译管理后台
│ └── 翻译缺失提示
├── 🏗️ 技术方案
│ ├── Spring i18n ⭐⭐⭐⭐⭐
│ ├── Vue i18n ⭐⭐⭐⭐⭐
│ ├── React i18next ⭐⭐⭐⭐⭐
│ └── 数据库国际化 ⭐⭐⭐⭐
├── ⚡ 高级功能
│ ├── 自动翻译(百度/谷歌API)
│ ├── 翻译缓存
│ ├── 翻译版本管理
│ └── 翻译审核流程
└── 📊 最佳实践
├── 资源文件命名规范
├── 占位符使用
├── 复数处理
└── 性能优化
📦 后端实现(Spring Boot)
1. 配置国际化
# application.yml
spring:
messages:
# 资源文件基础名
basename: i18n/messages
# 编码
encoding: UTF-8
# 缓存时间(-1表示永久缓存)
cache-duration: 3600
# 找不到消息时是否使用消息代码作为默认消息
fallback-to-system-locale: true
2. 资源文件
# i18n/messages_zh_CN.properties(中文)
welcome=欢迎使用系统
login.success=登录成功!
login.fail=登录失败,用户名或密码错误
user.not.found=用户不存在
order.created=订单创建成功,订单号:{0}
order.total=订单总金额:{0}元
# i18n/messages_en_US.properties(英文)
welcome=Welcome to the system
login.success=Login successful!
login.fail=Login failed, incorrect username or password
user.not.found=User not found
order.created=Order created successfully, order number: {0}
order.total=Order total: ${0}
# i18n/messages_ja_JP.properties(日语)
welcome=システムへようこそ
login.success=ログインに成功しました!
login.fail=ログインに失敗しました。ユーザー名またはパスワードが正しくありません
user.not.found=ユーザーが見つかりません
order.created=注文が正常に作成されました。注文番号:{0}
order.total=注文合計:{0}円
3. 国际化配置类
/**
* 国际化配置
*/
@Configuration
public class I18nConfig {
/**
* ⚡ 国际化消息源
*/
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// 设置资源文件基础名
messageSource.setBasenames("i18n/messages", "i18n/errors", "i18n/validation");
// 设置编码
messageSource.setDefaultEncoding("UTF-8");
// 设置缓存时间(秒)
messageSource.setCacheSeconds(3600);
// 找不到消息时使用消息代码
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
/**
* ⚡ 区域解析器(从请求头获取语言)
*/
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
// 设置默认语言
localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
// 支持的语言列表
List<Locale> supportedLocales = Arrays.asList(
Locale.SIMPLIFIED_CHINESE, // zh_CN
Locale.US, // en_US
Locale.JAPAN // ja_JP
);
localeResolver.setSupportedLocales(supportedLocales);
return localeResolver;
}
/**
* ⚡ 区域解析器(从Cookie/Session获取语言)
*/
@Bean
public LocaleResolver cookieLocaleResolver() {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
// 设置默认语言
localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
// Cookie名称
localeResolver.setCookieName("language");
// Cookie有效期(30天)
localeResolver.setCookieMaxAge(30 * 24 * 3600);
return localeResolver;
}
/**
* ⚡ 语言切换拦截器
*/
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
// 请求参数名(?lang=en_US)
interceptor.setParamName("lang");
return interceptor;
}
/**
* 注册拦截器
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LocaleChangeInterceptor localeChangeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor);
}
}
}
4. 国际化工具类
/**
* 国际化工具类
*/
@Component
public class I18nUtils {
private static MessageSource messageSource;
@Autowired
public void setMessageSource(MessageSource messageSource) {
I18nUtils.messageSource = messageSource;
}
/**
* ⚡ 获取国际化消息
*/
public static String getMessage(String code) {
return getMessage(code, null);
}
/**
* ⚡ 获取国际化消息(带参数)
*/
public static String getMessage(String code, Object... args) {
try {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(code, args, locale);
} catch (Exception e) {
return code;
}
}
/**
* ⚡ 获取国际化消息(指定语言)
*/
public static String getMessage(String code, Locale locale, Object... args) {
try {
return messageSource.getMessage(code, args, locale);
} catch (Exception e) {
return code;
}
}
/**
* 获取当前语言
*/
public static Locale getCurrentLocale() {
return LocaleContextHolder.getLocale();
}
/**
* 设置当前语言
*/
public static void setCurrentLocale(Locale locale) {
LocaleContextHolder.setLocale(locale);
}
/**
* ⚡ 格式化日期
*/
public static String formatDate(Date date) {
Locale locale = getCurrentLocale();
DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
return dateFormat.format(date);
}
/**
* ⚡ 格式化时间
*/
public static String formatDateTime(Date date) {
Locale locale = getCurrentLocale();
DateFormat dateFormat = DateFormat.getDateTimeInstance(
DateFormat.MEDIUM, DateFormat.MEDIUM, locale);
return dateFormat.format(date);
}
/**
* ⚡ 格式化货币
*/
public static String formatCurrency(BigDecimal amount) {
Locale locale = getCurrentLocale();
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
return currencyFormat.format(amount);
}
/**
* ⚡ 格式化数字
*/
public static String formatNumber(Number number) {
Locale locale = getCurrentLocale();
NumberFormat numberFormat = NumberFormat.getNumberInstance(locale);
return numberFormat.format(number);
}
}
5. 使用示例
/**
* Controller示例
*/
@RestController
@RequestMapping("/api")
public class UserController {
/**
* ⚡ 登录(自动国际化)
*/
@PostMapping("/login")
public Result<Void> login(@RequestBody LoginDTO dto) {
// 业务逻辑
boolean success = userService.login(dto);
if (success) {
// ⚡ 获取国际化消息
String message = I18nUtils.getMessage("login.success");
return Result.success(message);
} else {
String message = I18nUtils.getMessage("login.fail");
return Result.fail(message);
}
}
/**
* ⚡ 创建订单(带参数)
*/
@PostMapping("/order")
public Result<Void> createOrder(@RequestBody OrderDTO dto) {
Order order = orderService.createOrder(dto);
// ⚡ 带参数的国际化消息
String message = I18nUtils.getMessage("order.created", order.getOrderNo());
return Result.success(message);
}
/**
* ⚡ 切换语言
*/
@GetMapping("/language/switch")
public Result<Void> switchLanguage(@RequestParam String lang) {
// 解析语言
Locale locale = Locale.forLanguageTag(lang.replace("_", "-"));
// 设置语言
I18nUtils.setCurrentLocale(locale);
return Result.success(I18nUtils.getMessage("language.switched"));
}
}
/**
* 异常处理(国际化)
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* ⚡ 业务异常(自动国际化)
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
// 从异常中获取错误代码
String errorCode = e.getErrorCode();
// ⚡ 获取国际化错误消息
String message = I18nUtils.getMessage(errorCode, e.getArgs());
return Result.fail(message);
}
}
/**
* 校验消息国际化
*/
@Data
public class UserDTO {
@NotBlank(message = "{user.username.notblank}")
private String username;
@NotBlank(message = "{user.password.notblank}")
@Size(min = 6, max = 20, message = "{user.password.size}")
private String password;
@Email(message = "{user.email.invalid}")
private String email;
}
🌐 前端实现(Vue 3 + Vue I18n)
1. 安装依赖
npm install vue-i18n@9
2. 配置国际化
// src/i18n/index.js
import { createI18n } from 'vue-i18n'
import zhCN from './locales/zh-CN'
import enUS from './locales/en-US'
import jaJP from './locales/ja-JP'
// ⚡ 创建i18n实例
const i18n = createI18n({
legacy: false, // 使用Composition API模式
locale: localStorage.getItem('language') || 'zh-CN', // 默认语言
fallbackLocale: 'zh-CN', // 回退语言
messages: {
'zh-CN': zhCN,
'en-US': enUS,
'ja-JP': jaJP
}
})
export default i18n
3. 语言包文件
// src/i18n/locales/zh-CN.js
export default {
common: {
confirm: '确认',
cancel: '取消',
submit: '提交',
reset: '重置',
search: '搜索',
add: '新增',
edit: '编辑',
delete: '删除',
export: '导出',
import: '导入'
},
login: {
title: '用户登录',
username: '用户名',
password: '密码',
rememberMe: '记住我',
loginBtn: '登录',
success: '登录成功!',
fail: '登录失败,用户名或密码错误'
},
user: {
profile: '个人资料',
settings: '设置',
logout: '退出登录'
},
order: {
list: '订单列表',
detail: '订单详情',
create: '创建订单',
total: '订单总额:{amount}元',
status: {
pending: '待支付',
paid: '已支付',
shipped: '已发货',
completed: '已完成',
cancelled: '已取消'
}
}
}
// src/i18n/locales/en-US.js
export default {
common: {
confirm: 'Confirm',
cancel: 'Cancel',
submit: 'Submit',
reset: 'Reset',
search: 'Search',
add: 'Add',
edit: 'Edit',
delete: 'Delete',
export: 'Export',
import: 'Import'
},
login: {
title: 'User Login',
username: 'Username',
password: 'Password',
rememberMe: 'Remember Me',
loginBtn: 'Login',
success: 'Login successful!',
fail: 'Login failed, incorrect username or password'
},
user: {
profile: 'Profile',
settings: 'Settings',
logout: 'Logout'
},
order: {
list: 'Order List',
detail: 'Order Detail',
create: 'Create Order',
total: 'Order Total: ${amount}',
status: {
pending: 'Pending Payment',
paid: 'Paid',
shipped: 'Shipped',
completed: 'Completed',
cancelled: 'Cancelled'
}
}
}
4. Vue组件使用
<!-- LoginForm.vue -->
<template>
<div class="login-form">
<!-- ⚡ 使用$t()函数 -->
<h2>{{ $t('login.title') }}</h2>
<el-form :model="form" :rules="rules">
<el-form-item :label="$t('login.username')" prop="username">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item :label="$t('login.password')" prop="password">
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.rememberMe">
{{ $t('login.rememberMe') }}
</el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin">
{{ $t('login.loginBtn') }}
</el-button>
</el-form-item>
</el-form>
<!-- ⚡ 语言切换 -->
<div class="language-switch">
<el-select v-model="currentLanguage" @change="switchLanguage">
<el-option label="简体中文" value="zh-CN" />
<el-option label="English" value="en-US" />
<el-option label="日本語" value="ja-JP" />
</el-select>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
const { t, locale } = useI18n()
const form = ref({
username: '',
password: '',
rememberMe: false
})
const currentLanguage = ref(locale.value)
// ⚡ 切换语言
const switchLanguage = (lang) => {
locale.value = lang
localStorage.setItem('language', lang)
ElMessage.success(t('language.switched'))
}
// 登录
const handleLogin = () => {
// 业务逻辑
ElMessage.success(t('login.success'))
}
</script>
5. Composition API使用
// useI18n.js
import { computed } from 'vue'
import { useI18n as useVueI18n } from 'vue-i18n'
export function useI18n() {
const { t, locale } = useVueI18n()
// ⚡ 获取当前语言
const currentLanguage = computed(() => locale.value)
// ⚡ 切换语言
const switchLanguage = (lang) => {
locale.value = lang
localStorage.setItem('language', lang)
// 通知后端
document.cookie = `language=${lang}; path=/; max-age=${30 * 24 * 3600}`
}
// ⚡ 格式化日期
const formatDate = (date) => {
return new Intl.DateTimeFormat(locale.value).format(date)
}
// ⚡ 格式化货币
const formatCurrency = (amount) => {
return new Intl.NumberFormat(locale.value, {
style: 'currency',
currency: locale.value === 'zh-CN' ? 'CNY' :
locale.value === 'en-US' ? 'USD' : 'JPY'
}).format(amount)
}
return {
t,
currentLanguage,
switchLanguage,
formatDate,
formatCurrency
}
}
🗄️ 数据库国际化方案
-- 商品表
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_no VARCHAR(50) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 商品国际化表
CREATE TABLE product_i18n (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL COMMENT '商品ID',
locale VARCHAR(10) NOT NULL COMMENT '语言:zh_CN/en_US/ja_JP',
name VARCHAR(200) NOT NULL COMMENT '商品名称',
description TEXT COMMENT '商品描述',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_product_locale (product_id, locale),
INDEX idx_product_id (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品国际化表';
/**
* 商品VO(包含国际化字段)
*/
@Data
public class ProductVO {
private Long id;
private String productNo;
private BigDecimal price;
private Integer stock;
/**
* ⚡ 国际化字段
*/
private String name;
private String description;
}
/**
* 查询商品(自动国际化)
*/
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
/**
* ⚡ 查询商品详情(自动获取当前语言的翻译)
*/
public ProductVO getProduct(Long id) {
// 获取当前语言
String locale = LocaleContextHolder.getLocale().toString();
// 查询商品及国际化信息
return productMapper.selectWithI18n(id, locale);
}
}
@Mapper
public interface ProductMapper {
/**
* ⚡ 查询商品及国际化信息
*/
@Select("SELECT p.*, pi.name, pi.description " +
"FROM product p " +
"LEFT JOIN product_i18n pi ON p.id = pi.product_id AND pi.locale = #{locale} " +
"WHERE p.id = #{id}")
ProductVO selectWithI18n(@Param("id") Long id, @Param("locale") String locale);
}
✅ 最佳实践
多语言国际化最佳实践:
1️⃣ 资源文件管理:
□ 按模块分离(messages、errors、validation)
□ 命名规范(messages_zh_CN.properties)
□ UTF-8编码
□ 版本管理
2️⃣ 翻译管理:
□ 专业翻译
□ 审核流程
□ 版本控制
□ 缺失提示
3️⃣ 性能优化:
□ 资源文件缓存
□ 懒加载
□ CDN加速
□ 预加载常用语言
4️⃣ 用户体验:
□ 自动检测浏览器语言
□ 记住用户选择
□ 平滑切换
□ 实时生效
5️⃣ 格式化:
□ 日期格式(2025-01-15 vs 01/15/2025)
□ 货币格式(¥100 vs $100)
□ 数字格式(1,000 vs 1.000)
□ 时区转换
🎉 总结
多语言国际化核心:
1️⃣ 资源文件:分离文本和代码
2️⃣ 动态切换:运行时切换语言
3️⃣ 自动适配:浏览器语言检测
4️⃣ 格式化:日期、货币、数字本地化
5️⃣ 数据库国际化:多语言数据分表存储
记住:国际化要从设计阶段开始考虑! 🌍
文档编写时间:2025年10月24日
作者:热爱全球化的i18n工程师
版本:v1.0
愿你的应用走向全球! ✨