国际化实现
国际化是一种多语言支持的技术,它允许一个应用程序同时支持多种语言。应用程序需要根据根据用户所在的地区的习惯,用户需求和当地的语言响应相应的结果,这个结果可能包括 UI设计,语言。
本文要实现的是基于地区后端接口返回的文字为该对应的语言。效果如下:
js
// language = zh-CN
result = {
code : 200,
message : 请求成功,
}
// language = en-US
result = {
code : 200,
message : request sussessful,
}
国际化实现总结为以下三步:
- 获取系统当前的环境
- 根据环境获取对应的语言资源
- 返回资源结果
系统环境获取
系统环境主要是指获取系统运行时的语言或者地区,本文的核心思想是前端将系统此时的语言信息放入请求头中和请求一并传给后端。
对于前端来说,当首次加载页面时根据用户当前的系统语言或浏览器语言进行初始设置,通过页面上设置按钮控制语言切换,示意的代码如下:
js
// 首次加载页面获取浏览器界面语言,navigator.language 返回一个表示用户偏好语言(通常是浏览器界面语言)的字符串
let language = navigator.language;
// 获取页面中用户设置的语言
const languageValue = document.querySelector("#language-set").innerText;
language = languageValue || navigator.language;
const xhr = new XMLHttpRequest();
// 设置请求头
xhr.setRequestHeader("language",language);
对于后端来说,则需要从请求头中取出 language 的值,最简单的方式是通过参数注解 @RequestHeader
来获取请求头中的特定字段值,如下:
java
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.stereotype.Controller;
@Controller
public class MyController {
@GetMapping("/some-endpoint")
public String getLanguage(@RequestHeader("language") String language) {
// 处理language值
System.out.println("Detected language: " + language);
return "Some Response";
}
}
通过以上方式每个接口都需要重复如上过程,代码冗余度太高,可以结合 ThreadLocal
和过滤器(Filter
)来保存请求头中的language属性,来确保这个属性在整个请求处理生命周期内对当前线程可见。以下是一个简单的示例:
首先创建一个用于存储 language 属性的 ThreadLocal
类:
java
import java.lang.ThreadLocal;
public class RequestLanguageHolder {
private static final ThreadLocal<String> LANGUAGE_HOLDER = new ThreadLocal<>();
public static void setLanguage(String language) {
LANGUAGE_HOLDER.set(language);
}
public static String getLanguage() {
return LANGUAGE_HOLDER.get();
}
public static void removeLanguage() {
LANGUAGE_HOLDER.remove();
}
}
接下来创建一个过滤器,用于在每次请求时从请求头中取出language并保存到ThreadLocal中:
java
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class LanguageFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 获取请求头中的language属性
String language = httpServletRequest.getHeader("language");
// 将语言设置到ThreadLocal中
if (language != null) {
RequestLanguageHolder.setLanguage(language);
}
try {
// 继续执行过滤链
chain.doFilter(request, response);
} finally {
// 在请求处理完成后移除ThreadLocal中的language
RequestLanguageHolder.removeLanguage();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
然后需要将这个过滤器注册到Spring Boot应用中,可以在配置类里添加如下代码:
java
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<LanguageFilter> languageFilterRegistration() {
FilterRegistrationBean<LanguageFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new LanguageFilter());
// 设置过滤器应用于所有请求
registration.addUrlPatterns("/*");
return registration;
}
}
语言资源获取
Spring 的国际化,实际上就是在 Java 国际化(可以看相关知识点)的基础之上做了一些封装,提供了一些新的能力。
使用 Spring 国际化需要我们首先提供一个 MessageSource
实例,常用的 MessageSource
实例是 ReloadableResourceBundleMessageSource
,这是一个具备自动刷新能力的 MessageSource
,即,用户修改了配置文件之后,在项目不重启的情况下,新的配置就能生效。
java
@ConfigurationProperties("i18n")
@Data
public class MessageSourceProperties {
// 基础文件名
private String basename = "i18n/messages";
// 默认编码
private String defaultEncoding = "UTF-8";
// 是否使用代码作为默认消息
private boolean useCodeAsDefaultMessage = true;
}
配置方式很简答,我们只需要将这个 Bean 注册到 Spring 容器中:
java
@Configuration
// 将与配置文件绑定好的某个类注入到容器中,使其生效
@EnableConfigurationProperties(MessageSourceProperties.class)
public class MessageSourceAutoConfiguration {
private MessageSourceProperties messageSourceProperties;
// 构建该自动配置类时将与配置文件绑定的配置类作为入参传递进去
public MessageSourceAutoConfiguration(MessageSourceProperties messageSourceProperties) {
this.messageSourceProperties = messageSourceProperties;
}
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
Locale.setDefault(Locale.CHINA);
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setDefaultEncoding(messageSourceProperties.getDefaultEncoding());
// 设置是否回退到系统本地
messageSource.setFallbackToSystemLocale(false);
// 设置是否使用代码作为默认消息
messageSource.setUseCodeAsDefaultMessage(messageSourceProperties.isUseCodeAsDefaultMessage());
//设置国际化文件存储路径和名称 i18n目录,messages文件名
messageSource.setBasename(messageSourceProperties.getBasename());
return messageSource;
}
}
对获取资源文件内容的方法进行封装再用,封装类似下面这样:
java
import com.auth.cloud.i18n.enums.LanguageEnum;
import org.springframework.context.MessageSource;
import org.springframework.lang.Nullable;
import java.util.Locale;
@Component
public class I18nUtil implements MessageSourceAware{
private static MessageSource messageSource;
private static Locale getLanguage(LanguageEnum language) {
return new Locale(language.getLanguage(), language.getCountry());
}
public static String get(String code) {
return messageSource.getMessage(code, null, Locale.getDefault());
}
public static String get(String code, @Nullable Object[] args) {
return messageSource.getMessage(code, args, Locale.getDefault());
}
public static String get(String code, LanguageEnum language) {
Locale lang = getLanguage(language);
System.out.println(lang);
return messageSource.getMessage(code, null, lang);
}
public static String get(String code, @Nullable Object[] args, LanguageEnum language) {
Locale lang = getLanguage(language);
return messageSource.getMessage(code, args, lang);
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
}
这个工具类实现了 MessageSourceAware
接口,这样就可以拿到 messageSource
对象,然后将 getMessage 方法进行封装。
除了通过 MessageSourceAware
接口拿到 messageSource
对象外,还可以通过 applicationContext.getBean
来获得。
java
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
@Override
//设置Spring上下文
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
//判断SpringContextUtil.applicationContext是否为空
if (SpringContextUtilsAutoConfiguration.applicationContext == null) {
//如果为空,将applicationContext赋值给SpringContextUtil.applicationContext
SpringContextUtilsAutoConfiguration.applicationContext = applicationContext;
}
}
}
java
@Component
public class I18nUtil{
private static class Lazy {
// 使用懒加载方式实例化MessageSource对象
private static final MessageSource messageSource = SpringContextUtil.getBean(MessageSource.class);
}
private static MessageSource getInstance() {
return Lazy.messageSource;
}
}
测试代码及结果如下:
java
public CommonResult<String> testI18n(String role) {
return CommonResult.success(I18nUtil.get("test", new String[]{role}, LanguageEnum.ENGLISH));
}
java
@Test
public void roleTest() {
CommonResult<String> role1 = roleController.testI18n("角色");
log.info(role1);
//英文的结果: test,char:角色
//中文的结果: 测试test,字符:角色
}
相关知识点
java 的国际化用法
Java 的国际化(Internationalization, i18n)是指在开发软件时,确保其能够适应不同国家和地区的语言、文化习惯以及技术要求的过程。通过 Java 国际化,应用程序可以根据用户的本地设置动态地显示不同的文本、日期、货币等格式,从而提高用户体验并扩大软件的市场范围。
以下是在 Java 中实现国际化的关键组件和步骤:
Locale
类:java.util.Locale
表示特定的语言环境,它包含两个主要信息:语言代码(如 "en" 代表英语,"zh" 代表中文)和国家/地区代码(如 "US" 代表美国,"CN" 代表中国)。Locale
对象用于确定用户的文化偏好,例如日期、时间、数字和货币的格式。ResourceBundle
类:java.util.ResourceBundle
是一个用于加载本地化资源的工具类。开发者将文本和其他本地化数据存储在.properties或.xml文件中,每个文件对应一种特定的语言和区域。根据当前的Locale
,ResourceBundle
可以加载相应的资源文件,并从中获取与之对应的字符串。MessageFormat
类:java.text.MessageForma
t 提供了格式化消息的能力,允许插入参数化的变量到预定义的消息模板中。这样可以轻松地处理多语言下带占位符的消息,例如"你好,{0}!"在不同语言环境中,"{0}"的位置和内容会根据实际需要替换为具体的名字或其他值。DateFormat
类 和NumberFormat
类:这些类及其子类用于格式化和解析日期、时间和数字。例如,java.text.SimpleDateFormat
和java.text.DecimalFormat
根据给定的Locale
设置,可以生成符合当地习惯的日期、时间或数字格式。- Properties 文件:Properties 文件是一种常见的配置文件格式,用于存储键值对。在 Java 国际化中,可以使用 Properties 文件来存储本地化文本和其他资源的键值对。
其他相关的 API:还包括 Currency
、Collator
、ChoiceFormat
等类,它们分别帮助处理货币格式、字符排序规则以及基于条件的选择性消息输出等。
在实际应用中,国际化通常涉及以下几个步骤:
-
创建资源文件,如 messages_en.properties (英文)、messages_zh_CN.properties (简体中文)。
-
在资源文件中存储本地化后的文本字符串。
-
在代码中根据当前用户的 Locale 加载正确的 ResourceBundle。
-
使用 MessageFormat 进行动态消息格式化。
-
在需要的地方使用 DateFormat 或 NumberFormat 来格式化日期、时间或数字。
资源文件的创建和使用
首先我们需要定义自己的资源文件,资源文件命名方式是:
资源名_语言名称_国家/地区名称
.properties
其中 _语言名称_国家/地区名称
可以省略,如果省略的话,这个文件将作为默认的资源文件。在 resources 目录下创建如下2个资源文件
接下来我们看下 Java 代码如何加载:
java
// 定义 Locale 对象,这个 Locale 对象相当于定义本地环境,说明自己当前的语言环境和地区信息
Locale localeEn = new Locale("en", "US");
Locale localeZh = new Locale("zh", "CN");
// ResourceBundle.getBundle 方法去加载配置文件,该方法第一个参数就是资源的名称,第二个参数则是当前的环境
// 配置的 locale 实际上并不存在,那么就会读取 content.properties 文件中的内容(相当于这就是默认的配置)
ResourceBundle res = ResourceBundle.getBundle("messages", localeZh);
String success = res.getString("success");
System.out.println("success = " + success);
获取的资源格式化
Java 中的国际化还提供了一些 Format 对象,用来格式化传入的资源。
Format 主要有三类,分别是:
MessageFormat
:这个是字符串格式化,可以在资源中配置一些占位符,在提取的时候再将这些占位符进行填充。DateFormat
:这个是日期的格式化。NumberFormat
:这个是数字的格式化。
不过这三个完全可以单独当成工具类来使用,并非总是要结合 I18N 一起来用,实际上我们在日常的开发中,就会经常使用 DateFormat 的子类 SimpleDateFormat。
MessageFormat
占位符的使用:
java
Locale localeEn = new Locale("en", "US");
Locale localeZh = new Locale("zh", "CN");
ResourceBundle res = ResourceBundle.getBundle("messages", localeZh);
String test = res.getString("test");
MessageFormat format = new MessageFormat(test);
Object[] arguments = new Object[]{"java国际化"};
String s = format.format(arguments);
System.out.println("test = " + t);
附表:
语言简称表
语言 | 简称 |
---|---|
简体中文(中国) | zh_CN |
繁体中文(中国台湾) | zh_TW |
繁体中文(中国香港) | zh_HK |
英语(中国香港) | en_HK |
英语(美国) | en_US |
英语(英国) | en_GB |
英语(全球) | en_WW |
英语(加拿大) | en_CA |
英语(澳大利亚) | en_AU |
英语(爱尔兰) | en_IE |
英语(芬兰) | en_FI |
芬兰语(芬兰) | fi_FI |
英语(丹麦) | en_DK |
丹麦语(丹麦) | da_DK |
英语(以色列) | en_IL |
希伯来语(以色列) | he_IL |
英语(南非) | en_ZA |
英语(印度) | en_IN |
英语(挪威) | en_NO |
英语(新加坡) | en_SG |
英语(新西兰) | en_NZ |
英语(印度尼西亚) | en_ID |
英语(菲律宾) | en_PH |
英语(泰国) | en_TH |
英语(马来西亚) | en_MY |
英语(阿拉伯) | en_XA |
韩文(韩国) | ko_KR |
日语(日本) | ja_JP |
荷兰语(荷兰) | nl_NL |
荷兰语(比利时) | nl_BE |
葡萄牙语(葡萄牙) | pt_PT |
葡萄牙语(巴西) | pt_BR |
法语(法国) | fr_FR |
法语(卢森堡) | fr_LU |
法语(瑞士) | fr_CH |
法语(比利时) | fr_BE |
法语(加拿大) | fr_CA |
西班牙语(拉丁美洲) | es_LA |
西班牙语(西班牙) | es_ES |
西班牙语(阿根廷) | es_AR |
西班牙语(美国) | es_US |
西班牙语(墨西哥) | es_MX |
西班牙语(哥伦比亚) | es_CO |
西班牙语(波多黎各) | es_PR |
德语(德国) | de_DE |
德语(奥地利) | de_AT |
德语(瑞士) | de_CH |
俄语(俄罗斯) | ru_RU |
意大利语(意大利) | it_IT |
希腊语(希腊) | el_GR |
挪威语(挪威) | no_NO |
匈牙利语(匈牙利) | hu_HU |
土耳其语(土耳其) | tr_TR |
捷克语(捷克共和国) | cs_CZ |
斯洛文尼亚语 | sl_SL |
波兰语(波兰) | pl_PL |
瑞典语(瑞典) | sv_SE |
西班牙语(智利) | es_CL |
参考: