Spring国际化i18n的环球之旅 🌍🗺️

120 阅读10分钟

副标题: 让你的应用说遍全球语言!从Hello到你好到こんにちは!


🎬 开场白:你的应用会说几国语言?

嘿,朋友!👋 想象一下这个场景:

美国用户打开你的网站:
"Welcome to our website!"

中国用户打开同样的网站:
"欢迎来到我们的网站!"

日本用户打开:
"私たちのウェブサイトへようこそ!"

同一个应用,不同的语言!🎉

这就是**国际化(i18n)**的魔力!

小知识:为什么叫i18n?因为internationalization首字母i和尾字母n之间有18个字母!😄

今天,我们就来揭秘Spring如何实现这个魔法!✨


🤔 什么是国际化(i18n)?

官方定义(别睡着😴)

国际化(Internationalization,简称i18n)是指设计和开发应用程序,使其能够适应不同语言和地区,无需修改代码即可支持多种语言。

人话翻译(醒醒!👀)

i18n = 让你的应用变成"变色龙" 🦎

没有i18n的应用(硬编码):
System.out.println("Welcome!");  // 只能说英语

有i18n的应用(智能切换):
System.out.println(getMessage("welcome"));
- 英语环境 → Welcome!
- 中文环境 → 欢迎!
- 日语环境 → ようこそ!

核心概念:

  • 📝 i18n(国际化):让应用支持多语言
  • 🌍 L10n(本地化):为特定地区定制内容
  • 🗣️ Locale(语言环境):zh_CN、en_US、ja_JP

🏗️ Spring i18n架构

核心组件

┌─────────────────────────────────────┐
│         应用程序                      │
│  getMessage("welcome", locale)      │
└─────────────────┬───────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│      MessageSource(消息源)         │
│  - ResourceBundleMessageSource      │
│  - ReloadableResourceBundleMessageSource │
└─────────────────┬───────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│      LocaleResolver(语言解析器)     │
│  - AcceptHeaderLocaleResolver       │
│  - CookieLocaleResolver             │
│  - SessionLocaleResolver            │
└─────────────────┬───────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│      资源文件(.properties)          │
│  - messages_zh_CN.properties        │
│  - messages_en_US.properties        │
│  - messages_ja_JP.properties        │
└─────────────────────────────────────┘

三大核心接口

  1. MessageSource:消息源,提供国际化消息
  2. LocaleResolver:解析当前用户的语言环境
  3. LocaleChangeInterceptor:拦截器,处理语言切换

🎨 生活化比喻:国际酒店前台

场景:五星级国际酒店

没有i18n(单语言前台)

客人(中国人):"你好!"
前台(只会英语):"Sorry, I don't understand."
客人:😓 尴尬了...

有i18n(智能前台)

客人走进来...

前台系统自动识别:
- 看护照 → 中国人 → 切换中文模式
- 用中文问候:"您好,欢迎光临!"

客人换成日本人:
- 系统识别 → 日本人 → 切换日语模式
- 用日语问候:"いらっしゃいませ!"

客人换成美国人:
- 系统识别 → 美国人 → 切换英语模式
- 用英语问候:"Welcome!"

同一个前台,智能切换语言!👏

Spring的i18n就是这个"智能前台系统"!


💻 实战案例一:基础配置

Step 1:创建资源文件

src/main/resources/
├── i18n/
│   ├── messages.properties           # 默认(英文)
│   ├── messages_zh_CN.properties     # 简体中文
│   ├── messages_en_US.properties     # 美式英语
│   └── messages_ja_JP.properties     # 日语

messages.properties(默认)

# 通用
app.name=My Application
app.welcome=Welcome!
app.goodbye=Goodbye!

# 用户相关
user.login.success=Login successful!
user.login.fail=Invalid username or password!
user.register.success=Registration successful!

# 验证消息
validation.email.invalid=Invalid email format
validation.password.short=Password must be at least 6 characters
validation.required={0} is required

messages_zh_CN.properties(中文)

# 通用
app.name=我的应用
app.welcome=欢迎!
app.goodbye=再见!

# 用户相关
user.login.success=登录成功!
user.login.fail=用户名或密码错误!
user.register.success=注册成功!

# 验证消息
validation.email.invalid=邮箱格式不正确
validation.password.short=密码至少6个字符
validation.required={0}不能为空

messages_ja_JP.properties(日语)

# 通用
app.name=私のアプリ
app.welcome=ようこそ!
app.goodbye=さようなら!

