美文网首页
Spring boot半自动型国际化

Spring boot半自动型国际化

作者: Teddy_b | 来源:发表于2023-09-27 14:39 被阅读0次

Springboot国际化

Spring boot国际化本身并没有什么难度,因为Spring boot本身就支持了国际化的

但是如果是存量项目,异常信息、数据库信息等基本上都是中文的时候,这时候的国际化就稍微麻烦一点了

第一想法可能是定义好国际化工具类,然后再项目中的每一处中文的地方都加上工具的转换,完成国际化

这里记录下稍微能偷点懒的国际化经历

国际化的通用配置

@Component
@Slf4j
public class I18nService implements ApplicationContextAware {

    private static MessageSource messageSource;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        messageSource = applicationContext.getBean(MessageSource.class);
    }


    public static String getOrDefault(String key, String def, Object... args) {
        return getOrDefault(null, key, def, args);
    }

    public static String getOrDefault(Locale locale, String key, String def, Object... args) {
        AssertUtil.assertNotNull(messageSource, "Message source is null");
        if (null == locale) {
            locale = I18nInterceptor.getLocale();
        }
        if (!locale.equals(Locale.US) || StringUtils.isEmpty(def)) {
            return def;
        }
        String message;
        try {
            message = messageSource.getMessage(key, args, locale);
        } catch (NoSuchMessageException e) {
            log.warn("I18n no message for key:{}", key);
            message = def;
        }
        return message;
    }
}

通常情况下,我们只需要定义这么一个工具类,然后再项目中添加国际化配置


image.png

最后再加上配置参数

spring.messages.basename=i18n/messages

再国际化配置中添加自定义key所对应的中英文语句,类似这样


image.png

这样再出现中文的地方,我们就可以通过工具类的静态方法获取对应的国际化信息,其中KEY是完全自定义的,DEFAULT信息考虑兼容下可以直接使用项目中当前的异常信息、数据库信息

I18nService.getOrDefault(SOME_KEY, DEFAULT_MESSAGE)

如此一来,一个简单的国际化工作就完成了,剩下的就是人肉替换每一处中文了

虽然简单,但是工作量比较大,得想个法子偷懒

改进版国际化配置

这里使用的是拦截器去帮我们半自动翻译

拦截器
@Component
@Slf4j
public class I18nInterceptor implements HandlerInterceptor {

    private static final ThreadLocal<Locale> INHERITABLE_LOCALE_HOLDER =
            new NamedInheritableThreadLocal<>("MyLocale");

    private static final String LANG_HEADER = "Accept-Language";

    public static final String LANG_PARAM = "lang";

    private static final String EN_KEY_WORDS = "en";

    private static final String ZH_KEY_WORDS = "zh";


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String param = request.getParameter(LANG_PARAM);
        Locale byRequest = getLocaleByRequest(param);
        if (null != byRequest) {
            log.info("Locale by params values");
            setLocale(byRequest);
            return true;
        }

        String header = request.getHeader(LANG_HEADER);
        byRequest = getLocaleByRequest(header);
        if (null != byRequest) {
            log.info("Locale by header values");
            setLocale(byRequest);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        INHERITABLE_LOCALE_HOLDER.remove();
    }

    public static Locale getLocale() {
        Locale locale = INHERITABLE_LOCALE_HOLDER.get();
        return locale == null ? Locale.SIMPLIFIED_CHINESE : locale;
    }

    public static void setLocale(Locale locale) {
        INHERITABLE_LOCALE_HOLDER.set(locale);
    }

    public static Locale getLocaleByRequest(String request) {
        if (StringUtils.isEmpty(request)) {
            return null;
        }
        if (request.contains(ZH_KEY_WORDS)) {
            return Locale.SIMPLIFIED_CHINESE;
        }
        if (request.contains(EN_KEY_WORDS)) {
            return Locale.US;
        }
        return null;
    }

    public static Locale getLocaleOrDefault(String request) {
        Locale locale = getLocaleByRequest(request);
        return locale == null ? Locale.SIMPLIFIED_CHINESE : locale;
    }

这里拦截器主要是获取请求Locate,我们根据Locate来决定语言

