Spring Boot 国际化(i18n)完全指南

Spring Boot 国际化(i18n)完全指南

一、什么是 i18n?

i18n 是 “internationalization” 的缩写(i 和 n 之间有 18 个字母),核心思想是:把用户可见的文本从代码中抽离到外部资源文件,运行时根据语言环境动态加载对应文件,实现多语言切换而无需改代码。


二、核心机制

工作原理

客户端请求(携带 Accept-Language: en_US)
        ↓
LocaleResolver 解析语言环境 → Locale.US
        ↓
MessageSource 按优先级查找资源文件:
  1. messages_en_US.properties  ← 精确匹配
  2. messages_en.properties     ← 语言匹配
  3. messages.properties        ← 默认兜底
        ↓
找到 key 对应的 value → 返回文案

三个核心组件

组件职责
MessageSource负责根据 key + Locale 加载对应语言的文本
LocaleResolver负责从请求中解析出用户的语言偏好
资源文件 (*.properties)存储各语言的 key-value 文案

LocaleResolver 的常见实现

类型判断依据适用场景
AcceptHeaderLocaleResolverHTTP 请求头 Accept-LanguageAPI 服务、微服务
CookieLocaleResolverCookie 中存储的语言偏好Web 应用
SessionLocaleResolverSession 中存储的语言偏好传统 Web 应用
FixedLocaleResolver固定语言,不可更改单语言应用

三、资源文件规则

命名规范

{basename}.properties              ← 默认(兜底)
{basename}_{language}.properties   ← 按语言
{basename}_{language}_{country}.properties  ← 按语言+地区

查找优先级(以 Locale("zh", "CN") 为例)

1. messages_zh_CN.properties   ← 最精确
2. messages_zh.properties      ← 语言级别
3. messages.properties         ← 默认兜底

如果高优先级文件中没有某个 key,会自动 fallback 到低优先级文件。

文件编码

  • .properties 文件默认使用 ISO-8859-1 编码
  • 中文需要写成 Unicode 转义形式:\u4F1A\u5458 = “会员”
  • 配置 encoding: UTF-8 后可直接写中文(Spring Boot 推荐方式)

四、配置方式

方式一:application.yml 配置(推荐)

spring:
  messages:
    basename: i18n/messages          # 资源文件路径前缀(相对于 classpath)
    cache-duration: 3s               # 缓存时长,开发时设短方便热更新
    encoding: UTF-8                  # 文件编码
    fallback-to-system-locale: true  # 是否回退到系统默认语言