# 用户相关
user.login.success=ログイン成功!
user.login.fail=ユーザー名またはパスワードが無効です!
user.register.success=登録成功!

# 验证消息
validation.email.invalid=メール形式が無効です
validation.password.short=パスワードは6文字以上である必要があります
validation.required={0}は必須です

Step 2:配置MessageSource

@Configuration
public class I18nConfig {
    
    /**
     * 消息源配置
     */
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = 
            new ResourceBundleMessageSource();
        
        // 设置资源文件基础名
        messageSource.setBasenames("i18n/messages");
        
        // 设置默认编码
        messageSource.setDefaultEncoding("UTF-8");
        
        // 设置缓存时间(秒),-1表示永久缓存
        messageSource.setCacheSeconds(3600);
        
        // 找不到key时不抛异常,返回key本身
        messageSource.setUseCodeAsDefaultMessage(true);
        
        return messageSource;
    }
    
    /**
     * LocaleResolver配置(解析语言环境)
     */
    @Bean
    public LocaleResolver localeResolver() {
        // 方式1:从Session中获取Locale
        SessionLocaleResolver resolver = new SessionLocaleResolver();
        resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return resolver;
    }
    
    /**
     * 语言切换拦截器
     */
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        // 通过参数lang切换语言:?lang=zh_CN
        interceptor.setParamName("lang");
        return interceptor;
    }
    
    /**
     * 注册拦截器
     */
    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Autowired
        private LocaleChangeInterceptor localeChangeInterceptor;
        
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(localeChangeInterceptor);
        }
    }
}

Step 3:使用MessageSource

@RestController
@RequestMapping("/api")
public class I18nController {
    
    @Autowired
    private MessageSource messageSource;
    
    /**
     * 自动获取当前Locale
     */
    @GetMapping("/welcome")
    public String welcome(Locale locale) {
        String message = messageSource.getMessage(
            "app.welcome",    // key
            null,             // 参数
            locale            // 语言环境
        );
        return message;
    }
    
    /**
     * 带参数的消息
     */
    @GetMapping("/greeting")
    public String greeting(@RequestParam String name, Locale locale) {
        // messages.properties: greeting=Hello, {0}!
        String message = messageSource.getMessage(
            "greeting",
            new Object[]{name},  // 参数数组
            locale
        );
        return message;  // "Hello, Zhang San!"
    }
    
    /**
     * 手动指定Locale
     */
    @GetMapping("/test")
    public Map<String, String> test() {
        Map<String, String> result = new HashMap<>();
        
        // 中文
        result.put("zh_CN", messageSource.getMessage(
            "app.welcome", null, Locale.SIMPLIFIED_CHINESE
        ));
        
        // 英文
        result.put("en_US", messageSource.getMessage(
            "app.welcome", null, Locale.US
        ));
        
        // 日语
        result.put("ja_JP", messageSource.getMessage(
            "app.welcome", null, Locale.JAPAN
        ));
        
        return result;
    }
}

Step 4:测试效果

# 默认中文
curl http://localhost:8080/api/welcome
# 输出:欢迎!

# 切换到英文
curl http://localhost:8080/api/welcome?lang=en_US
# 输出:Welcome!

# 切换到日语
curl http://localhost:8080/api/welcome?lang=ja_JP
# 输出:ようこそ!

# 带参数的消息
curl http://localhost:8080/api/greeting?name=Zhang%20San&lang=en_US
# 输出:Hello, Zhang San!

💻 实战案例二:四种LocaleResolver

1. AcceptHeaderLocaleResolver(请求头)

@Bean
public LocaleResolver localeResolver() {
    AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
    resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
    return resolver;
}

工作原理:从HTTP请求头Accept-Language中获取语言。

curl -H "Accept-Language: zh-CN" http://localhost:8080/api/welcome
# 输出:欢迎!

curl -H "Accept-Language: en-US" http://localhost:8080/api/welcome
# 输出:Welcome!

特点

  • ✅ 自动根据浏览器语言切换
  • ❌ 用户无法手动切换
  • ✅ 无状态,适合RESTful API

2. SessionLocaleResolver(Session)

@Bean
public LocaleResolver localeResolver() {
    SessionLocaleResolver resolver = new SessionLocaleResolver();
    resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
    return resolver;
}

工作原理:将Locale存储在Session中。

@GetMapping("/changeLanguage")
public String changeLanguage(@RequestParam String lang, HttpServletRequest request) {
    LocaleResolver resolver = RequestContextUtils.getLocaleResolver(request);
    
    if (resolver instanceof SessionLocaleResolver) {
        Locale locale = StringUtils.parseLocaleString(lang);
        ((SessionLocaleResolver) resolver).setLocale(request, null, locale);
    }
    
    return "Language changed to: " + lang;
}

