JFinal使用技巧-半自动国际化 懒人必备

本来觉得没有太大必要分享这个功能的,因为有些涉及到业务了, 不太好拆了,而且有弊端,只适合一部分业务使用。先不说那么多, 有用得着的人自然就觉得有用了! 

JF框架里自带的 I18nInterceptor 已经很方便了, 但是需要写properties文件,,,而且需要自己翻译,如果有翻译错的地方,被客户指出来了, 还不能立刻修改,,,或者客户给到了一篇自己的翻译内容(有英语日语等其他好几国的语言。。。),我们填进去也是需要时间的,现在有IDEA了还能自动打开好几个文件协同但是填写也是蛋疼特别好几个国家的一起填。。。在以前eclipse上还得自己比对key是不是填对着。。。

那为了解决上面这些梗,我们想到的是把文件内容迁移到数据库中, 用表存起来就好了嘛! 然后了发现以前都是用一个英文单词或者一个字符串作为了国际化内容的key,这样下来导致维护的时候,看着页面不知道写的是什么东西了(英语差)。。。然后我们就用了中文作为key。。。发现非常的好维护啊!再就是自动翻译了,这个很好做,调用一下第三方的翻译接口就可以了,23333   上石马~


I18nCacheInterceptor

package com.momathink.common.interceptor;

import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.core.Const;
import com.jfinal.core.Controller;
import com.jfinal.kit.StrKit;
import com.momathink.system.i18n.I18n;
import com.momathink.system.i18n.I18nCache;
import com.momathink.system.login.LoginService;
import com.momathink.system.model.SysAccount;

/**
 * 国际化_数据库存储方式<BR>
 * 页面调用 时: #(i18n.get('xxxx'))<BR>
 * 控制器调用 时: getI18n().get('xxxx')
 * 
 * @author dufuzhong
 */
public class I18nCacheInterceptor implements Interceptor {

    public static final String LOCALE_PARA_NAME = "_locale_i18n";
    public static final String I18N             = "i18n";

    @Override
    public void intercept(Invocation inv) {

        Controller c = inv.getController();
        String locale = c.getPara(LOCALE_PARA_NAME);

        // 检查 是否携带 国际化 语言环境
        if (StrKit.notBlank(locale)) {
            c.setCookie(LOCALE_PARA_NAME, locale,
                    Const.DEFAULT_I18N_MAX_AGE_OF_COOKIE);
        } else {
            // 如果没有携带 语言环境, 从Cookie中取语言环境
            locale = c.getCookie(LOCALE_PARA_NAME);
            if (StrKit.isBlank(locale)) {
                // 如果没有携带 语言环境, 从用户中取语言环境
                SysAccount loginAccount = c
                        .getAttr(LoginService.loginAccountCacheName);
                if (loginAccount != null
                        && StrKit.notBlank(loginAccount.getLocale())) {
                    locale = loginAccount.getLocale();
                }
                // 如果没有 语言环境, 默认语言环境是 > 中文
                if (StrKit.isBlank(locale)) {
                    locale = I18n.ZH;
                }
            }
        }
        // 后续业务 可取到这个 语言容器
        c.setAttr(I18N, I18nCache.me.getI18n(locale));

        inv.invoke();
    }

}

上面使用的是i18n为命名,保留JF框架的_res取值 https://www.jfinal.com/doc/11-2

I18n

package com.momathink.system.i18n;

import java.io.Serializable;


/**
 * 语言容器<BR>
 * 装载着 某一种语言环境 的所有中文翻译语
 * 
 * @author dufuzhong
 */
public class I18n implements Serializable {

    private static final long serialVersionUID = 1L;

    public static final String ZH = "zh";

    private static final String CACHE_NAME = "I18n";

    private String locale = null;

    public I18n(String locale) {
        this.locale = locale;
    }

    /**
     * 获取中文翻译语内容
     */
    public String get(String k) {
        // 中文直接返回中文
        if (ZH.equals(getLocale())) {
            return k;
        }
        String ck = getLocale().concat(k);
        // 关联着 某一种语言 对象集合
        String v = CacheKit.get(CACHE_NAME, ck);
        if (v == null) {
            v = getV(getLocale(), k);
            CacheKit.put(CACHE_NAME, ck, v);
        }
        return v != null ? v : "待翻译:".concat(k);
    }

    public String getLocale() {
        return locale;
    }

    public boolean isLocale(String locale) {
        return this.locale.equals(locale);
    }

    /**
     * 数据库 查询
     */
    public String getV(String lang, String k) {
        return I18nServer.me.queryByZh(lang, k);
    }

}


I18nCache

package com.momathink.system.i18n;


