文章目录
1. 国际化基础概念
1.1 什么是国际化?
国际化(Internationalization,通常缩写为i18n,因为在"i"和"n"之间有18个字母)是指设计和开发软件时,使其能够适应不同语言和地区而无需进行工程上的更改。通过国际化,同一个应用程序可以在不同国家、使用不同语言的环境中运行,并为用户提供本地化的界面和内容。
1.2 国际化的重要性
- 扩大用户群体:支持多种语言可以使应用程序触达更广泛的用户
- 提升用户体验:用户使用母语操作应用会感到更舒适
- 符合法规要求:某些国家和地区有法律要求软件必须支持当地语言
- 提高市场竞争力:多语言支持是全球化产品的基本要求
- 降低维护成本:统一的国际化框架比为每种语言单独开发更高效
1.3 国际化相关术语
- 国际化(i18n):使软件适应不同语言和地区的过程
- 本地化(l10n):为特定地区调整软件的过程,包括翻译和文化适应
- 区域(Locale):特定的语言和地区组合,如zh_CN(中文-中国)
- 资源包(Resource Bundle):包含特定区域翻译文本的文件
- 消息源(Message Source):提供消息检索机制的组件
2. Spring Boot国际化架构
2.1 Spring Boot国际化核心组件
- MessageSource接口:Spring框架中处理国际化消息的核心接口
- ResourceBundleMessageSource:基于Java的ResourceBundle机制实现的MessageSource
- ReloadableResourceBundleMessageSource:支持资源文件热加载的MessageSource实现
- LocaleResolver:区域解析器,确定当前请求的Locale
- LocaleChangeInterceptor:区域变更拦截器,处理区域切换请求
2.2 国际化工作流程
- 用户发送请求到应用
- LocaleResolver确定当前请求的Locale
- 控制器处理请求,并使用MessageSource获取对应Locale的消息
- 消息被渲染到视图中
- 响应返回给用户,显示本地化内容
2.3 区域解析器类型
Spring Boot提供了多种区域解析器:
AcceptHeaderLocaleResolver:
- 基于HTTP请求头中的Accept-Language
- 无法更改Locale,只能使用浏览器设置
CookieLocaleResolver:
- 将Locale信息存储在Cookie中
- 可以在会话之间保持Locale设置
SessionLocaleResolver:
- 将Locale信息存储在HTTP会话中
- 仅在当前会话有效
FixedLocaleResolver:
- 使用固定的Locale,不允许更改
3. Spring Boot国际化基础配置
3.1 添加依赖
Spring Boot默认包含了国际化支持,无需额外依赖。如果使用Thymeleaf模板引擎,需要添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
3.2 创建国际化资源文件
在src/main/resources
目录下创建以下文件:
messages.properties(默认,当找不到特定语言的资源文件时使用):
greeting=你好
welcome.message=欢迎来到Spring Boot
language.change=更改语言
messages_en_US.properties(英文-美国):
greeting=Hello
welcome.message=Welcome to Spring Boot
language.change=Change Language
messages_zh_CN.properties(中文-中国):
greeting=你好
welcome.message=欢迎来到Spring Boot
language.change=更改语言
messages_ja_JP.properties(日语-日本):
greeting=こんにちは
welcome.message=Spring Bootへようこそ
language.change=言語を変更する
3.3 配置MessageSource
在application.properties
或application.yml
中配置:
spring:
messages:
basename: messages # 资源文件基础名(不包括语言和地区后缀)
encoding: UTF-8 # 资源文件编码
cache-duration: 3600 # 缓存时间(秒)
fallback-to-system-locale: false # 当找不到对应Locale的资源文件时,是否回退到系统Locale
3.4 配置LocaleResolver和LocaleChangeInterceptor
创建配置类:
@Configuration
public class InternationalizationConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); // 设置默认语言为简体中文
return resolver;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // 设置切换语言的参数名
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
4. 在不同场景中使用国际化
4.1 在Thymeleaf模板中使用国际化
创建一个简单的Thymeleaf模板(index.html
):
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot i18n</title>
</head>
<body>
<h1 th:text="#{greeting}">默认问候语</h1>
<p th:text="#{welcome.message}">默认欢迎消息</p>
<!-- 语言切换链接 -->
<div>
<a href="?lang=zh_CN" th:text="中文">中文</a>
<a href="?lang=en_US" th:text="English">English</a>
<a href="?lang=ja_JP" th:text="日本語">日本語</a>
</div>
</body>
</html>
4.2 在Java代码中使用国际化
创建一个控制器:
@Controller
public class HomeController {
@Autowired
private MessageSource messageSource;
@GetMapping("/")
public String home() {
return "index"; // 返回index.html模板
}
@GetMapping("/greeting-api")
@ResponseBody
public String getGreeting(Locale locale) {
// 在代码中获取国际化消息
return messageSource.getMessage("greeting", null, locale);
}
@GetMapping("/welcome-api")
@ResponseBody
public String getWelcome(Locale locale) {
// 使用参数化消息
return messageSource.getMessage("welcome.user",
new Object[]{"John"},
locale);
}
}
为参数化消息,添加以下内容到资源文件:
messages.properties:
welcome.user=欢迎,{0}!
messages_en_US.properties:
welcome.user=Welcome, {0}!
4.3 在RESTful API中使用国际化
创建一个REST控制器:
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
private MessageSource messageSource;
@GetMapping("/messages")
public Map<String, String> getMessages(
@RequestHeader(name = "Accept-Language", required = false) Locale locale) {
if (locale == null) {
locale = Locale.getDefault();
}
Map<String, String> messages = new HashMap<>();
messages.put("greeting", messageSource.getMessage("greeting", null, locale));
messages.put("welcome", messageSource.getMessage("welcome.message", null, locale));
return messages;
}
}
5. 高级国际化特性
5.1 参数化消息
资源文件中可以包含参数占位符:
messages.properties:
user.greeting=你好,{0}!今天是{1}。
items.count=你有{0}个物品在购物车中。
messages_en_US.properties:
user.greeting=Hello, {0}! Today is {1}.
items.count=You have {0} items in your cart.
在Java代码中使用:
@GetMapping("/parameterized")
@ResponseBody
public String getParameterizedMessage(Locale locale) {
// 格式化当前日期
String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
// 使用多个参数
return messageSource.getMessage("user.greeting",
new Object[]{"Alice", today},
locale);
}
5.2 复数形式处理
资源文件中可以定义不同数量的消息形式:
messages.properties:
cart.items=购物车中有{0}个商品
cart.items.zero=购物车是空的
cart.items.one=购物车中有1个商品
cart.items.many=购物车中有{0}个商品
messages_en_US.properties:
cart.items=There are {0} items in the cart
cart.items.zero=The cart is empty
cart.items.one=There is 1 item in the cart
cart.items.many=There are {0} items in the cart
在Java代码中使用:
@GetMapping("/cart/{count}")
@ResponseBody
public String getCartMessage(@PathVariable int count, Locale locale) {
String key;
if (count == 0) {
key = "cart.items.zero";
} else if (count == 1) {
key = "cart.items.one";
} else {
key = "cart.items.many";
}
return messageSource.getMessage(key, new Object[]{count}, locale);
}
5.3 自定义区域解析器
创建一个自定义的区域解析器,例如基于用户设置:
public class UserPreferenceLocaleResolver implements LocaleResolver {
@Autowired
private UserService userService; // 假设有一个用户服务
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 获取当前用户
User user = userService.getCurrentUser();
if (user != null && user.getPreferredLocale() != null) {
// 使用用户首选语言
return user.getPreferredLocale();
}
// 回退到请求头中的语言
String acceptLanguage = request.getHeader("Accept-Language");
if (StringUtils.hasText(acceptLanguage)) {
return Locale.forLanguageTag(acceptLanguage.split(",")[0]);
}
// 默认语言
return Locale.SIMPLIFIED_CHINESE;
}
@Override
public void setLocale(HttpServletRequest request,
HttpServletResponse response,
Locale locale) {
// 更新用户首选语言
User user = userService.getCurrentUser();
if (user != null) {
user.setPreferredLocale(locale);
userService.updateUser(user);
}
}
}
注册自定义解析器:
@Bean
public LocaleResolver localeResolver() {
return new UserPreferenceLocaleResolver();
}
6. 国际化最佳实践
6.1 资源文件组织
对于大型应用,可以按模块或功能区域组织资源文件:
resources/
├── i18n/
│ ├── common/
│ │ ├── messages.properties
│ │ ├── messages_en_US.properties
│ │ └── messages_zh_CN.properties
│ ├── validation/
│ │ ├── messages.properties
│ │ ├── messages_en_US.properties
│ │ └── messages_zh_CN.properties
│ └── user/
│ ├── messages.properties
│ ├── messages_en_US.properties
│ └── messages_zh_CN.properties
配置多个资源包:
spring:
messages:
basename: i18n/common/messages,i18n/validation/messages,i18n/user/messages
6.2 消息键命名约定
采用一致的命名约定:
- 使用点号分隔层次:
module.submodule.element
- 使用小写字母和点号:
user.profile.title
- 相关消息使用相同前缀:
error.validation.email
,error.validation.password
示例:
# 用户模块
user.profile.title=用户资料
user.profile.name=姓名
user.profile.email=电子邮件
# 错误消息
error.validation.required=此字段为必填项
error.validation.email=请输入有效的电子邮件地址
6.3 处理缺失的翻译
配置默认消息,当找不到对应的翻译时使用:
@GetMapping("/safe-message")
@ResponseBody
public String getSafeMessage(Locale locale) {
// 提供默认消息
return messageSource.getMessage(
"unknown.key",
null,
"This is a default message when translation is missing",
locale
);
}
或者在配置中设置使用消息代码作为默认消息:
spring:
messages:
use-code-as-default-message: true
7. 国际化高级配置
7.1 配置资源文件重载
在开发环境中,可以配置资源文件的自动重载,无需重启应用:
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheMillis(1000); // 缓存时间1秒,便于开发时测试
return messageSource;
}
7.2 配置多个资源包
当应用较大时,可以将资源文件分成多个包:
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasenames(
"classpath:messages/core",
"classpath:messages/validation",
"classpath:messages/ui"
);
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
7.3 国际化数据库内容
对于需要在数据库中存储的多语言内容,可以创建专门的实体和表:
@Entity
public class TranslatedContent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String contentKey; // 内容的唯一标识
@Column(nullable = false)
private String locale; // 语言代码
@Column(nullable = false, length = 4000)
private String content; // 翻译内容
// getters and setters
}
创建一个服务来获取翻译:
@Service
public class DatabaseMessageService {
@Autowired
private TranslatedContentRepository repository;
public String getMessage(String key, Locale locale) {
TranslatedContent content = repository.findByContentKeyAndLocale(
key, locale.toString());
if (content != null) {
return content.getContent();
}
// 回退到默认语言
content = repository.findByContentKeyAndLocale(key, "en_US");
return content != null ? content.getContent() : "???" + key + "???";
}
}
8. 国际化测试
8.1 单元测试国际化消息
创建测试类来验证国际化配置:
@SpringBootTest
public class InternationalizationTest {
@Autowired
private MessageSource messageSource;
@Test
public void testDefaultMessages() {
String greeting = messageSource.getMessage("greeting", null, Locale.SIMPLIFIED_CHINESE);
assertEquals("你好", greeting);
}
@Test
public void testEnglishMessages() {
String greeting = messageSource.getMessage("greeting", null, Locale.US);
assertEquals("Hello", greeting);
}
@Test
public void testParameterizedMessages() {
String message = messageSource.getMessage(
"welcome.user",
new Object[]{"John"},
Locale.US
);
assertEquals("Welcome, John!", message);
}
@Test
public void testFallbackMessages() {
// 测试不存在的键是否正确回退到默认消息
String message = messageSource.getMessage(
"nonexistent.key",
null,
"Default Message",
Locale.US
);
assertEquals("Default Message", message);
}
}
8.2 集成测试国际化控制器
测试控制器是否正确处理国际化:
@SpringBootTest
@AutoConfigureMockMvc
public class InternationalizationControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testDefaultLocale() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("欢迎来到Spring Boot")));
}
@Test
public void testEnglishLocale() throws Exception {
mockMvc.perform(
get("/")
.header("Accept-Language", "en-US")
)
.andExpect(status().isOk())
.andExpect(content().string(containsString("Welcome to Spring Boot")));
}
@Test
public void testLocaleChangeParameter() throws Exception {
mockMvc.perform(get("/?lang=en_US"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Welcome to Spring Boot")));
}
}
9. 常见问题和解决方案
9.1 字符编码问题
问题:资源文件中的非ASCII字符显示为乱码。
解决方案:
- 确保资源文件使用UTF-8编码保存
- 在IDE中设置正确的文件编码
- 在配置中指定编码:
spring:
messages:
encoding: UTF-8
9.2 资源文件未被加载
问题:应用无法找到资源文件。
解决方案:
- 检查资源文件命名是否正确(如
messages_zh_CN.properties
) - 确认资源文件位于正确的路径(默认是
src/main/resources
) - 验证配置中的basename是否正确:
spring:
messages:
basename: messages # 不要包含.properties后缀
9.3 区域切换不起作用
问题:点击语言切换链接后,语言没有变化。
解决方案:
- 确保已配置
LocaleChangeInterceptor
并添加到拦截器注册表 - 检查URL参数名是否与配置匹配(默认是
locale
,但上面示例中使用了lang
) - 验证
LocaleResolver
配置正确
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // 确保与URL参数一致
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
9.4 默认语言不正确
问题:应用启动时使用了错误的默认语言。
解决方案:
- 明确设置默认Locale:
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); // 设置默认语言
return resolver;
}
- 禁用回退到系统Locale:
spring:
messages:
fallback-to-system-locale: false
10. 实战示例:完整的国际化登录页面
10.1 资源文件
messages.properties:
login.title=登录系统
login.username=用户名
login.password=密码
login.remember=记住我
login.button=登录
login.error=用户名或密码错误
login.success=登录成功
login.language=语言
messages_en_US.properties:
login.title=Login System
login.username=Username
login.password=Password
login.remember=Remember Me
login.button=Sign In
login.error=Invalid username or password
login.success=Login successful
login.language=Language
10.2 控制器
@Controller
public class LoginController {
@Autowired
private MessageSource messageSource;
@GetMapping("/login")
public String loginPage() {
return "login";
}
@PostMapping("/login")
public String processLogin(
@RequestParam String username,
@RequestParam String password,
RedirectAttributes redirectAttributes,
Locale locale) {
// 简单的登录逻辑示例
if ("admin".equals(username) && "password".equals(password)) {
redirectAttributes.addFlashAttribute(
"message",
messageSource.getMessage("login.success", null, locale)
);
return "redirect:/dashboard";
} else {
redirectAttributes.addFlashAttribute(
"error",
messageSource.getMessage("login.error", null, locale)
);
return "redirect:/login";
}
}
@GetMapping("/dashboard")
public String dashboard() {
return "dashboard";
}
}
10.3 Thymeleaf模板
login.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="#{login.title}">Login</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.error {
color: red;
margin-bottom: 15px;
}
.success {
color: green;
margin-bottom: 15px;
}
.language-selector {
margin-top: 20px;
}
</style>
</head>
<body>
<h1 th:text="#{login.title}">Login System</h1>
<div class="error" th:if="${error}" th:text="${error}"></div>
<div class="success" th:if="${message}" th:text="${message}"></div>
<form method="post" th:action="@{/login}">
<div class="form-group">
<label for="username" th:text="#{login.username}">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password" th:text="#{login.password}">Password</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="remember">
<span th:text="#{login.remember}">Remember Me</span>
</label>
</div>
<div class="form-group">
<button type="submit" th:text="#{login.button}">Sign In</button>
</div>
</form>
<div class="language-selector">
<span th:text="#{login.language}">Language</span>:
<a href="?lang=zh_CN">中文</a> |
<a href="?lang=en_US">English</a>
</div>
</body>
</html>
dashboard.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Dashboard</title>
</head>
<body>
<h1>Dashboard</h1>
<div class="success" th:if="${message}" th:text="${message}"></div>
<p>You are logged in!</p>
<div class="language-selector">
<span th:text="#{login.language}">Language</span>:
<a href="?lang=zh_CN">中文</a> |
<a href="?lang=en_US">English</a>
</div>
</body>
</html>
11. 总结
Spring Boot提供了强大而灵活的国际化支持,通过本指南我们学习了:
- 国际化的基本概念和重要性
- Spring Boot国际化架构和核心组件
- 基础配置和资源文件创建
- 在不同场景中使用国际化(模板、代码、API)
- 高级特性(参数化消息、复数形式、自定义解析器)
- 最佳实践和资源文件组织
- 高级配置选项
- 测试国际化功能
- 常见问题和解决方案
- 完整的实战示例
通过合理使用Spring Boot的国际化功能,可以轻松构建多语言应用,提升全球用户的体验。关键是理解核心组件(如MessageSource和LocaleResolver)的工作原理,并采用一致的资源文件组织和命名约定。