前言
随着公司业务的扩展, 在日本,韩国, 新加坡成立了众多海外公司,但是我们的合同,请款,报销,以及一些it申请,考勤请假加班申请等系统目前都是中文的, 所以我们需要实现一个多语言翻译的功能, 让我们的系统支持多语言。
预期
1、实现业务系统的多语言版本切换后的翻译诉求。
2、系统改动尽量的小,目前的数据存储不要改动,只在展示环节,需要的时候翻译一下
3、业务里如公司名不需要翻译,但是分类名可能需要翻译, 需要能明确标注,按需翻译
实现思路
目标限定
本方案只针对不变的文案进行翻译,比如分类,状态,固定的提示语
本方案主要实现能适配各种数据结构的翻译,能够在VO的定义中按标记,在需要的时候进行翻译
对于含有动态的语言结构,如“xxx提交了报销申请等待您的审批”,这里的xxx 代表一个人名,可能是张三, 可能是李四,可能在语句的最前面,也可能在语句的中间 这句话要翻译的时候,不同的语言这个变量可能位置不一样,不能简单的用变量+不变内容翻译的结果直接连接这种方式, 需要单独的使用模版化翻译, 不在本方案考虑之列
数据存储
先设计一个字典表,把语言映射关系存储起来,后再放到redis里或者本地内存中; 需要把中文翻译到其他语言时, 把中文md5一下,然后去找对应的翻译
CREATE TABLE `finance_i18n_resource` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`key` varchar(255) NOT NULL DEFAULT '' COMMENT '默认中文加密后唯一键',
`cn_value` varchar(255) NOT NULL DEFAULT '' COMMENT '中文',
`en_value` varchar(255) NOT NULL DEFAULT '' COMMENT '英文',
`jp_value` varchar(255) NOT NULL DEFAULT '' COMMENT '日文',
PRIMARY KEY (`id`),
UNIQUE KEY `key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
方案说明
引入三个注解 @TranslateData, @TranslateField, @TranslateTrigger
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TranslateData {}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TranslateField {}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TranslateTrigger {
int isRecursion() default 0;
}
假如我们目前有个TranslateTestVo, 其中字段noTranslateSting是不需要翻译的, name需要翻译, statusTitle需要翻译,那么我们只需要在对应的字段上加上@TranslateField 这个注解作为标记;考虑到有的vo 可能所有字段都不需要翻译,我们就没必要去扫描这个vo的所有字段是否有@TranslateField注解; 那么我们引入第二个注解@TranslateData, 如果这个对象里有需要翻译的字段,那么需要在类上加上@TranslateData这个注解,以标记这个vo需要翻译, 接下来就需要扫描这个vo里含有@TranslateField 注解的字段;否则不扫描, 以避免不必要的扫描
@Data
@TranslateData
public class TranslateTestVo {
private Integer id;
private String noTranslateSting;
@TranslateField
private String name;
private Integer status;
@JsonProperty("status_title")
@TranslateField
private String statusTitle;
@TranslateField
private TranslateTestVo children;
private TranslateTestVo children2;
@TranslateField
@JsonProperty("string_list")
private List<String> stringList;
@TranslateField
@JsonProperty("string_map")
private Map<String, String> stringmap;
@TranslateField
@JsonProperty("string_arr")
private String[] stringArr;
@TranslateField
@JsonProperty("object_list")
private List<TranslateTestVo> objectList;
@TranslateField
@JsonProperty("object_map")
private Map<String, Object> objectMap;
public String getStatusTitle(){
if(statusTitle!=null){
return statusTitle;
}
if(status == null){
return "";
}
if(status == ServiceApiFileServiceDownloadTaskManagerConstant.TASK_STATUS.NOT_START.getValue()) {
return ServiceApiFileServiceDownloadTaskManagerConstant.TASK_STATUS.NOT_START.getName();
}else if(status == ServiceApiFileServiceDownloadTaskManagerConstant.TASK_STATUS.EXCECUTING.getValue()) {
return ServiceApiFileServiceDownloadTaskManagerConstant.TASK_STATUS.EXCECUTING.getName();
}else if(status == ServiceApiFileServiceDownloadTaskManagerConstant.TASK_STATUS.FINISHED.getValue()) {
return ServiceApiFileServiceDownloadTaskManagerConstant.TASK_STATUS.FINISHED.getName();
}else if(status == ServiceApiFileServiceDownloadTaskManagerConstant.TASK_STATUS.FAILED.getValue()) {
return ServiceApiFileServiceDownloadTaskManagerConstant.TASK_STATUS.FAILED.getName();
}else {
return "";
}
}
}
接下来我们就需要实现一个这样的针对对象的翻译工具了
该方法具有以下特点::
- 可以对单个String进行翻译
- 可以对String[]进行翻译
- 可以对 List< String> 进行翻译
- 可以对Map<String, String>进行翻译
- 可以对已经标记好的自定义对象进行翻译
- 支持递归
具体实现:
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public final class ObjectTranslationUtil {
private ObjectTranslationUtil() { }
/* ---------- 对外 API ---------- */
/** 非递归翻译 */
public static <T> T translate(T obj) {
return (T) translateInternal(obj, false);
}
/** 递归翻译 */
public static <T> T recursionTranslate(T obj) {
return (T) translateInternal(obj, true);
}
/* ---------- 内部实现 ---------- */
private static Object translateInternal(Object obj, boolean recursion) {
if (obj == null) return null;
if (obj instanceof String) {
return doTranslate((String) obj);
}
if (obj instanceof String[]) {
String[] arr = (String[]) obj;
String[] newArr = new String[arr.length];
for (int i = 0; i < arr.length; i++) {
newArr[i] = doTranslate(arr[i]);
}
return newArr;
}
if (obj instanceof List<?>) {
return handleList((List<?>) obj, recursion);
}
if (obj instanceof Map<?, ?>) {
return handleMap((Map<?, ?>) obj, recursion);
}
handleObject(obj, recursion);
return obj;
}
/* ---------- 容器处理 ---------- */
private static List<Object> handleList(List<?> list, boolean recursion) {
List<Object> res = new ArrayList<>(list.size());
for (Object e : list) {
if (e instanceof String) {
String t = doTranslate((String) e);
res.add(t);
} else {
if(recursion){
Object translated = translateInternal(e, recursion);
res.add(translated);
}else{
res.add(e);
}
}
}
return res;
}
private static Map<Object, Object> handleMap(Map<?, ?> map, boolean recursion) {
Map<Object, Object> res = new HashMap<>(map.size());
for (Map.Entry<?, ?> e : map.entrySet()) {
if (e.getValue() instanceof String) {
String t = doTranslate((String) e.getValue());
res.put(e.getKey(), t);
} else {
if(recursion){
Object val = translateInternal(e.getValue(), recursion);
res.put(e.getKey(), val);
}else{
res.put(e.getKey(), e);
}
}
}
return res;
}
/* ---------- 普通对象处理 ---------- */
private static void handleObject(Object obj, boolean recursion) {
doRecursiveTranslate(obj, recursion);
}
/* ---------------------------------------------------------
2. 递归模式处理当前对象
--------------------------------------------------------- */
private static void doRecursiveTranslate(Object obj, Boolean isRecursion) {
boolean forceStringOnly = isRecursion?false:true;
Class<?> clazz = obj.getClass();
while ( clazz != Object.class ) {
for (Field field : clazz.getDeclaredFields()) {
if (!field.isAnnotationPresent(TranslateField.class)) continue;
processField(field, obj, forceStringOnly);
}
clazz = clazz.getSuperclass();
}
}
/* ---------------------------------------------------------
3. 处理单个字段的主控逻辑
--------------------------------------------------------- */
private static void processField(Field field, Object obj, boolean forceStringOnly) {
try {
field.setAccessible(true);
Object value = field.get(obj);
Class<?> type = field.getType();
/* -------- 1) String -------- */
if (String.class.equals(type)) {
String translated = translateString(field, obj, value);
if (translated != null) {
field.set(obj, translated);
}
return;
}
if (value == null) return;
//
// if (value instanceof List<String>) {
//
// }
/* -------- 3) List / String[] -------- */
if (value instanceof List) {
// handleList(field, obj, (List<?>) value);
if (((List) value).get(0) instanceof String){
List<String> newList = new ArrayList<>();
Integer count = ((List) value).size();
for (int i = 0; i < count; i++) {
Object e = ((List) value).get(i);
String t = doTranslate((String) e);
newList.add(t);
}
field.set(obj, newList);
}else {
if (forceStringOnly) return;
handleList((List<?>) value);
}
} else if (type.isArray() && type.getComponentType() == String.class) {
log.info("=============66666==========");
handleStringArray(value);
}
/* -------- 4) Map -------- */
else if (value instanceof Map) {
if (forceStringOnly) return;
handleMap(field, obj, (Map<?, ?>) value);
}
/* -------- 5) 普通对象递归 -------- */
else {
if (forceStringOnly) return;
handleObject(value);
}
} catch (Exception e) {
log.error("处理字段 {} 失败", field.getName(), e);
}
}
/* ---------------------------------------------------------
4. 统一翻译 String:优先字段值,空则调用 getter
--------------------------------------------------------- */
private static String translateString(Field field, Object obj, Object original) {
try {
if (original != null) {
return doTranslate((String) original);
}
// 字段为 null,尝试 getter
String getterName = "get" + StringUtils.capitalize(field.getName());
Method getter = obj.getClass().getMethod(getterName);
Object getterValue = getter.invoke(obj);
if (getterValue != null) {
return doTranslate((String) getterValue);
}
} catch (NoSuchMethodException ignored) {
// 没有 getter 正常跳过
} catch (IllegalAccessException | InvocationTargetException e) {
log.error("translateString 失败", e);
}
return null;
}
/* ---------------------------------------------------------
5. 处理 List<String>
--------------------------------------------------------- */
// @SuppressWarnings("unchecked")
// private static void handleList(Field field, Object obj, List<?> list) {
// Integer count = list.size();
// Object fe = list.get(0);
// if (fe instanceof String){
// List<String> newList = new ArrayList<>();
// for (int i = 0; i < count; i++) {
// Object e = list.get(i);
// String t = doTranslate((String) e);
// newList.add(t);
// }
// try {
// field.set(obj, newList);
// } catch (IllegalAccessException e) {
// e.printStackTrace();
// }
// }
//
// for (int i = 0; i < count; i++) {
// Object e = list.get(i);
// handleObject(e);
// }
// }
private static void handleList(List<?> list) {
for (int i = 0; i < list.size(); i++) {
Object e = list.get(i);
handleObject(e);
}
}
/* ---------------------------------------------------------
6. 处理 String[]
--------------------------------------------------------- */
private static void handleStringArray(Object array) {
int len = Array.getLength(array);
for (int i = 0; i < len; i++) {
String e = (String) Array.get(array, i);
if (e != null) {
Array.set(array, i, doTranslate(e));
}
}
}
/* ---------------------------------------------------------
7. 处理 Map
--------------------------------------------------------- */
@SuppressWarnings("unchecked")
private static void handleMap(Field field, Object obj, Map<?, ?> map) {
Map<Object, Object> newMap = new HashMap<>(map);
for (Map.Entry<?, ?> entry : newMap.entrySet()) {
Object v = entry.getValue();
if (v instanceof String) {
String t = doTranslate((String) v);
((Map<Object, Object>) newMap).put(entry.getKey(), t);
} else {
handleObject(v);
}
}
try {
field.set(obj, newMap);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/* ---------------------------------------------------------
8. 普通对象递归
--------------------------------------------------------- */
private static void handleObject(Object obj) {
if (obj != null
&& obj.getClass().isAnnotationPresent(TranslateData.class)) {
doRecursiveTranslate(obj, true);
}
if (obj != null && obj instanceof List ) {
handleList((List<?>) obj);
}
if (obj != null && obj instanceof String[] ) {
handleStringArray(obj);
}
}
/* ---------- 翻译实现 ---------- */
private static String doTranslate(String origin) {
return TranslateUtil.doTranslate(origin);
}
}
可以使用这两个方法直接手工翻译:ObjectTranslationUtil.recursionTranslate(yourObject),ObjectTranslationUtil.translate(yourObject)
接下来我们引入AOP
增加一个 TranslateTriggerAspect
@Slf4j
@Aspect
@Component
@Order(0) // 如有必要,可调整执行顺序
public class TranslateTriggerAspect {
/**
* 环绕通知:拦截所有带 @TranslateTrigger 的方法
*/
@Around("@annotation(translateTrigger)")
public Object around(ProceedingJoinPoint pjp,
TranslateTrigger translateTrigger) throws Throwable {
// 1. 执行原方法,拿到返回值
Object result = pjp.proceed();
// todo 通过threadloacl获取翻译设置,如果获取不到翻译信息, 则不翻译
// 2. 根据注解选择是否递归
boolean recursion = translateTrigger.isRecursion() == 1;
// 3. 翻译返回值
Object translated = recursion
? ObjectTranslationUtil.recursionTranslate(result)
: ObjectTranslationUtil.translate(result);
log.debug("Translate result: original={}, translated={}", result, translated);
return translated;
}
}
当我们需要对某个函数的返回结果进行翻译时,在该函数上加上@TranslateTrigger 这个注解即可
测试例子:
@Override
public TranslateTestVo getOriginVo() {
TranslateTestVo vo = getVo(1);
List<TranslateTestVo> objectlist = new ArrayList<>();
objectlist.add(getVo(11));
objectlist.add(getVo(12));
Map<String, Object> map = new HashMap<>();
map.put("mapObject", getVo(21));
map.put("mapString", "测试666");
vo.setObjectList(objectlist);
vo.setObjectMap(map);
vo.setChildren(getVo(233));
vo.setChildren2(getVo(6666));
return vo;
}
@Override
public TranslateTestVo getTranslateVo() {
TranslateTestVo testVo = getOriginVo();
return ObjectTranslationUtil.recursionTranslate(testVo);
}
@Override
@TranslateTrigger(isRecursion = 1)
public TranslateTestVo apsectTranslateVo() {
return getOriginVo();
}
public TranslateTestVo getVo(Integer id) {
String[] stringArr = new String[]{"测试一", "测试二"};
List<String> stringList = List.of("测试一", "测试二");
// List<String> stringList = new ArrayList<>();
// stringList.add("测试一");
// stringList.add("测试二");
Map<String, String> StringMap = Map.of("key1", "测试1", "key2", "测试二");
TranslateTestVo vo = new TranslateTestVo();
vo.setId(id);
vo.setName("测试");
vo.setNoTranslateSting("本内容不翻译");
vo.setStatus(1);
vo.setStringArr(stringArr);
vo.setStringList(stringList);
vo.setStringmap(StringMap);
return vo;
}
@Override
@TranslateTrigger(isRecursion = 1)
public List<TranslateTestVo> listTranslateVo() {
List<TranslateTestVo> objectlist = new ArrayList<>();
objectlist.add(getOriginVo());
objectlist.add(getOriginVo());
return objectlist;
}
如此我们就可以灵活方便的快乐的进行翻译了, 基本不需要改动业务逻辑,只需要两步操作.
1、找到需要翻译的对象进行标记.
2、手动执行, 或者通过AOP的方式,在对应的函数上做标记
结语
方法虽然方便灵活,但是也容易被滥用, 最好在最后一层进行翻译,避免因为方法之间的相互引用,产生重复翻译,不必要的翻译,不该翻译等一系列问题