1.业务场景:
在国际化语言展示信息的时候,翻译内容一般情况分为2种,一种静态内容翻译,一种动态内容翻译;
2.静态内容翻译实现:
springboot提供的i18n实现方式,此处贴出一种扩展性比较好的实现方式。
1).LocalConfig.class配置AcceptHeaderLocaleResolver,通过请求头参数Accept-Language访问后端接口
@Configuration
public class LocalConfig {
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderResolver resolver = new AcceptHeaderResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return resolver;
}
}
2).AcceptHeaderResolver.class,该类实现AcceptHeaderLocaleResolver,可以通过HttpServletRequest自定义包装请求头信息
public class AcceptHeaderResolver extends AcceptHeaderLocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 可以改写输入请求的实现逻辑
return super.resolveLocale(request);
}
}
3).application.yml配置
spring:
messages:
basename: static/i18n/discount
4).在resources下建立目录static/i18n
discount.properties
discount.desc=所有人都可以获得折扣,{0}
discount_en_US.properties
discount.desc=Everyone can get a discount,{0}
discount_zh_CN.properties
discount.desc=所有人都可以获得折扣,{0}
5).资源调用
MessageSourceService.class
public interface MessageSourceService {
/**
* 获取翻译的内容
*
* @param msgKey
* @param args
* @returno
*/
String get(String msgKey, String... args);
}
MessageSourceServiceImpl.class
@Slf4j
@Service
public class MessageSourceServiceImpl implements MessageSourceService, ApplicationContextAware {
private static MessageSource messageSource;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
messageSource = applicationContext.getBean(MessageSource.class);
}
/**
* 获取翻译的内容
*
* @param msgKey
* @param args
* @return
*/
@Override
public String get(String msgKey, String... args) {
return messageSource.getMessage(msgKey, args, LocaleContextHolder.getLocale());
}
}
6).测试调用
@Autowired
private MessageSourceService messageSourceService;
@GetMapping("test1")
public String test1(@RequestHeader(value = "Accept-Language", defaultValue = "zh-CN", required = false) String language) {
return messageSourceService.get(DiscountEnum.DESC.getCode(), "\uD83D\uDE04");
}
3.动态内容翻译实现
在实现动态内容翻译的时候,有过很多思考,也尝试过很多方式,有尝试过基于jackson和fastjson的序列化器和转化器以及fliter的形式实现国际化翻译,但是在实现过程中发现代码的入侵性实在是太强了,所以后来采用基于springboot的国际化翻译的实现方式;
1).数据库表结构设计,基于h2,github上有基于mysql例子
schema.sql
DROP TABLE IF EXISTS `i18n`;
CREATE TABLE `i18n` (
`id` int(11) NOT NULL auto_increment,
`ref_id` int(11) NOT NULL,
`ref_type` varchar(4) NOT NULL,
`language_type` varchar(10) NOT NULL,
`translate_text` text NOT NULL,
`created_at` timestamp NOT NULL default CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_ref_id_type_lang` ( `ref_id`, `ref_type`, `language_type` )
)
data.sql
DELETE FROM `i18n`;
INSERT INTO `i18n`(`ref_id`, `ref_type`, `language_type`, `translate_text`)
VALUES (1, '1', 'en-US', 'english product name');
2).自定义翻译注解@I18n,使用方式在对应字段加上注解即可
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface I18n {
/**
* 指定要翻译字段的主键id名称,eg:refId=id
*
* @return
*/
String refIdAlias();
/**
* 按顺序翻译,如果第一个值为空,就用第二个,以此类推
*
* @return
*/
RefTypeEnum[] refType();
}
3).翻译核心实现方法I18nServiceImpl.class部分代码
@Override
public <T> T translate(String language, T in) {
Map<String, Integer> refIdMap = new HashMap<>();
try {
Field[] fields = in.getClass().getDeclaredFields();
// 获取refId的分组refType
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(com.i18n.core.annotation.I18n.class)) {
com.i18n.core.annotation.I18n i18nAnnotation =
field.getAnnotation(com.i18n.core.annotation.I18n.class);
String refIdAlias = i18nAnnotation.refIdAlias();
if (StringUtils.isEmpty(refIdAlias)) {
continue;
}
Field refField = in.getClass().getDeclaredField(refIdAlias);
refField.setAccessible(true);
Integer refId = (Integer)refField.get(in);
refIdMap.put(refIdAlias, refId);
}
}
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(com.i18n.core.annotation.I18n.class)) {
com.i18n.core.annotation.I18n i18nAnnotation =
field.getAnnotation(com.i18n.core.annotation.I18n.class);
String refIdAlias = i18nAnnotation.refIdAlias();
// refId和被翻译的数据必须属于同一个refType
Integer refId = refIdMap.get(refIdAlias);
if (refId != null) {
// 获取refType,languageType
RefTypeEnum[] refTypeEnums = i18nAnnotation.refType();
List<String> refTypeList =
Arrays.stream(refTypeEnums).map(RefTypeEnum::getCode).collect(Collectors.toList());
Map<String, String> i18nMap = getI18nMap();
// 被翻译值的原始值
String defaultText = String.valueOf(field.get(in));
List<String> translateTextList = new ArrayList<>();
// 从翻译map中取出匹配的值
refTypeList.forEach(refType -> {
String translateKey = refId + ":" + refType + ":" + language;
String translateValue = i18nMap.get(translateKey);
if (!StringUtils.isEmpty(translateValue)) {
translateTextList.add(translateValue);
}
});
// 复制给翻译的值
String translateText = translateTextList.stream().findFirst().orElse(defaultText);
// 翻译后重新设置
field.set(in, translateText);
}
}
}
} catch (Exception e) {
log.warn("多语言序列化时异常,被翻译的对象:{},i18n:{}", in, e.toString(), e);
}
return in;
}
4).针对http请求,通过请求头参数Accept-Language访问后端接口,统一接口处理I18nResponseBodyAdvice.class
@ControllerAdvice
@Slf4j
public class I18nResponseBodyAdvice implements ResponseBodyAdvice {
@Autowired
private I18nService i18nService;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 这里直接返回true,表示对任何handler的responseBody都调用beforeBodyWrite方法
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// resBody就是controller方法中返回的值,对其进行修改后再return就可以了
request.getHeaders();
// 当视图的多语言有值的时候
HttpHeaders httpHeaders = request.getHeaders();
if (httpHeaders == null) {
return body;
}
List<String> languageList = httpHeaders.get("Accept-Language");
if (CollectionUtils.isEmpty(languageList)) {
return body;
}
String language = languageList.get(0);
if (StringUtils.isEmpty(language)) {
return body;
}
return i18nService.translate(language, body);
}
}
5).测试例子
@RestController
public class TestController {
@GetMapping("test")
public ShopDemo
test(@RequestHeader(value = "Accept-Language", defaultValue = "zh-CN", required = false) String language) {
ShopDemo shopDemo = new ShopDemo();
return shopDemo;
}
@Data
class ShopDemo {
private Integer id = 1;
@I18n(refIdAlias = "id", refType = {RefTypeEnum.PRODUCT_NAME})
private String name = "中文商品名称";
}
}
}
6)请求样例:
4.本文皆为原创,且在项目中实战。
5.项目github源码地址。
github地址:https://github.com/java-joker/i18n
转载需指明出处