特点

  • ✅ 用户可以手动切换
  • ✅ Session期间保持语言设置
  • ❌ 需要Session支持

3. CookieLocaleResolver(Cookie)

@Bean
public LocaleResolver localeResolver() {
    CookieLocaleResolver resolver = new CookieLocaleResolver();
    resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
    resolver.setCookieName("language");
    resolver.setCookieMaxAge(3600);  // 1小时
    return resolver;
}

工作原理:将Locale存储在Cookie中。

特点

  • ✅ 跨Session保持语言设置
  • ✅ 关闭浏览器后仍保持
  • ❌ 依赖Cookie

4. FixedLocaleResolver(固定)

@Bean
public LocaleResolver localeResolver() {
    FixedLocaleResolver resolver = new FixedLocaleResolver();
    resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
    return resolver;
}

工作原理:固定语言,不可切换。

特点

  • ✅ 简单
  • ❌ 无法切换语言
  • 🎯 适合单语言应用

💻 实战案例三:Thymeleaf模板集成

application.yml

spring:
  messages:
    basename: i18n/messages
    encoding: UTF-8
    cache-duration: 3600

Controller

@Controller
public class WebController {
    
    @GetMapping("/")
    public String index(Model model, Locale locale) {
        model.addAttribute("currentLocale", locale.toString());
        return "index";
    }
}

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="#{app.name}">My App</title>
</head>
<body>
    <h1 th:text="#{app.welcome}">Welcome</h1>
    
    <!-- 带参数的消息 -->
    <p th:text="#{user.greeting(${username})}">Hello, User!</p>
    
    <!-- 语言切换链接 -->
    <div>
        <a th:href="@{/?lang=zh_CN}">中文</a> |
        <a th:href="@{/?lang=en_US}">English</a> |
        <a th:href="@{/?lang=ja_JP}">日本語</a>
    </div>
    
    <!-- 显示当前语言 -->
    <p>Current Locale: <span th:text="${currentLocale}"></span></p>
</body>
</html>

💻 实战案例四:RESTful API国际化

方案1:通过请求头

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private MessageSource messageSource;
    
    @PostMapping("/login")
    public Result login(@RequestBody LoginRequest request, 
                       @RequestHeader(value = "Accept-Language", defaultValue = "zh-CN") 
                       String lang) {
        
        Locale locale = Locale.forLanguageTag(lang);
        
        // 验证失败
        if (!validate(request)) {
            String message = messageSource.getMessage(
                "user.login.fail", null, locale
            );
            return Result.fail(message);
        }
        
        // 登录成功
        String message = messageSource.getMessage(
            "user.login.success", null, locale
        );
        return Result.ok(message);
    }
}

方案2:通过URL参数

@GetMapping("/profile")
public Result profile(@RequestParam(defaultValue = "zh_CN") String lang) {
    Locale locale = StringUtils.parseLocaleString(lang);
    
    String message = messageSource.getMessage(
        "user.profile.title", null, locale
    );
    
    return Result.ok(message);
}

方案3:自定义响应包装

@Data
@AllArgsConstructor
public class I18nResult<T> {
    private int code;
    private String message;
    private T data;
    
    public static <T> I18nResult<T> ok(String messageKey, Locale locale) {
        MessageSource ms = SpringContextHolder.getBean(MessageSource.class);
        String message = ms.getMessage(messageKey, null, locale);
        return new I18nResult<>(200, message, null);
    }
    
    public static <T> I18nResult<T> fail(String messageKey, Locale locale) {
        MessageSource ms = SpringContextHolder.getBean(MessageSource.class);
        String message = ms.getMessage(messageKey, null, locale);
        return new I18nResult<>(500, message, null);
    }
}

// 使用
@PostMapping("/register")
public I18nResult register(@RequestBody User user, Locale locale) {
    // 业务逻辑...
    return I18nResult.ok("user.register.success", locale);
}

💻 实战案例五:参数化消息

资源文件

# messages_zh_CN.properties
user.welcome=欢迎,{0}!
order.created=订单{0}创建成功,总金额:{1}元
email.sent={0}封邮件已发送到{1}
file.uploaded={0}上传成功,大小:{1}MB

使用

@Service
public class NotificationService {
    
    @Autowired
    private MessageSource messageSource;
    
