今天来说说,如何改造已经上线的项目来支持国际化,今天就来简单说一下,第一篇博客,以下如有错误,请及时指出.
项目中要整理国际化,需要考虑的几点
- 数据库的数据需要支持国际化,什么时候显示name,什么时候显示english_name
- 数据库增加了一个新的字段,如果在查询逻辑不变的基础上,最小成本完成返回数据的name和english_name的动态切换呢,也就是原有查询不变,但是要根据用户语言状态来判断如何给前端返回数据
- 接口的返回信息,每次前后端交互的时候,返回的信息要动态支持中英文切换,比如用户切换语言状态变为英文,那么页面显示变成英文,返回的话术提示也要变成英文
其中, 2 是查数据库返回的数据是英文还是中文的逻辑, 3 是业务话术返回中文还是英文的逻辑, 是不一样的, 那么搞清楚我们要做的事情, 然后一步步来弄就可以了
数据库数据支持英文属性
首先数据库中所有在页面显示出来的字段,例如name这样的字段,都要新增一个english_name字段,翻译的话,可以把把表中的数据导出,然后给相应的部门进行翻译,或者通过google翻译自己翻译,然后再把english_name对应的刷回到表里面,这里有两个办法
- 导出的excel文件数据里面的格式可以这样通过excel拼接sql的方式实现,但是要注意,这种只适合数据量比较少的时候,比如只是导航或者菜单这种表数据在页面显示很少的时候
- 利用kettle组件来插入数据,方法也比较简单,写的逻辑也不复杂,一个转换就可以实现
这里解释一下,流字段我自己的理解就是经过前面记录关联,也就是笛卡尔输出之后存在的字段,相当于表里面的字段和excel中的表头信息(一定要存在第一行,默认第一行不是数据,而是表头信息)的合集
后台返回话术国际化
到这里我们数据库的已经存在的数据国际化就已经处理好了, 那么代码逻辑, 返回给前端的提示信息或者错误信息如何支持国际化呢, 这里用到了messageSource这个类来实现, 后台返回话术这里要做的事情特别多,还很琐碎,很杂
- 把项目中的所有涉及到的返回话术导出来,进行翻译
- 生成properties配置文件的key-value,这里我处理的时候没有太好的办法,人为的手工命名每一个key值,而且要保证key唯一,还得保证key的可读性要好维护,工作量大的一点,还没什么技术含量,如果有更好的办法,请大神赐教
- 通过excel里面的各种组合命令,拼出来properties配置文件
- 批量替换项目中的代码
首先把项目中的提示话术都得导出来, 操作办法, 我是用的IDEA
打开全局搜索,输入: “."
->
导出数据到文本编辑器
->
".[^\x00-\xff]+.*” 正则匹配出符合的字符
->
选中剪切出来
导出txt文本,然后用文本编辑器提取主要信息,我用的话sublime Text
全选后把符合条件的数据剪切出来,可以放在excel表格中,然后为每一个提示话术起key值,并用excel + 命令 拼接出来key=value格式的properties文件, 这里我遇到一个问题, 如果数据是已经给好的会很好处理, 但是实际开发过程中, 大多数情况都是, 翻译和开发同时进行的, 那么就代表你自己需要弄假的数据(自己测试用的是机器翻译出来的英文数据), 然后等人工翻译出来的英文数据好了之后, 替换原有的数据, 这个过程是痛苦又反复, 因为人工需要反复检查翻译,那么你也需要不断替换文件,如果没有一个简单的办法替换的话,你会难受死, 我的解决办法就是excel的函数, =VLOOKUP(C2,$D$2:$E$495,2,0) Excel中如何设置一个单元格的值等于另外一列的其中一个单元格的值时,等于对应的的单元格的值
这里我每次只需要替换DE两列值,后面的F就会根据VLOOKUP函数自动匹配成功,其实,自己写一个脚本也可以实现,只不过有现成的函数可以用减少工作量,何乐不为呢,然后在用一些简单的拼接就可以拼出来一个新的messages.properties文件了,配置文件的数据问题就解决了,然后如何查询配置文件的数据呢, 我们可以根据 messageSource封装方法
@Controller
@Slf4j
public class I18nService {
@Autowired
private MessageSource messageSource;
// 多语言资源配置文件( basenames = i18n/messages ),即配置文件所在目录为 i18n,文件前缀为 messages
@Value("${spring.messages.basename}")
private String basename;
@Value("${spring.messages.encoding}")
private String encoding;
/**
* 设置当前的返回信息,带有动态参数
*
* @param code
* @param params
* @return
*/
public String getMessage(String code, Object ... params) {
Locale locale = LocaleContextHolder.getLocale(); // 获取语言环境,前提是要提前setLocal,哪里set,后面拦截器会说
String result = null;
try {
result = messageSource.getMessage(code, params, locale);
} catch (NoSuchMessageException e) {
log.warn("Cannot find the error message of internationalization, return the original error message.");
}
if (result == null) {
return code;
}
return result;
}
/**
* 设置当前的返回信息
*
* @param code
* @return
*/
public String getMessage(String code) {
Locale locale = LocaleContextHolder.getLocale();
String result = null;
try {
result = messageSource.getMessage(code, null, locale);
} catch (NoSuchMessageException e) {
log.warn("Cannot find the error message of internationalization, return the original error message.");
}
if (result == null) {
return code;
}
return result;
}
}
spring:
messages:
basename: i18n/messages
encoding: UTF-8
i18n文件夹里面有三个配置文件
这里还有一个注意的点,一定要存在message.properties默认的配置文件,否则项目启动会报错
messages.properties配置文件支持动态参数匹配
messages.properties配置文件:
key=我的:{0}是{1} // 我的名字是XXX,类似这样的话术
java代码:
i18nController.getMessage("message配置文件中的key",
参数1, 参数2);
这里基本的逻辑大框已经有了,接下来就是如何批量替换掉项目中涉及到返回的信息的代码,举了例子,假设我们项目的一个接口的返回值是这样的
return new AjaxResult(Status.STATUS_SUCCESS, "新增成功");
而我们要处理成支持动态显示的代码
return new AjaxResult(Status.STATUS_SUCCESS, i18nService.getMessage("insert.success"));
这里我用的办法是写了一个方法,批量替换项目中的代码,
public static void main(String[] args) throws Exception {
String userDir = System.getProperty("user.dir");
// File srcDir1 = new File(userDir + "/XXX/src/main/java/com/xxx");
File srcDir1 = new File(userDir + "/XXX/src/main/java/com/xxx");
List<File> list = Arrays.asList(srcDir1);
List<File> result = new ArrayList<>();
for (File file : list) {
getAllJavaFiles(file, result); // 拿到所有文件
}
String propFile = userDir + "/XXX/src/main/resources/i18n/messages.properties";
Properties props = loadR(propFile); // 读取配置文件
for (File file : result) {
FileReader in = new FileReader(file);
BufferedReader bufIn = new BufferedReader(in);
// 内存流, 作为临时流
CharArrayWriter tempStream = new CharArrayWriter();
// 替换
String line = null;
while ((line = bufIn.readLine()) != null) {
// 替换每行中, 符合条件的字符串
Iterator it = props.entrySet().iterator();
while(it.hasNext()){
Map.Entry entry = (Map.Entry) it.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
line = line.replaceAll(key, "i18nService.getMessage(\"" +value + "\")");
}
// 将该行写入内存
tempStream.write(line);
// 添加换行符
tempStream.append(System.getProperty("line.separator"));
}
// 关闭 输入流
bufIn.close();
// 将内存中的流 写入 文件
FileWriter out = new FileWriter(file);
tempStream.writeTo(out);
out.close();
}
}
private static void getAllJavaFiles(File file, List<File> result) {
if (!file.exists()) {
return;
}
if (file.isFile()) {
result.add(file);
}
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
getAllJavaFiles(f, result);
}
}
}
}
public static Properties loadR(String propFile) throws IOException {
Properties props = new Properties();
props.load(new FileReader(propFile));
return props;
}
就是一个批量替换kv的方法,有更好的逻辑的话,欢迎大神赐教,到这里项目中的返回话术就处理好了
查询逻辑支持国际化
这里就是我踩到坑的一点了,最初为了尽量保证小成本下改造查询逻辑. 由于前端涉及到修改的页面比较多,工作量大,所以为了减少前端的工作量,查询这里后台进行了特殊处理,逻辑为,如果是英文环境下,那么name和englishName显示的都是英文,就代表要把englishName赋值给name,我们的做法就是,重写了name属性的getName方法,在里面判断local
public String getName() {
Locale locale = LocaleContextHolder.getLocale();
if (locale == Locale.ENGLISH && StringUtils.isNotEmpty(this.englishName)) {
return this.englishName;
} else {
return this.name;
}
}
这样写的确解决了查询时的显示问题,可以让查询逻辑最小化满足,但是忽略了一个问题,如果是英文环境下进行新增操作,就会发生问题了,新增的时候,填写的name就会默认设置成englishName,所以优化后的逻辑变为
public String getName(Locale locale) {
if (locale == Locale.ENGLISH && StringUtils.isNotEmpty(this.englishName)) {
return this.englishName;
} else {
return this.name;
}
}
保证原有的getName逻辑不变,然后根据业务判断,在需要显示英文的地方处理,把确定支持国际化的地方的getName方法替换掉就可以了.
另外也想了其他的方案,但是有顾虑,所以项目没有这么搞
- 通用化处理返回的json,实现可以搜一下这个类HandlerMethodReturnValueHandler,实现handleReturnValue()方法, 把name和englishName互换.担心的点,因为项目业务比较复杂,有些VO返回的时候多个表数据组合,name有多个表的name在一个VO里面,所以name会区分开,叫法不同,这种场景不知道怎么覆盖,也需要人为去处理,如果返回数据格式一致可以这么搞,所以放弃了
前端替换js文件
前端的逻辑是用不同的js文件来处理返回值,类似用上面说的properties, 我是用python写的脚本替换的,相当于java的等值替换,由于前端同学不懂java,没办法安装java语言环境,所以写好python脚本,给前端同学自己执行替换python命令
#!/usr/bin/python
# coding=utf-8
import ConfigParser
import time # 引入time模块
ticks = time.time()
print "当前时间戳为:", ticks
cf = ConfigParser.ConfigParser()
# cf.read("test.conf")
cf.read("/Users/XXX/PycharmProjects/xxPY/eam/test.conf")
# print (cf.get("db", "'领用'"))
# for f in cf.options("db"):
# print f
# print (cf.get("db", f))
opts = cf.options("db")
# print opts
# print ("'请选择用户'" in opts)
# infile = open("en.js", "r") #打开文件
infile = open("/Users/XXX/PycharmProjects/xxPY/eam/en.js", "r") #打开文件
outfile = open("/Users/XXX/PycharmProjects/xxPY/eam/content-test.js", "w") # 内容输出
for line in infile: #按行读文件,可避免文件过大,内存消耗
for f in cf.options("db"):
if f in line:
line = line.replace(f, cf.get("db", f))
outfile.write(line)#first is old ,second is new
infile.close() #文件关闭
outfile.close()
ticks = time.time()
print "当前时间戳为:", ticks
en.js文件
export default {
allSelect: {
holder: '请选择用户'
}
};
test.conf文件
[db]
'请选择用户'='Please select User.'
语言环境保存问题
前提: 前端不能做到每个请求都改掉,加上这个Accept-Language参数,那么后台如果解决这个问题,然后解决语言切换问题呢?
实现:
在user表中增加一个字段language,这个字段来代表语言环境,然后通过Interseptor拦截器来是实现setLocal(),
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
@Component
@Slf4j
public class MyInteceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
MDC.put("logId", LogIdUtils.logId());
Map reqMap = new HashMap();
methodParams(request, reqMap);
MyEntity myEntity = new MyEntity(); // 把请求信息封装成一个实体,放进 ThreadLocal里面
// ThreadLocal.set(myEntity);
// 参数map reqMap
// 请求的api路径 request.getRequestURI();
// request.getMethod(); // GET | POST
// redis里面获取用户信息,判断语言环境
// 默认没有就是请求地区的语言
Locale locale;
if ("en".equals(language)) {
locale = Locale.ENGLISH;
LocaleContextHolder.setLocale(locale);
} else {
locale = Locale.CHINA;
LocaleContextHolder.setLocale(locale);
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// ThreadLocal.get(); 获得preHandle方法中的信息
String.valueOf(response.getStatus()); // 访问结果
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
// hm.getBeanType().getName(); ControllerName的全名
// hm.getMethod().getName(); // 方法名
}
Object body = request.getSession().getAttribute("body"); // 方法的返回值
// 插入数据,记录日志信息
MDC.clear();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
LocaleContextHolder.resetLocaleContext();
}
// 这个方法需要前端每个请求都得传递Accept-Language,改动量大,所以放弃了这个方式
private Locale getLanguage(HttpServletRequest request) {
String language = request.getHeader("Accept-Language");
Locale locale;
if (language == null) {
locale = request.getLocale();
} else if ("en-US".equals(language)) {
locale = Locale.ENGLISH;
} else {
locale = Locale.CHINA;
}
return locale;
}
}
private void methodParams(HttpServletRequest request, Map reqMap) { // 获取方法的参数信息
Map ParameterMap = request.getParameterMap();
Set<Map.Entry<String, String[]>> entry = ParameterMap.entrySet();
Iterator<Map.Entry<String, String[]>> it = entry.iterator();
while (it.hasNext()) {
Map.Entry<String, String[]> me = it.next();
String key = me.getKey();
String value = me.getValue()[0];
reqMap.put(key, value);
}
}
总结
- Kettle创建转换,向数据库插入english数据
- Inteseptor拦截器统一处理语言
- properties配置文件, 支持动态参数方式
- 后台getName逻辑统一处理
这次分享自己在国际化中的经验,喜欢的小伙伴可以收藏点赞,第一次写博客,不足的地方请多多指教