什么是国际化?
国际化,也叫i18n
,为什么叫i18n呢?
"i18n"是国际化(internationalization)的缩写,数字18代表了国际化这个单词中间的字母数量。类似这样的缩写还有k8s(kubernetes)等
为什么使用国际化?
简单理解就是开发的软件需要能同时应对不同国家和地区的用户访问,并根据用户地区和语言习惯,提供相应的、符合用具阅读习惯的页面和数据,例如,为中国用户提供汉语界面显示,为美国用户提供提供英语界面显示。
国际化细分
国际化需要分成两个部分:
前端国际化
前端国际化主要关注页面的显示和用户界面的本地化。它涉及将应用程序的界面元素,如文本、标签、按钮等,根据用户的语言和地区进行翻译和适配。前端国际化通常使用资源文件、语言包或翻译服务来存储和管理不同语言的文本。前端开发人员可以通过使用国际化框架或库,如React Intl、Vue I18n或Angular i18n等,来实现前端国际化功能
后端国际化
后端国际化主要关注处理与业务逻辑和数据相关的国际化问题。这包括但不限于日期和时间格式、货币符号、数字格式、排序规则、接口提示信息等。后端国际化的目标是确保应用程序能够适应不同的语言和地区,并提供正确的本地化数据。后端国际化可以通过使用国际化库或框架,如SpringBoot I18n,来实现后端国际化功能
总之,前端国际化主要关注页面显示和用户界面的本地化,而后端国际化则处理与业务逻辑和数据相关的国际化问题。两者通常需要协同工作,以实现完整的国际化功能
国际化相关知识
Locale对象
需要支持国际化,得先知道选择的是哪种地区的哪种语言,java中使用java.util.Locale来表示地区语言,这个对象内部包含了国家和语言的信息。
Locale中最常用的构造方法:
public Locale(String language, String country) {
this(language, country, "");
}
构造方法有两个参数:language:语言、country:国家
这两个参数的值不是乱写的,国际上有统一的标准,如:zh-CN表示中国大陆地区的中文,zh-TW表示中国台湾地区的中文,en-US表示美国地区的英文,en-GB表示英国地区的英文等等。通过语言和国家构造Locale对象,比如Locale locale = new Locale(“zh”, “CN”);,表示中国大陆地区的中文。
MessageSource接口
这是 Spring 国际化的核心接口,其定义如下:
public interface MessageSource {
/**
* 获取国际化信息
*/
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
/**
* 与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出NoSuchMessageException异常
*/
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
/**
* @param MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一个方法相同
*/
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
MessageSource
接口提供了三个获取国际化消息的方法,其主要是根据 Locale 信息获取对应的国际化消息的集合,然后根据 code 获取对应的消息,并且通过提供的参数 args 还可以对获取后的消息进行格式化。
具体参数含义如下:
参数名 | 含义 |
---|---|
code | 表示国际化资源中的属性名 |
args | 为消息中的参数填充的值 |
defaultMessage | 默认的消息,如果没有找到将返回默认消息 |
resolvable | 消息参数,封装了 code、args、defaultMessage |
locale | 表示本地化对象 |
类图结构
常见3个实现类:
类名 | 含义 |
---|---|
ResourceBundleMessageSource | 这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化文件 |
ReloadableResourceBundleMessageSource | 这个功能和第一个类的功能类似,多了定时刷新功能,允许在不重启系统的情况下,更新国际化文件的信息 |
StaticMessageSource | 它允许通过编程的方式提供国际化信息。 |
重点:我们在项目中会创建 MessageSource接口,但不管使用哪个实现类或者我们自定义的类,都要将Bean名称设置为messageSource
加载ApplicationContext时,自动搜索上下文中定义的MessagesSource Bean(名称必须为messageSource)。
如果无法找到消息的任何源,则实例化一个空的messageSource。
LocaleResolver接口
这个接口是用来设置当前会话默认的国际化语言的,其定义如下:
public interface LocaleResolver {
/**
* 根据当前请求解析当前请求的本地化信息
*/
Locale resolveLocale(HttpServletRequest request);
/**
* 设置当前请求、响应的本地化信息
*/
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
resolveLocale
方法用于从当前request
中解析对应出对应的Locale对象,场景如:
常见4个实现类:
类名 | 含义 |
---|---|
AcceptHeaderLocalResovler | 通过请求头里面的Accept-Language:zh,en;q=0.9,zh-HK;q=0.8来决定使用具体的Locale实例,其实AcceptHeaderLocalResovler就是使用HttpServletRequest实例中的Locale实例,在进入DispatcherServlet的时候HttpServletRequest实例里面就已经有Locale实例了,可以通过request.getLocale();来获取Locale实例,HttpServletRequest里面的Locale实例就是使用请求头里面的Accept-Language:zh,en;q=0.9,zh-HK;q=0.8来构建的,比如请求头里面的Accept-Language:zh,en;q=0.9,zh-HK;q=0.8 就表示zh的权重是1,en;q=0.9表示en的权重是0.9,zh-HK;q=0.8就表示zh-HK的权重是0.8,那么我么通过request.getLocale();获取到的就是权重最高的zh,然后就是构建一个zh的Locale实例,那么AcceptHeaderLocalResovler在解析Locale的时候就会返回zh的Locale实例 |
CookieLocaleResovler | 根据用户在Cookie中设置的某参数来进行确定具体的本地化Locale实例 |
SessionLocaleResovler | 根据用户在HttpSession中设置某参数来进行确定具体的本地化Locale实例 |
FixedLocalResovler | 使用jdk自带的默认的Locale实例 |
国际化文件
项目中,在resources
目录下创建名为i18n
的文件目录,然后我们在i18n
目录创建国际化文件
格式为:名称_语言_地区.properties
我们先来创建两种语言,如:
message.properties
(这个文件名称没有指定Local信息,当系统找不到的时候会使用这个默认的)
name=您的姓名
text=默认文本
message_cn_ZH.properties
:中文【中国】
name=姓名
text=文本
messages_en_US.properties
英文【美国】
name=name
text=text
我们通过MessageSource
接口的getMessage方法传入对应的key(如name、text),便可以从国际化文件中取值。同时我们还可以指定Locale对象,便能找到对应的国际化文件然后取值。
国际化一般实现
一般让前端在请求头中, 添加 { “Accept-Language”: “zh” }来标识,用户使用的语言
然后我们添加拦截器, 将这个值取出来, 这一步springboot已经帮我们做了(默认配置)
所以一般的单体springboot项目中, 直接在配置一下国际化资源文件即可
@Configuration
public class I18nConfig implements WebMvcConfigurer {
@Bean
public MessageSource messageSource() {
// 多语言文件地址
Locale.setDefault(Locale.CHINA);
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
//设置国际化文件存储路径 resources目录下 可以设置多个
messageSource.addBasenames("i18n/common/messages","i18n/system/messages","i18n/device/messages");
//设置根据key如果没有获取到对应的文本信息,则返回key作为信息
messageSource.setUseCodeAsDefaultMessage(true);
//设置字符编码
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.toString());
return messageSource;
}
}
然后在对应的目录文件(/i18n/common/)下定义国际化资源文件
美式英语 messages_en_US.properties
user.login.username=User name
user.login.password=Password
user.login.code=Security code
user.login.remember=Remember me
user.login.submit=Sign In
中文简体 messages_zh_CN.properties
user.login.username=用户名
user.login.password=密码
user.login.code=验证码
user.login.remember=记住我
user.login.submit=登录
MessageUtils工具类
public class MessageUtils
{
/**
* 根据消息键和参数 获取消息 委托给spring messageSource
*
* @param code 消息键
* @param args 参数
* @return 获取国际化翻译值
*/
public static String message(String code, Object... args)
{
MessageSource messageSource = SpringUtils.getBean(MessageSource.class);
return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
}
}
然后直接这样使用就行
MessageUtils.message("user.login.username")
MessageUtils.message("user.login.password")
国际化改进版
下面看网上的两种国际化
通过数据库实现国际化
基于若依-cloud的国际化方案
框架中国际化
目前我们国际化应用场景1.接口抛出的异常国际化 2.参数校验实现国际化
① 因为每个服务都有国际化配置文件,所以关于加载国际化配置的配置类我们放在common包中,首选创建加载MessageSource实现类的I18nConfig
配置类,这个配置类有以下作用:
- 实例化ReloadableResourceBundleMessageSource
- MessageSourceAccessor是对MessageSource的封装,提供了更便捷的方法来获取消息,免去我们封装MessageUtils类。
package com.tps.cloud.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class I18nConfig {
private static final String BASE_NAME = "classpath:i18n/messages";
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = new ReloadableResourceBundleMessageSource();
reloadableResourceBundleMessageSource.setBasenames(BASE_NAME);
reloadableResourceBundleMessageSource.setCacheSeconds(5);
reloadableResourceBundleMessageSource.setDefaultEncoding("UTF-8");
return reloadableResourceBundleMessageSource;
}
@Bean
public MessageSourceAccessor messageSourceAccessor(MessageSource messageSource) {
MessageSourceAccessor accessor = new MessageSourceAccessor(messageSource) ;
return accessor ;
}
}
②ValidatorConfig
配置类,此类作用:
- 注入MessageSource,用于国际化配置。
- 实例化工厂LocalValidatorFactoryBean,设置:
- 设置国际化:将MessageSource设置到ValidationMessageSource
- 设置提供者类(校验器):HibernateValidator
- 设置属性:实例化Properties,配置Hibernate的快速异常返回,hibernate.validator.fail_fast,加入到工厂配置。(快速返回指的是遇到一个不合法的,就不继续往下校验。)
- 加载配置:调用factoryBean的afterPropertiesSet
- 返回工厂方法的Validator
这里可以顺便谈谈关于Spring的FactoryBean:
- FactoryBean是接口,实现该接口的类可以自定义创建Bean。一般在框架中用来创建复杂的Bean。
- 这里的FactoryBean,实现InitializingBean接口,在afterPropertiesSet方法中创建bean,会在bean 实例化后调用。
- FactoryBean让Bean构建过程更灵活,可以理解为一种策略模式,我们需要生成什么样的
bean,可以通过实现接口来自定义。
package com.tps.cloud.config;
import jakarta.validation.Validator;
import org.hibernate.validator.HibernateValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import java.util.Properties;
@AutoConfiguration
public class ValidatorConfig {
@Autowired
private MessageSource messageSource;
/**
* 配置校验框架 快速返回模式
*/
@Bean
public Validator validator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
// 国际化
factoryBean.setValidationMessageSource(messageSource);
// 设置使用 HibernateValidator 校验器
factoryBean.setProviderClass(HibernateValidator.class);
Properties properties = new Properties();
// 设置 快速异常返回
properties.setProperty("hibernate.validator.fail_fast", "true");
factoryBean.setValidationProperties(properties);
// 加载配置
factoryBean.afterPropertiesSet();
return factoryBean.getValidator();
}
}
③ 在对应的服务下创建国际化资源文件
④ 在接口入参对象上添加占位符,然后测试保存用户接口
@NotBlank(message = "{SystemUserDto.Username.NotEmpty}")
@UniqueUsername
private String username;
@PostMapping
public Result<Long> save(@RequestBody @Validated SystemUserDto systemUserDto) {
Long id=systemUserService.createUser(systemUserDto);
return Result.ok(id);
}
⑤ 创建异常处理器,用于捕获参数验证异常,并返回统一数据结构
package com.tps.cloud.handler;
import com.tps.cloud.response.Result;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
/**
* 全局异常处理器MethodArgumentNotValidException
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* validation Exception (以form-data形式传参)
* @param exception
* @return Result
*/
@ExceptionHandler({ BindException.class })
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result bindExceptionHandler(BindException exception) {
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
return Result.failed(fieldErrors.get(0).getDefaultMessage());
}
/**
* validation Exception
* @param exception
* @return Result
*/
@ExceptionHandler({ MethodArgumentNotValidException.class })
public Result handleBodyValidException(MethodArgumentNotValidException exception) {
FieldError fieldError = exception.getBindingResult().getFieldError();
return Result.failed(String.format("%s", fieldError.getDefaultMessage()));
}
}
⑥通过Apifox测试,通过Header传递国际化参数Accept-Language参数(en-US,zh-CN),模拟参数username为空状态
⑦ 现在模拟异常抛出国际化,首先在保存接口添加用户账号唯一校验(记得去除掉上节添加的@UniqueUsername注解,防止被影响)
private final MessageSourceAccessor messages;
@PostMapping
public Result<Long> save(@RequestBody @Validated SystemUserDto systemUserDto) {
//验证用户唯一
SystemUserVo systemUserVo=systemUserService.findByUsername(systemUserDto.getUsername());
if(systemUserVo!=null){
return Result.failed(messages.getMessage("SystemUserDto.Username.Exist"));
}
Long id=systemUserService.createUser(systemUserDto);
return Result.ok(id);
}
国际化配置文件添加SystemUserDto.Username.Exist:
通过Apifox测试,通过Header传递国际化参数Accept-Language参数(en-US,zh-CN),模拟参数username重复存在的情况。