    public String welcomeUser(String username, Locale locale) {
        return messageSource.getMessage(
            "user.welcome",
            new Object[]{username},
            locale
        );
        // 中文:欢迎,张三!
        // 英文:Welcome, Zhang San!
    }
    
    public String orderCreated(String orderId, BigDecimal amount, Locale locale) {
        return messageSource.getMessage(
            "order.created",
            new Object[]{orderId, amount},
            locale
        );
        // 中文:订单ORD20250123创建成功,总金额:99.00元
    }
    
    public String emailSent(int count, String email, Locale locale) {
        return messageSource.getMessage(
            "email.sent",
            new Object[]{count, email},
            locale
        );
        // 中文:3封邮件已发送到zhangsan@example.com
    }
}

💻 实战案例六:动态切换语言

前端代码

<!-- 语言选择器 -->
<select id="languageSelector" onchange="changeLanguage()">
    <option value="zh_CN">简体中文</option>
    <option value="en_US">English</option>
    <option value="ja_JP">日本語</option>
</select>

<script>
function changeLanguage() {
    const lang = document.getElementById('languageSelector').value;
    
    // 方式1:URL参数
    window.location.href = `?lang=${lang}`;
    
    // 方式2:AJAX请求
    fetch(`/api/changeLanguage?lang=${lang}`, {
        method: 'POST'
    }).then(response => {
        window.location.reload();
    });
}
</script>

后端代码

@RestController
public class LanguageController {
    
    @PostMapping("/api/changeLanguage")
    public Result changeLanguage(@RequestParam String lang, 
                                 HttpServletRequest request,
                                 HttpServletResponse response) {
        
        Locale locale = StringUtils.parseLocaleString(lang);
        
        // 获取LocaleResolver
        LocaleResolver resolver = RequestContextUtils.getLocaleResolver(request);
        
        // 设置新的Locale
        resolver.setLocale(request, response, locale);
        
        return Result.ok("Language changed to: " + lang);
    }
    
    @GetMapping("/api/currentLanguage")
    public Result getCurrentLanguage(Locale locale) {
        return Result.ok(locale.toString());
    }
}

🎯 高级技巧

技巧1:自定义MessageSource

@Configuration
public class CustomMessageSourceConfig {
    
    @Bean
    public MessageSource messageSource() {
        return new DatabaseMessageSource();
    }
}

/**
 * 从数据库读取国际化消息
 */
public class DatabaseMessageSource extends AbstractMessageSource {
    
    @Autowired
    private I18nMessageRepository repository;
    
    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {
        // 从数据库查询
        I18nMessage message = repository.findByCodeAndLocale(
            code, 
            locale.toString()
        );
        
        if (message != null) {
            return new MessageFormat(message.getText(), locale);
        }
        
        return null;
    }
}

// 数据库表结构
@Entity
@Table(name = "i18n_messages")
public class I18nMessage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String code;       // 消息key
    private String locale;     // 语言环境
    private String text;       // 消息内容
    
    // Getter和Setter...
}

技巧2:支持热更新

@Bean
public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource = 
        new ReloadableResourceBundleMessageSource();
    
    messageSource.setBasenames("classpath:i18n/messages");
    messageSource.setDefaultEncoding("UTF-8");
    
    // 设置缓存时间(秒)
    // 修改properties文件后,最多1秒后生效
    messageSource.setCacheSeconds(1);
    
    return messageSource;
}

技巧3:多个资源文件

@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource messageSource = 
        new ResourceBundleMessageSource();
    
    // 支持多个资源文件
    messageSource.setBasenames(
        "i18n/messages",      // 通用消息
        "i18n/validation",    // 验证消息
        "i18n/errors",        // 错误消息
        "i18n/business"       // 业务消息
    );
    
    messageSource.setDefaultEncoding("UTF-8");
    
    return messageSource;
}

技巧4:Spring Boot简化配置

# application.yml
spring:
  messages:
    basename: i18n/messages,i18n/validation,i18n/errors
    encoding: UTF-8
    cache-duration: 3600
    fallback-to-system-locale: true
// 自动配置,无需手动创建MessageSource Bean
@RestController
public class SimpleController {
    
    @Autowired
    private MessageSource messageSource;  // 自动注入
    
    @GetMapping("/hello")
    public String hello(Locale locale) {
        return messageSource.getMessage("app.welcome", null, locale);
    }
}

📊 LocaleResolver选择指南

