i18n国际化翻译

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)请求样例:


image.png

4.本文皆为原创,且在项目中实战。

5.项目github源码地址。
github地址:https://github.com/java-joker/i18n
转载需指明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。