配置项说明
basename文件路径前缀,多个用逗号分隔:i18n/messages,i18n/errors
cache-duration缓存刷新间隔,生产环境可设大(如 1h),开发设小(如 3s
encoding设为 UTF-8 后 properties 文件可直接写中文
fallback-to-system-locale找不到对应语言文件时是否用 JVM 默认 Locale

方式二:Java Config 配置

@Configuration
public class I18nConfig {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setBasename("i18n/messages");
        source.setDefaultEncoding("UTF-8");
        source.setCacheSeconds(3);
        return source;
    }

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

两种方式效果相同。当 Java Bean 和 YAML 同时配置时,Java Bean 优先。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

五、完整通用示例

以一个用户注册模块为例,演示完整的 i18n 实现。

1. 目录结构

src/main/resources/
├── application.yml
└── i18n/
    ├── messages.properties           # 默认(中文)
    ├── messages_zh_CN.properties     # 中文(可为空,默认已是中文)
    └── messages_en_US.properties     # 英文

2. 资源文件内容

messages.properties(默认,中文兜底):

# 用户模块
user.register.username.empty=用户名不能为空
user.register.username.duplicate=用户名"{0}"已被注册
user.register.password.too.short=密码长度不能少于{0}位
user.register.email.invalid=邮箱格式不正确
user.register.success=注册成功,欢迎{0}!

# 通用
common.param.invalid=参数校验失败
common.system.error=系统繁忙,请稍后重试

messages_en_US.properties(英文):

# User module
user.register.username.empty=Username cannot be empty
user.register.username.duplicate=Username "{0}" is already taken
user.register.password.too.short=Password must be at least {0} characters
user.register.email.invalid=Invalid email format
user.register.success=Registration successful, welcome {0}!

# Common
common.param.invalid=Parameter validation failed
common.system.error=System is busy, please try again later

messages_zh_CN.properties(留空即可,fallback 到默认文件):

# 留空,默认文件已是中文

3. application.yml

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

4. 配置类

@Configuration
public class I18nConfig {

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

5. 封装工具类(方便全局调用)

@Component
public class I18nUtil {

    private static MessageSource messageSource;

    @Resource
    public void setMessageSource(MessageSource messageSource) {
        I18nUtil.messageSource = messageSource;
    }

    /**
     * 获取国际化消息.
     *
     * @param key  消息 key
     * @param args 占位符参数
     * @return 对应语言的消息文本
     */
    public static String getMessage(String key, Object... args) {
        Locale locale = LocaleContextHolder.getLocale();
        return messageSource.getMessage(key, args, locale);
    }

    /**
     * 获取国际化消息(带默认值).
     */
    public static String getMessage(String key, String defaultMsg, Object... args) {
        Locale locale = LocaleContextHolder.getLocale();
        return messageSource.getMessage(key, args, defaultMsg, locale);
    }
}

6. Service 层使用

@Slf4j
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    public void register(UserRegisterRequest request) {
        // 校验用户名
        if (StringUtils.isBlank(request.getUsername())) {
            throw new BusinessException(I18nUtil.getMessage("user.register.username.empty"));
        }

        // 校验密码长度(带占位符参数)
        if (request.getPassword().length() < 8) {
            throw new BusinessException(
                I18nUtil.getMessage("user.register.password.too.short", 8));
        }

        // 校验用户名重复
        if (userMapper.existsByUsername(request.getUsername())) {
            throw new BusinessException(
                I18nUtil.getMessage("user.register.username.duplicate", request.getUsername()));
        }

        // 保存用户
        userMapper.insert(buildUser(request));
        log.info("用户注册成功, username={}", request.getUsername());
    }
}

7. Controller 层

@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {

    @Resource
    private UserService userService;

    @PostMapping("/register")
    public Result<String> register(@RequestBody UserRegisterRequest request) {
        try {
            userService.register(request);
            String successMsg = I18nUtil.getMessage("user.register.success", request.getUsername());
            return Result.success(successMsg);
        } catch (BusinessException e) {
            return Result.fail(e.getMessage());
        } catch (Exception e) {
            log.error("用户注册异常, error={}", e.getMessage(), e);
            return Result.fail(I18nUtil.getMessage("common.system.error"));
        }
    }
}

8. 运行效果

中文请求(或不带 Accept-Language):

POST /api/user/register
Body: {"username": "", "password": "123"}

响应:{"code": 500, "message": "用户名不能为空"}

英文请求

POST /api/user/register
Headers: Accept-Language: en_US
Body: {"username": "", "password": "123"}

响应:{"code": 500, "message": "Username cannot be empty"}

带占位符参数

POST /api/user/register
Headers: Accept-Language: en_US
Body: {"username": "tom", "password": "123"}

响应:{"code": 500, "message": "Password must be at least 8 characters"}

六、占位符语法

资源文件支持 {0}, {1}, {2} 等位置占位符:

# {0} = 用户名, {1} = 日期
user.welcome=欢迎{0},您的账号创建于{1}

代码中传参:

messageSource.getMessage("user.welcome", new Object[]{"张三", "2026-06-16"}, locale);
// 输出:欢迎张三,您的账号创建于2026-06-16

七、参数校验 + i18n 整合

Spring Validation 的注解也支持 i18n,在 message 属性中引用资源 key:

@Data
public class UserRegisterRequest {

    @NotBlank(message = "{user.register.username.empty}")
    private String username;

    @Size(min = 8, message = "{user.register.password.too.short}")
    private String password;

    @Email(message = "{user.register.email.invalid}")
    private String email;
}

注意:用 {} 包裹 key 名。Spring 会自动从 MessageSource 查找对应文案。


八、常见问题

Q1: 为什么 zh_CN 文件是空的?

默认文件(messages.properties)里已经写了中文。查找链是 zh_CN → 默认,所以不需要重复写一遍。只有当默认文件用英文、要额外支持中文时才需要填 zh_CN。

Q2: cache-duration: 3s 生产环境要改吗?

要。开发时设 3 秒方便调试,生产环境建议设大一些(如 1h-1 表示永不刷新),避免频繁读文件影响性能。

Q3: 一个项目能有多个 basename 吗?

可以,逗号分隔:

spring:
  messages:
    basename: i18n/messages,i18n/errors,i18n/validation

对应的文件结构:

i18n/
├── messages.properties
├── messages_en_US.properties
├── errors.properties
├── errors_en_US.properties
├── validation.properties
└── validation_en_US.properties

Q4: 找不到 key 时会怎样?

默认抛出 NoSuchMessageException。可以用带默认值的方法避免:

messageSource.getMessage("some.key", null, "默认文案", locale);

Q5: 如何动态切换语言(不靠请求头)?

使用 LocaleChangeInterceptor,通过 URL 参数切换:

@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
    LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
    interceptor.setParamName("lang");  // ?lang=en_US
    return interceptor;
}

访问 http://localhost:8080/api/user?lang=en_US 即可切换到英文。


九、总结

要素作用
messages.properties默认兜底文案
messages_{locale}.properties特定语言文案
MessageSource根据 key + locale 查找文案的核心接口
LocaleResolver从请求中解析用户语言偏好
{0} 占位符支持动态参数替换
cache-duration控制文件缓存刷新频率
@NotBlank(message = "{key}")校验注解直接对接 i18n
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霸道流氓气质

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值