/**
 * 国际化 缓存 管理
 * 
 * @author dufuzhong
 */
public class I18nCache {

    public static I18nCache     me         = new I18nCache();
    private static final String CACHE_NAME = "I18nDb";

    private I18nCache() {
    }

    /**
     * 根据 语言环境 取 语言容器
     */
    public I18n getI18n(String locale) {
        I18n i18n = CacheKit.get(CACHE_NAME, locale);
        if (i18n == null) {
            i18n = new I18n(locale);
            CacheKit.put(CACHE_NAME, locale, i18n);
        }
        return i18n;
    }

}

这样就OK了, 通过 

I18nCache.me.getI18n(locale)

进行调用的


然后就是数据库查询和保存了

I18nServer

package com.momathink.system.i18n;

import com.momathink.system.model.SysI18n;

/***
 * 国际化服
 * 
 * @author dufuzhong
 *
 */
public class I18nServer {

    public static final I18nServer me = new I18nServer();

    /**
     * dao私有化,禁止外面直接访问
     */
    private final SysI18n dao = new SysI18n().dao();

    public String queryByZh(String lang, String k) {
        // 开发模式: 如果数据库中没有 就自动添加一条
        SysI18n i18n = queryByZh(k);
        if (i18n == null) {
            I18nDbTask.add(k);
            return null;
        }
        if (i18n.getMy() != null) {
            return i18n.getMy();
        }
        return i18n.get(lang);
    }

    /**
     * 根据中文查询
     */
    public SysI18n queryByZh(String zh) {
        return dao.findFirst("SELECT * FROM `sys_i18n` WHERE `zh` = ? LIMIT 1",
                zh);
    }

}

再贴上数据库结构