  • 为了通用性,同时支持了Header、请求参数指定语言体系,默认是中文

  • 为了再子线程中共享Locate,使用的是NamedInheritableThreadLocal,即可继承的ThreadLocal

  • NamedInheritableThreadLocal定义的是静态变量,所以必须再请求完成后进行清除,否则可能会对下次请求的默认值产生影响

扩展下工具类
@Component
@Slf4j
public class I18nService implements ApplicationContextAware {

    private static MessageSource messageSource;

    private static final String BASE_PATH = "i18n/";

    private static final String DEFAULT_PROPERTIES = "messages.properties";
...

    public static String findOrDefault(String def) {
        return findOrDefault(def, null);
    }

    public static String findOrDefault(String def, Locale locale) {
        if (null == locale) {
            locale = I18nInterceptor.getLocale();
        }
        if (!locale.equals(Locale.US) || StringUtils.isEmpty(def)) {
            return def;
        }

        String[] messages = getMessageFromErrorMessage(def);
        if (null == messages || messages.length == 0) {
            log.info("Cannot found message from error message:{}", def);
            return def;
        }
        //Object[] args = getArgsFromErrorMessage(def);
        Map<String, Map<String, String>> dump = dumpI18nProperties(BASE_PATH+DEFAULT_PROPERTIES);
        Map<String, String> defaultDump = dump.get(DEFAULT_PROPERTIES);
        if (MapUtils.isEmpty(defaultDump)) {
            log.info("No default properties found");
            return def;
        }
        List<String> keys = I18nUtil.i18nFindLatestKeys(messages, defaultDump);
        if (keys.size() != messages.length) {
            log.warn("Cannot found key in default properties for error message:{}", Joiner.on(",").join(messages));
            return def;
        }

        List<String> i18ns = Lists.newArrayList();
        for (int i = 0; i < keys.size(); i++ ) {
            if ("_something_non_exists".equals(keys.get(i))) {
                i18ns.add(messages[i]);
            } else {
                i18ns.add(getOrDefault(locale, keys.get(i), def));
            }
        }

        return Joiner.on(":").join(i18ns).replace(",", ", ");
    }

    public static Map<String, Map<String, String>> dumpI18nProperties(String locationPattern) {
        Map<String, Resource> resourceMap = Maps.newHashMap();
        try {
            ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
            Resource[] resources = resourceResolver.getResources(locationPattern);
            AssertUtil.assertFalse(resources.length == 0, "No messages found");
            for (Resource resource : resources) {
                resourceMap.put(resource.getFilename(), resource);
            }
        } catch (IOException e) {
            log.error("Read resources error", e);
            return Maps.newHashMap();
        }
        Map<String, Map<String, String>> dump = Maps.newHashMap();
        for (Map.Entry<String, Resource> resourceEntry : resourceMap.entrySet()) {
            BufferedReader reader = null;
            Map<String, String> resourceKv = Maps.newHashMap();
            try {
                reader = new BufferedReader(new InputStreamReader(resourceEntry.getValue().getInputStream()));
                Properties props = new Properties();
                props.load(reader);
                resourceKv = propsToMap(props);
            } catch (IOException e) {
                log.error("Read resource:{} error", resourceEntry.getKey());
            } finally {
                IOUtils.closeQuietly(reader);
            }
            dump.put(resourceEntry.getKey(), resourceKv);
        }
        return dump;
    }

    private static Map<String, String> propsToMap(Properties props) {
        AssertUtil.assertNotNull(props, "Properties is null");
        Map<String, String> kv = Maps.newHashMap();
        for (Object propsKey : props.keySet()) {
            kv.put(propsKey.toString(), props.getProperty(propsKey.toString()));
        }
        return kv;
    }

    private static String[] getMessageFromErrorMessage(String message) {
        if (StringUtils.isEmpty(message)) {
            return null;
        }
        message = message.replace(":", ":");
        return message.split(":");
    }
}

这里主要是扩展了两个find系列的方法,根据message直接进行翻译

