服务端实现多语言翻译

yuyu888 于 2025-08-07 发布

前言

随着公司业务的扩展, 在日本,韩国, 新加坡成立了众多海外公司,但是我们的合同,请款,报销,以及一些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 "";
        }
    }
}

接下来我们就需要实现一个这样的针对对象的翻译工具了

该方法具有以下特点::

  1. 可以对单个String进行翻译
  2. 可以对String[]进行翻译
  3. 可以对 List< String> 进行翻译
  4. 可以对Map<String, String>进行翻译
  5. 可以对已经标记好的自定义对象进行翻译
  6. 支持递归

具体实现:

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的方式,在对应的函数上做标记

结语

方法虽然方便灵活,但是也容易被滥用, 最好在最后一层进行翻译,避免因为方法之间的相互引用,产生重复翻译,不必要的翻译,不该翻译等一系列问题