Spring Boot i18n 国际化实战:从资源文件到多语言接口完整指南

简介: Spring Boot i18n 怎么配?基于 3.x 带你配置 MessageSource 与 LocaleResolver,让后端接口返回多语言消息

大家好,我是程序员天天困。

想象一个很常见的场景:团队把 App 推向东南亚市场,海外用户点登录失败,屏幕上弹出来的提示却是中文的「用户名或密码错误」。开发在本地测试全绿,翻出代码一看——错误文案直接写死在 Java 类里。要改文案?重新打包部署吧。

说真的,只要业务有出海计划,后端迟早要碰 Spring Boot i18n。这篇文章就把这套东西从头到尾讲清楚。点个收藏,我们开始。

一、i18n 是什么,为什么后端也需要它

i18n(internationalization,国际化):单词 internationalization 首尾字母 i 和 n 之间有 18 个字母,所以简写为 i18n。指的是在产品设计阶段就让代码具备适配多种语言和地区的能力,而不是事后硬改。你可以理解为「给软件预留多语言插槽」。

和它一起出现的还有 l10n(localization,本地化):i18n 是搭好插槽,l10n 是往插槽里填具体某种语言的翻译和格式(日期、货币、数字符号)。

很多人觉得 i18n 是前端的事,后端返回错误码、前端自己映射文案就行。这种做法在纯 App 场景能跑,但一旦对接第三方回调、开放平台 API、服务端推送消息(邮件、短信、Webhook),文案是从后端直接发出去的,前端根本接不住。

我列几个后端必须做 i18n 的典型场景:

  1. 开放 API:第三方开发者调用你的接口,错误信息得是对方能看懂的语言。
  2. 服务端推送:注册邮件、登录验证码、订单状态短信,这些文案由后端拼接。
  3. 统一错误码体系:后端维护错误码和消息模板,多端共用,避免 iOS、Android、Web 各翻译一遍还对不上。
  4. 日志与审计:部分合规场景要求操作日志按用户地区语言留痕。

说白了,只要文案是从你的服务进程里产生并发给用户的,后端就绕不开 i18n。

二、Spring Boot i18n 的三大核心组件

Spring Boot 处理多语言消息就靠三样东西,记住「查字典」这个类比就够了:

组件 角色 字典类比
MessageSource 消息源,加载并管理多语言资源文件 字典本身
LocaleResolver 区域解析器,决定当前请求用哪种语言 判断读者要查哪种语言版本
LocaleChangeInterceptor 语言切换拦截器,从请求参数里切换语言 读者翻字典前指定语言

MessageSource(消息源):Spring 定义的一个接口,负责根据消息键(key)和区域(Locale)解析出对应文案。Spring Boot 通过 MessageSourceAutoConfiguration 自动装配它,你只需要告诉它资源文件在哪、用什么编码。

LocaleResolver(区域解析器):决定「这次请求到底用什么语言」的策略组件。它可以从 HTTP 头、Cookie、Session 甚至固定值里解析出一个 Locale 对象。DispatcherServlet 处理请求时会调用它,把结果存进 LocaleContextHolder,后续整条链路都能取到。

LocaleChangeInterceptor(语言切换拦截器):一个可选的拦截器,允许通过请求参数(比如 ?lang=en_US)动态切换语言。它本质上是调用 LocaleResolver.setLocale() 把新语言写回 Cookie 或 Session。

整个流程其实就是三步:请求进来 → LocaleResolver 判断语言 → MessageSource 按 key + Locale 查文案。没有任何魔法。

三、从零搭建一个多语言项目

光说不练假把式。下面基于 Spring Boot 3.x 搭一个最小可用的多语言后端,用到的代码都是标准写法。

1、准备资源文件

src/main/resources/ 下建一个 i18n/ 目录,放四份资源文件:

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

messages.properties 是默认文件,当请求的语言找不到对应翻译时,会回退到这里。这个文件必须存在,否则启动和运行时都会有警告。

每份文件内容长这样(默认文件建议用英文兜底),key 保持一致,value 按语言填:

messages.properties

user.welcome=Welcome
user.login.fail=Invalid username or password
user.notfound=User not found: {0}

messages_zh_CN.properties

user.welcome=欢迎回来
user.login.fail=用户名或密码错误
user.notfound=用户不存在:{0}

messages_en_US.properties

user.welcome=Welcome back
user.login.fail=Invalid username or password
user.notfound=User not found: {0}