  • 首先对需要翻译的message进行分割,需要分割成哪些是需要翻译的部分,如常见的中文,哪些是不需要翻译的部门,如实时的数据

  • 按照项目特点,需要翻译的和不需要翻译的一般使用的是:进行分隔的,所以就按照:进行分隔了

  • 最后再把翻译好的重新组装起来

对中文进行直接翻译的时候,采用的方式是先根据中文找到对应的KEY,然后再根据KEY找到对应的英文消息

根据中文找到对应的KEY的时候,会面临一个问题是,字面相近的中文,怎么匹配到正确的KEY的问题

打分工具
public class I18nUtil {

    private static final Integer MAX_SCORE = 1000;

    private static final Integer MIN_SCORE = 0;

    public static final Pattern CHINESE = Pattern.compile("[\u4e00-\u9fa5]");

    public static List<String> i18nFindLatestKeys(String[] messages, Map<String, String> defaultDump) {
        if (null == messages || messages.length == 0 || MapUtils.isEmpty(defaultDump)) {
            return null;
        }
        List<String> keys = Lists.newArrayList();
        for (String message : messages) {
            keys.add("_something_non_exists");
        }
        for (int i = 0; i < messages.length; i++) {
            int min = MIN_SCORE;
            for (Map.Entry<String, String> entry : defaultDump.entrySet()) {
                String s = messages[i].trim();
                if (!filter(entry.getValue(), s)) {
                    continue;
                }
                int score = score(entry.getValue(), s);
                if (min == MIN_SCORE) {
                    min = score;
                    keys.set(i, entry.getKey());
                } else if (min < score) {
                    min = score;
                    keys.set(i, entry.getKey());
                }
            }
        }
        return keys;
    }

    private static boolean filter(String message, String target) {
        Matcher m = CHINESE.matcher(target);
        if (!m.find()) {
            return false;
        }
        return StringUtils.isNotEmpty(message) && StringUtils.isNotEmpty(target) && message.contains(target);
    }

    private static int score(String message, String target) {
        AssertUtil.assertTrue(StringUtils.isNotEmpty(message), "I18n message is empty");
        AssertUtil.assertTrue(StringUtils.isNotEmpty(target), "I18n target message is empty");
        // 1. equals
        if (message.equals(target)) {
            return MAX_SCORE;
        }

        // 2. only contains, small length refer to more score
        return MAX_SCORE - message.length();
    }

为了解决这个问题,定义了一个打分的工具类

  • 首先是过滤规则:对于非中文信息,我们不予翻译,然后找到那些包含了需要翻译的语句的那些国际化配置信息

  • 然后是打分规则:简单的认为完全一致的配置得分最高,其次配置越短的得分越高

全局返回结果
public static <T> MyResult<T> failResult(String ... errorMsg) {
        MyResult<T> MyResult= new MyResult<>();
        MyResult.setStatus(FAIL);
        String message = getString(errorMsg);
        message = I18nService.findOrDefault(message);
        MyResult.setMessage(message);
        return MyResult;
    }

最后再全局返回结果里加上对异常信息的翻译

如此一来,就不需要每一处出现中文的地方都用工具类去改一下了,只需要调整下异常信息的格式

把那些不是用:分隔的异常信息改过来,就能够实现个半自动的翻译了

最后,对于那些写死再数据库中的中文信息

  • 如果是出现在异常信息里的,那么只需要把它用:分隔开就行了

  • 如果是返回数据结构体里的,那没啥好办法了,只用再用到的地方单独调一下I18nService.findOrDefault(message);
    或者再全局返回里根据类型的class转一下,然后加上翻译

public static <T> MyResult<T> successfulResult(T result) {
MyResult<T> MyResult= new MyResult<>();
MyResult.setStatus(SUCCESS);
MyResult.setData(result);
MyResult.setStatusCode(MyErrorCode.SUCCESS);
MyResult.setMessage(MyErrorCode.SUCCESS.getResultMsg());
return MyResult;
}

相关文章

网友评论

      本文标题:Spring boot半自动型国际化

      本文链接:https://www.haomeiwen.com/subject/cedkbdtx.html