-- ----------------------------
-- Table structure for sys_i18n
-- ----------------------------
DROP TABLE IF EXISTS `sys_i18n`;
CREATE TABLE `sys_i18n` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `zh` varchar(255) DEFAULT NULL COMMENT '中文',
  `my` varchar(255) DEFAULT NULL COMMENT '自定义',
  `en` text COMMENT '英语',
  `ru` text COMMENT '俄语',
  `cht` text COMMENT '繁体中文',
  `kor` text COMMENT '韩语',
  `jp` text COMMENT '日语',
  `fra` text COMMENT '法语',
  `vie` text COMMENT '越南语',
  `th` text COMMENT '泰语',
  `spa` text COMMENT '西班牙语',
  `ara` text COMMENT '阿拉伯语',
  `pt` text COMMENT '葡萄牙语',
  `de` text COMMENT '德语',
  `it` text COMMENT '意大利语',
  `el` text COMMENT '希腊语',
  `nl` text COMMENT '荷兰语',
  `pl` text COMMENT '波兰语',
  `bul` text COMMENT '保加利亚语',
  `est` text COMMENT '爱沙尼亚语',
  `dan` text COMMENT '丹麦语',
  `fin` text COMMENT '芬兰语',
  `cs` text COMMENT '捷克语',
  `rom` text COMMENT '罗马尼亚语',
  `slo` text COMMENT '斯洛文尼亚语',
  `swe` text COMMENT '瑞典语',
  `hu` text COMMENT '匈牙利语',
  `yue` text COMMENT '粤语',
  PRIMARY KEY (`id`),
  UNIQUE KEY `zh` (`zh`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='国际化词库';

SET FOREIGN_KEY_CHECKS = 1;

再贴上保存代码

package com.momathink.system.i18n;

import com.jfinal.kit.LogKit;
import com.momathink.common.kit.ExecKit;
import com.momathink.system.model.SysI18n;

/**
 * 国际化添加, 自动翻译 保存到 数据库 <br>
 * 增加新国际化的时候或者修改排序的时候 , 使用下面的main方法生成后, 粘贴控制台的内容就可以了
 *
 * @author dufuzhong
 */
public class I18nDbTask {

    // 在平台申请的APP_ID 详见
    // http://api.fanyi.baidu.com/api/trans/product/desktop?req=developer
    /** //杜福忠开发号 */
    private static final String APP_ID       = "手动马赛克,自己注册吧,我的就不放这里了";
    /** //杜福忠开发号 */
    private static final String SECURITY_KEY = "手动马赛克,自己注册吧,我的就不放这里了";

    private static BaiDuFanYiApi api = new BaiDuFanYiApi(APP_ID, SECURITY_KEY);

    /** 国际化 语言简写K */
    public static final String[] ARRAY      = {"zh", "en", "ru", "cht", "kor",
            "jp", "fra", "vie", "th", "spa", "ara", "pt", "de", "it", "el",
            "nl", "pl", "bul", "est", "dan", "fin", "cs", "rom", "slo", "swe",
            "hu", "yue"};
    /** 国际化 语言 名称 */
    public static final String[] ARRAY_NAME = {"中文", "英语", "俄语", "繁体中文", "韩语",
            "日语", "法语", "越南语", "泰语", "西班牙语", "阿拉伯语", "葡萄牙语", "德语", "意大利语",
            "希腊语", "荷兰语", "波兰语", "保加利亚语", "爱沙尼亚语", "丹麦语", "芬兰语", "捷克语", "罗马尼亚语",
            "斯洛文尼亚语", "瑞典语", "匈牙利语", "粤语"};

    /** 创建一个翻译任务 */
    public static void add(final String query) {
        ExecKit.submit(new Runnable() {

            @Override
            public void run() {
                
                try {
                    SysI18n i18n = new SysI18n()._setAttrs(
                            api.getTransResult(query, ARRAY[0], ARRAY));
                    if (null == I18nServer.me.queryByZh(query)) {
                        // 数据库有中文唯一检查
                        i18n.save();
                    }
                } catch (Exception e) {
                    LogKit.error("国际化翻译异常: ".concat(query), e);
                }
            }
        });
    }

}

然后就是翻译接口的代码了,用的百度翻译接口,还行访问能快点,翻译准确率一般吧,毕竟免费。。。

package com.momathink.system.i18n;

import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.jfinal.kit.HashKit;
import com.jfinal.kit.HttpKit;
import com.jfinal.kit.LogKit;

/***
 * 百度翻译接口
 * 
 * @author dufuzhong
 *
 */
public class BaiDuFanYiApi {

    private static final String TRANS_API_HOST = "http://api.fanyi.baidu.com/api/trans/vip/translate";
    private static String       CHARSET        = "UTF-8";

    private String appid;
    private String securityKey;

    public BaiDuFanYiApi(String appid, String securityKey) {
        this.appid = appid;
        this.securityKey = securityKey;
    }

    public Map<String, Object> getTransResult(String query, String from,
            String... object) {
        Map<String, String> params = buildParams(query, from);
        Map<String, Object> ret = new HashMap<String, Object>();
        for (String to : object) {
            params.put("to", to);
            String dst = getDst(HttpKit.get(TRANS_API_HOST, params));
            ret.put(to, dst);
        }
        return ret;
    }

    private Map<String, String> buildParams(String query, String from) {
        Map<String, String> params = new HashMap<String, String>();
        params.put("q", query);
        params.put("from", from);

        params.put("appid", appid);

        // 随机数
        String salt = String.valueOf(System.currentTimeMillis());
        params.put("salt", salt);

        // 签名
        String src = appid + query + salt + securityKey; // 加密前的原文
        params.put("sign", HashKit.md5(src));

        return params;
    }

    private String getDst(String transResult) {
        JSONObject jsonObject = JSON.parseObject(transResult,
                JSONObject.class);
        JSONArray jsonArray = jsonObject.getJSONArray("trans_result");
        String dst="";
        if(null!=jsonArray) {
            JSONObject object = jsonArray.getJSONObject(0);
             dst = object.getString("dst");
        }
        try {
            dst = URLDecoder.decode(dst, CHARSET);
        } catch (Exception e) {
            LogKit.error("百度翻译内容解码:" + dst + "  异常信息:", e);
        }
        return dst;
    }

}

上面大家看到还有一个异步任务工具,这个是用来保证如果本地没有的时候,去百度API调用的时候不用等待页面,继续开发自己的,等下次刷新它就自动翻译好了

package com.momathink.common.kit;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 任务线程池
 * 
 * @author dufuzhong
 */
public class ExecKit {

    private static ExecutorService exec = null;

    /**
     * 添加一个任务
     */
    public static void submit(Runnable runnable) {
        getExec().submit(runnable);
    }

    /** 开发模式才会 进入这个 */
    public static ExecutorService getExec() {
        if (exec == null) {
            synchronized (ExecKit.class) {
                if (exec == null) {
                    exec = new ThreadPoolExecutor(5, 500, 0L,
                            TimeUnit.MILLISECONDS,
                            new LinkedBlockingQueue<Runnable>(1024),
                            new ThreadFactory() {

                                @Override
                                public Thread newThread(Runnable r) {
                                    return new Thread(r);
                                }
                            }, new ThreadPoolExecutor.AbortPolicy());
                }
            }
        }
        return exec;
    }

}

上面这个不是很严谨,但是能用,而且是开发时使用的,所以影响不大。

好了,下面放几张图,大致看下调用:

image.png

这个是在模板中用的,如果是前后分离的,也是可以渲染为js国际化文件的,再渲染过去就可以了。

好了,我们一直在用百度的翻译接口,还算稳定,很多项目都是这样干的

image.png

http://api.fanyi.baidu.com/api/trans/product/desktop

这个工具吧,特别适合像我们这样的业务和系统,在已知字段的情况下:

菜单.jpg

然后客户有需要纠正的词组,客户自己就在线修改了,不用我们双方沟通这个事情了,省开发时间~


image.png

OK, 看着代码挺多, 其实没两步,重在思路!
就是通过拦截器 获取语言环境, 然后根据语言key去取语言包的内容,再根据参数中文在语言包的缓存中找,缓存没有就去数据库中找,数据库没有就去百度找。。。 和人手动操作流程没啥两样~~~

好了,又水一篇~ 路过的记得点个赞哦~


评论区

杜福忠

2019-10-17 21:06

忘说了, 有人肯定会说, 用得着这么多语言吗?。。。。我想说,我们其中有一个业务就是:语言培训。。。意思就是做外语培训的,有很多外教使用系统。。。自然就有这个需求了。。。

JFinal

2019-10-17 22:28

@杜福忠 通过百度 API 来翻译的创意很不错,面对实际问题给出与众不同的创新方案,不但技术好,而且思路开阔,将来做出好产品指日可待

此外,截图中的 UI 挺好看,用什么做的?

杜福忠

2019-10-17 23:28

@JFinal 这套系列项目是18年初 最火的前端layui 和一直火着的后端jfinal-club模板引擎布局 改造过来的,果断放弃了以前jsp笨重的布局模式,新项目都是基于这套项目框架迭代的

JFinal

2019-10-17 23:35

@杜福忠 我感觉你这个 UI 做得比 layui 官方的好看,尤其是右侧的样式很好,估计你自己有一些细节改进

enjoy 代替 JSP 后的那种爽,只有爽过的人才会明白 ^_^

JFinal

2019-10-17 23:36

@杜福忠 补充一下,由于我个人并不喜欢、也不擅长前端,所以 jfinal club 后台 UI 的 layout 与设计其实还可以更好

社区几件大事做完以后,我看能不能再改进改进

杜福忠

2019-10-17 23:51

@JFinal 是的,layui原装的菜单右侧太宽,和顶部导航也是太宽,占用了很多业务处理的地方,都被优化掉了,初版也是很丑,是我做的。。。 然后被几个客户美女老师们喷太丑,然后我们就上了设计师和专业前端给打磨了一下23333 专业的事上专业的人,才能做的更专业些~

凉凉凉凉凉

2019-10-18 10:35

一直在用,有很多项目实践,这种方式真的很爽。思路珍贵,赞赞

falost

2019-10-18 10:40

思路很棒,给点个赞哈。

zhangchuang

2019-10-19 22:06

有一天,某韩国外教 看到了 동전 한 위안 네 개 (窝窝头,一块钱四个),高呼一声 阿西吧 ♪(^∀^●)ノシ (●´∀`)♪

杜福忠

2019-10-19 23:32

@zhangchuang 这个时候,该外教兴奋的反馈他们主管,主管登录系统在国际化词库进行一下友好矫正~
OK搞定~ 刷新立刻看见效果23333

zzyong2008

2019-10-22 11:17

点赞,之前也打算这样弄,只是没有这么清晰

tuxming

2019-10-26 11:56

我也是存数据库的,不过我没调用百度翻译!我的主要目的是有什么客户觉得不合理的文字描述,他自己的修改!省的每次因为文字描述错误或者不精准还得重新发布一次。

我不同的地方是:当一个新的语言请求进来时,如果没有,就在数据库copy一份新语言,然后客户自己去翻译。

还有个不同就是:当key只不存在的时候,会将不存在的key插入到数据库,这样的话,就不用在数据单独插入了。

杜福忠

2019-10-26 17:07

@tuxming 赞!根据需求业务创造适合自己的轮子才是最好得

小徐同学

2021-07-03 10:17

这个真的挺骚的,不过前后端分离有什么好法子 ?

小徐同学

2021-07-03 10:18

也许可以基于你这个生成个json给前端。。

杜福忠

2021-07-03 16:02

@小徐同学 是了,可以直接渲染为JSON,如果支持js加载的话,还可以直接渲染js文件。。。总的来说,得看前端想咋做了

jiaxiang

2022-10-11 16:26

对于富文本编辑器发布的文章有没有什么好的自动化翻译思路呢

杜福忠

2022-10-11 17:07

@jiaxiang 我记得百度有文章翻译的接口,可以试试。 还有网页翻译的接口,都可以试试,内容翻译了还保持了文章结构

jiaxiang

2022-10-11 17:54

@杜福忠 好的,我看看,有道也有网页翻译接口