messages_ja_JP.properties

user.welcome=おかえりなさい
user.login.fail=ユーザー名またはパスワードが正しくありません
user.notfound=ユーザーが見つかりません:{0}

{0} 是占位符,运行时可以用参数替换,后面会演示。

2、配置 application.yml

spring:
  messages:
    # 资源文件基础名,不要写 .properties 后缀
    basename: i18n/messages
    encoding: UTF-8
    # 资源文件缓存秒数;开发时设为 0 方便热更新,生产给 3600
    cache-duration: 3600
    # 找不到对应语言的资源文件时,不回退到系统 Locale,直接用 messages.properties
    fallback-to-system-locale: false

basename 支持配置多个,用逗号分隔,比如 i18n/messages,i18n/errors。一般项目一个就够。

3、配置 LocaleResolver 和拦截器

新建一个配置类:

@Configuration
public class I18nConfig implements WebMvcConfigurer {
   

    // Bean 名必须叫 localeResolver,Spring 按此名查找
    @Bean
    public LocaleResolver localeResolver() {
   
        CookieLocaleResolver resolver = new CookieLocaleResolver("lang");
        resolver.setCookieMaxAge(Duration.ofDays(30));
        resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return resolver;
    }

    // 支持通过 ?lang=en_US 切换语言
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
   
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        return interceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
   
        registry.addInterceptor(localeChangeInterceptor());
    }
}

这里有个坑:Bean 名必须localeResolver。Spring 在 DispatcherServlet 里按这个名字去容器里找,你要是写成 cookieLocaleResolver(),它找不到就会回退到默认的 AcceptHeaderLocaleResolver,配置直接失效。

4、封装一个消息工具类

直接在业务代码里每次注入 MessageSource 再传三个参数有点啰嗦,封装一下:

@Component
public class MessageSourceUtils {
   

    private final MessageSource messageSource;

    public MessageSourceUtils(MessageSource messageSource) {
   
        this.messageSource = messageSource;
    }

    /**
     * 取当前请求 Locale 对应的文案
     */
    public String get(String key, Object... args) {
   
        return messageSource.getMessage(key, args, LocaleContextHolder.getLocale());
    }

    /**
     * 指定 Locale 取文案(后台任务、异步线程里用)
     */
    public String get(String key, Locale locale, Object... args) {
   
        return messageSource.getMessage(key, args, locale);
    }
}

LocaleContextHolder 里的 Locale 是 DispatcherServlet 在请求进来时通过 LocaleResolver 设进去的,整条请求链路都能取。但要注意:异步线程里它是空的,因为 LocaleContextHolder 底层用的是 ThreadLocal。异步场景必须显式传 Locale,这也是我留第二个重载方法的原因。

5、在 Controller 里使用

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

    private final MessageSourceUtils messageSource;

    public UserController(MessageSourceUtils messageSource) {
   
        this.messageSource = messageSource;
    }

    @GetMapping("/welcome")
    public Result<String> welcome() {
   
        return Result.ok(messageSource.get("user.welcome"));
    }

    @GetMapping("/{id}")
    public Result<UserVO> getUser(@PathVariable Long id) {
   
        UserVO user = userService.getById(id);
        if (user == null) {
   
            // 只抛错误码和参数,文案由全局异常处理器统一翻译
            throw new BusinessException("user.notfound", id);
        }
        return Result.ok(user);
    }
}

启动项目,默认请求(Cookie 没设置时走 defaultLocale=zh_CN):

curl http://localhost:8080/api/users/welcome

返回:欢迎回来

通过参数切到英文:

curl "http://localhost:8080/api/users/welcome?lang=en_US"

返回:Welcome back

切到日文:

curl "http://localhost:8080/api/users/welcome?lang=ja_JP"

返回:おかえりなさい

?lang=en_US 第一次请求时,拦截器会把 Locale 写进名为 lang 的 Cookie,之后不带参数也会记住语言,30 天有效。

6、全局异常处理里用 i18n

更常见的做法是业务代码只抛错误码,文案翻译交给全局异常处理器:

@RestControllerAdvice
public class GlobalExceptionHandler {
   

    private final MessageSource messageSource;