LocaleResolver适用场景优点缺点
AcceptHeaderLocaleResolverRESTful API自动识别浏览器语言无法手动切换
SessionLocaleResolver传统Web应用用户可切换,Session内保持需要Session
CookieLocaleResolver需要持久化语言设置跨Session保持依赖Cookie
FixedLocaleResolver单语言应用简单不支持多语言

推荐方案

/**
 * 组合方案:优先Cookie,其次Header
 */
@Bean
public LocaleResolver localeResolver() {
    CookieLocaleResolver resolver = new CookieLocaleResolver();
    resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
    resolver.setCookieName("APP_LANGUAGE");
    resolver.setCookieMaxAge(7 * 24 * 3600);  // 7天
    return resolver;
}

⚠️ 常见坑点与避坑指南

坑点1:编码问题

# ❌ 错误:中文乱码
user.welcome=欢迎!  # ISO-8859-1编码会乱码

# ✅ 方案1:使用Unicode转义
user.welcome=\u6B22\u8FCE\uFF01

# ✅ 方案2:设置编码(推荐)
@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
    ms.setBasenames("i18n/messages");
    ms.setDefaultEncoding("UTF-8");  // 设置编码!
    return ms;
}

坑点2:找不到资源文件

// ❌ 错误:路径写错
messageSource.setBasenames("messages");  // 找不到

// ✅ 正确
messageSource.setBasenames("i18n/messages");  // 基于classpath

// ✅ 或者使用绝对路径
messageSource.setBasenames("classpath:i18n/messages");

坑点3:key不存在

// ❌ 错误:找不到key时抛异常
String msg = messageSource.getMessage("nonexist.key", null, locale);
// 抛出:NoSuchMessageException

// ✅ 正确:设置返回key本身
messageSource.setUseCodeAsDefaultMessage(true);
String msg = messageSource.getMessage("nonexist.key", null, locale);
// 返回:"nonexist.key"

// ✅ 或者提供默认值
String msg = messageSource.getMessage(
    "nonexist.key", 
    null, 
    "Default Message",  // 默认值
    locale
);

坑点4:参数顺序错误

# messages.properties
order.info=Order {0} created by {1}, amount: {2}
// ❌ 错误:参数顺序不对
messageSource.getMessage(
    "order.info",
    new Object[]{"张三", "ORD123", 99.00},  // 顺序错了
    locale
);
// 输出:Order 张三 created by ORD123, amount: 99.00

// ✅ 正确
messageSource.getMessage(
    "order.info",
    new Object[]{"ORD123", "张三", 99.00},
    locale
);
// 输出:Order ORD123 created by 张三, amount: 99.00

🎉 总结

核心要点

  1. 三大组件:

    • MessageSource:消息源
    • LocaleResolver:语言解析器
    • LocaleChangeInterceptor:语言切换拦截器
  2. 资源文件命名:

    messages.properties              # 默认
    messages_{language}.properties   # 语言
    messages_{language}_{country}.properties  # 语言+国家
    
  3. 常用LocaleResolver:

    • AcceptHeaderLocaleResolver:请求头
    • SessionLocaleResolver:Session
    • CookieLocaleResolver:Cookie
  4. Spring Boot自动配置:

    spring.messages.basename: i18n/messages
    spring.messages.encoding: UTF-8
    
  5. 最佳实践:

    • ✅ 统一资源文件位置
    • ✅ 设置UTF-8编码
    • ✅ 提供默认语言
    • ✅ 使用参数化消息
    • ✅ 考虑缓存策略

📚 参考资料

  • Spring官方文档:Internationalization using MessageSource
  • Java官方文档:Locale
  • 《Spring实战》- Craig Walls
  • Unicode转义工具:native2ascii

🎮 课后练习

练习1:多级回退

实现资源文件的多级回退:

zh_CN → zh → 默认

练习2:数据库国际化

实现从数据库读取国际化消息,支持在线编辑。

练习3:Excel导入导出

实现国际化消息的Excel批量导入导出功能。


💬 最后的话

国际化是让你的应用走向世界的第一步!🌍

虽然配置看起来有点繁琐,但一旦配置好,就能轻松支持全球用户。

记住这个公式:

好的i18n = 统一的资源文件 + 合适的LocaleResolver + 完善的翻译

现在,让你的应用说遍全球语言吧!🗣️✨


作者心声:我第一次做国际化时,把中文直接写在properties文件里,结果全是乱码😂。后来才知道要用UTF-8编码。希望这篇文章能帮你避开这些坑!

如果觉得有用,点赞收藏走一波!👍⭐


文档版本:v1.0
最后更新:2025-10-23
难度等级:⭐⭐⭐(中高级)