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;
}
}
通常情况下,我们只需要定义这么一个工具类,然后再项目中添加国际化配置
![](https://img.haomeiwen.com/i12861705/fed92dec2e333d30.png)
最后再加上配置参数
spring.messages.basename=i18n/messages
再国际化配置中添加自定义key所对应的中英文语句,类似这样
![](https://img.haomeiwen.com/i12861705/ca98b3ac0b6b1eb1.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;
}
网友评论