    public GlobalExceptionHandler(MessageSource messageSource) {
   
        this.messageSource = messageSource;
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<Result<Void>> handleBusiness(BusinessException e) {
   
        // 优先用错误码当 key 去资源文件查;查不到用异常自带的默认消息
        String message = messageSource.getMessage(
                e.getCode(),
                e.getArgs(),
                e.getDefaultMessage(),
                LocaleContextHolder.getLocale()
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(Result.fail(e.getCode(), message));
    }
}

这样业务层只管抛 new BusinessException("user.notfound", id),文案怎么显示交给统一的处理器。错误码直接作为资源文件的 key,规范且好维护。

四、四种 LocaleResolver 怎么选

Spring 内置了四种 LocaleResolver,各有适用场景,别一上来就抄 Cookie 方案。

实现 语言来源 持久化 适用场景
AcceptHeaderLocaleResolver 请求头 Accept-Language 无(每次按头解析) 纯 API 服务、对接第三方、无登录态
CookieLocaleResolver 指定 Cookie 浏览器端持久 前后端分离、未登录也要记语言
SessionLocaleResolver HttpSession 会话级 传统服务端渲染、登录后定语言
FixedLocaleResolver 写死一个 Locale 永不变 测试、强制单语言的内部系统

我的选型建议很直接:

  • 纯后端 API,不维护用户语言偏好:用默认的 AcceptHeaderLocaleResolver,啥都不用配。客户端(浏览器、App)会自动带 Accept-Language 头。
  • 需要记住用户选择,且未登录也要生效:用 CookieLocaleResolver。这也是最通用的方案。
  • 登录后由用户中心统一下发语言:用 SessionLocaleResolver 或干脆自定义一个从用户信息里读 Locale 的解析器。
  • 想同时支持 Cookie 和 Header 兜底,可以自定义 LocaleResolver,先读 Cookie,Cookie 没有再回退到 Accept-Language 头。

注意 AcceptHeaderLocaleResolver 不支持通过拦截器切换语言——它的 setLocale() 方法会直接抛 UnsupportedOperationException,因为 HTTP 头是客户端发的,服务端改不了。如果你需要 ?lang=xxx 切换,必须换成 Cookie 或 Session 方案。

五、踩坑记录与最佳实践

实际项目里有几个高频坑,挑最容易踩的说。

1)编码问题:properties 文件乱码

这是最经典的坑。Java 9 之前 JDK 读取 .properties 文件默认按 ISO-8859-1 编码,中文只能写成 \uXXXX 转义;Java 9 起 PropertyResourceBundle 默认改成了 UTF-8,但老项目、老服务器上的乱码问题大多源自这。Spring Boot 3.x 要求 JDK 17+,默认已经是 UTF-8,但显式配置一下更稳妥:

spring:
  messages:
    encoding: UTF-8

另外,IDEA 里也要把 properties 文件的编码对齐(Settings → Editor → File Encodings → Default encoding for properties files 选 UTF-8;老项目如果还在用 ISO-8859-1,可以勾选 Transparent native-to-ascii conversion,IDEA 会自动在编辑时显示原文、保存时写转义码)。文件本身的编码和 Spring 配置里声明的编码要一致,一头 UTF-8 一头 ISO-8859-1 必乱。

2)key 的命名要规范

别图省事用 msg1msg2 这种名字。推荐按「模块.场景.含义」分层:

user.login.fail=用户名或密码错误
user.register.email.duplicate=该邮箱已被注册
order.pay.timeout=支付超时,请重试
system.error.internal=系统繁忙,请稍后再试

好处是 IDE 里搜 user. 就能把用户模块所有文案找出来,翻译人员也好按文件分工。

3)用占位符而不是字符串拼接

不同语言语序不一样。中文说「用户 123 不存在」,日语可能是「ユーザー 123 が見つかりません」,数字位置不同。用 {0} 占位符由 MessageFormat 去拼,能保证语序正确:

user.notfound=用户不存在:{0}
messageSource.get("user.notfound", id);

千万别自己用 String.format 或字符串 + 拼,翻译的时候语序全乱。

4)找不到 key 怎么办

MessageSource.getMessage() 默认找不到 key 会抛 NoSuchMessageException。生产环境因为漏配一个翻译就 500 不值得。前面全局异常处理器里用的四参版本:

messageSource.getMessage(code, args, defaultMessage, locale);

第三个参数 defaultMessage 就是兜底文案,查不到 key 时返回它,不会抛异常。建议线上统一用这个版本。

可能有人会问:前端已经做了 i18n,后端还有必要做吗?

