记录一次java改造国际化的经历

今天来说说,如何改造已经上线的项目来支持国际化,今天就来简单说一下,第一篇博客,以下如有错误,请及时指出.

项目中要整理国际化,需要考虑的几点

  1. 数据库的数据需要支持国际化,什么时候显示name,什么时候显示english_name
  2. 数据库增加了一个新的字段,如果在查询逻辑不变的基础上,最小成本完成返回数据的name和english_name的动态切换呢,也就是原有查询不变,但是要根据用户语言状态来判断如何给前端返回数据
  3. 接口的返回信息,每次前后端交互的时候,返回的信息要动态支持中英文切换,比如用户切换语言状态变为英文,那么页面显示变成英文,返回的话术提示也要变成英文

其中, 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逻辑统一处理

这次分享自己在国际化中的经验,喜欢的小伙伴可以收藏点赞,第一次写博客,不足的地方请多多指教

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值