看你的文案在哪产生。如果所有提示都是前端本地映射,后端只返回错误码,那后端确实可以不做;但邮件、短信、第三方回调、文件导出这些由后端直接产出文案的场景,后端不做就没法多语言。两者不是互斥的,是分工问题。

六、小结

Spring Boot i18n 没有任何黑魔法,本质就是「按 Locale 查 properties 文件」。记住三个要点:

  1. MessageSource 管字典——配置 basename 和编码,资源文件按 messages_{locale}.properties 命名。
  2. LocaleResolver 定语言——纯 API 用 Header,要记住偏好就用 Cookie,别让 Bean 名写错。
  3. 业务层只抛错误码——文案翻译交给全局异常处理器,用默认消息兜底,避免漏配翻译导致 500。

Spring Boot i18n 代码不复杂,真正的工作量在翻译和 key 的规范管理上。建议项目初期就把 i18n 搭好,别等出海了再回头改硬编码字符串——那时候散落各处的文案能改到你怀疑人生。


我是程序员天天困,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:踩过最离谱的 i18n 坑是什么?

相关文章
|
6天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
1616 116
|
7天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
1096 5
|
13天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1954 9
阿里云联动百位企业安全专家,共识Agent防御最佳实践
|
7天前
|
编解码 人工智能 安全
2核4G/4核8G/8核16G阿里云服务器如何选择实例?经济型e、通用算力型u2i与计算型c9i选哪个?
本文介绍了阿里云2核4G、4核8G、8核16G三档主流配置下经济型e、通用算力型u2i和计算型c9i三种实例的最新活动价格与适用场景。同配置下三者价差显著,以2核4G为例,经济型e低至599.93元/年,计算型c9i则高达1742.08元/年。文章详细解析了各实例的性能定位:经济型e适合轻负载入门场景,u2i兼顾稳定算力与性价比,c9i凭借第9代至强处理器与芯片级安全能力支撑高性能业务。同时提示用户可叠加满减优惠券享受折上折,建议根据业务负载与预算综合决策。
538 112
|
19天前
|
人工智能 前端开发 Linux
Codex 桌面版安装 + CC Switch 接入第三方 API 完整教程(2026 最新)
2026最新教程:手把手教你安装Codex桌面版,通过CC Switch v3.17.0一键接入Fenno等国产API(兼容OpenAI Responses格式),跳过账号登录,完整启用代码审查、多步任务与上下文感知功能。零基础友好,全程图文实操。(239字)
2745 4
|
11天前
|
存储 人工智能 关系型数据库
阿里云AI产品与云产品最新组合套餐:Token Plan、AI coding及云服务器和建站等组合优惠价
阿里云推出全新“算力+模型+应用”一站式云与AI组合套餐活动,覆盖从个人开发者到中大型企业的全场景需求。核心亮点为分三档定价的Token Plan订阅服务,支持Qwen3.8-Max-Preview大模型调用,错峰时段最低可享0.2折优惠。活动同步推出AI Coding、智能体部署、云电脑托管、0代码建站等十余类场景化组合,搭配99元/年的普惠云服务器、88元/年的入门数据库等经典特惠产品,还为企业提供1V1定制化AI转型方案,大幅降低了不同用户群体拥抱AI的技术门槛与采购成本。
730 111
|
21天前
|
人工智能 JSON 安全
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
阿里云AI安全产品联动防御Fastjson攻击
2652 13
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
|
7天前
|
人工智能 JSON Shell
2026AI漫剧本地全开源方案(附各个软件模型链接),8G显卡也能流畅运行
这是一套完全本地化部署的AI漫剧生成技术链路:涵盖LLM剧本分镜生成、FLUX文生图(IP-Adapter人脸锁定)、StoryDiffusion时序连贯控制、LTX-2.3唇形同步视频生成,及ComfyUI全流程调度。零云端费用,仅耗硬件算力,单集2–4小时可产出竖屏短视频,适配抖音/B站分发。
|
5天前
|
人工智能 API 开发工具
2026 零基础本地 AI 漫剧完整实操教程(8G 笔记本显卡可用|附可直接复制命令与代码)
本方案提供完全离线、本地运行的漫剧全自动制作流程:RTX3060/4050 8G显卡即可驱动,涵盖Qwen写分镜→ComfyUI统一角色绘图→LTX2.3图生微动画→Qwen3-TTS本地配音→FFmpeg自动合成,全程无水印、免API、不限次。专为低显存优化,解决变脸、闪烁、爆内存三大痛点。(239字)

热门文章

最新文章