初步实现 I18N 插件

本文是《轻量级 Java Web 框架架构设计》的系列博文。

在 JSTL、Struts、Spring 中都提供了 I18N(国际化)支持,也就是说,同一个页面可支持多种语言,这是一个非常有用的特性。当然,底层都是使用的 Java 提供的 ResourceBundle 技术,通过设置不同的 Locale 来访问具体的语言包(实际上就是一个 properties 文件),这样就实现了国际化支持。

对于语言包还有一些命名规则,比如:中文语言包为 xxx_zh_CN.properties,英文语言包为 xxx_en_US.properties。xxx 表示语言包的前缀(可以不要),zh/en 表示语言,CN/US 表示国家。

市面上这些工具仅提供了 JSP 中的国际化支持,实际上它们都是通过 JSP 标签实现的。那么如果想在 JS 中使用 Java 语言包,可以吗?恐怕不太现实吧,因为 JS 是无法读取 WEB-INF 下 classes 中的 properties 文件的。

所以,如果想让 JSP 与 JS 都实现国际化支持,必须想一个办法让 JS 可以读取语言包。我们知道 JS 是可以读取 JSON 的,那么就可以将 Java 语言包通过代码生成技术,生成为对应的 JS 语言包(JSON 格式),这样问题是否就能解决了呢?

下面就是我的解决方案。

第一步:创建一个 I18NPlugin 插件类

该插件类继承于 Smart 框架提供的 Plugin 接口,所以可以进行初始化(实现 init 方法即可)。我就是想在初始化的时候,读取 Java 语言包,从而生成 JS 语言包。详细的代码如下:

public class I18NPlugin implements Plugin {

    @Override
    public void init() {
        // 生成 JS 语言包
        String appBasePath = ClassUtil.getClassPath() + "../../";
        generateJS(appBasePath);
    }

    public static void generateJS(String appBasePath) {
        // 定义相关根路径
        String propsBasePath = appBasePath + I18NConstant.I18N_PROPS_PATH;
        String jsBasePath = appBasePath + I18NConstant.I18N_JS_PATH;
        // 获取属性文件目录
        File propsBaseDir = new File(propsBasePath);
        if (propsBaseDir.exists()) {
            // 获取所有属性文件
            String[] propsFileNames = propsBaseDir.list();
            if (ArrayUtil.isNotEmpty(propsFileNames)) {
                // 遍历所有属性文件
                for (String propsFileName : propsFileNames) {
                    // 定义 JS 文件路径
                    String jsFilePath = jsBasePath + propsFileName.substring(0, propsFileName.lastIndexOf(".")) + ".js";
                    // 从属性文件中加载相关数据
                    Map<String, String> map = new HashMap<String, String>();
                    Properties props = FileUtil.loadPropFile(I18NConstant.I18N_PATH + propsFileName);
                    Enumeration names = props.propertyNames();
                    while (names.hasMoreElements()) {
                        String name = (String) names.nextElement();
                        String value = props.getProperty(name);
                        map.put(name, value);
                    }
                    // 将数据转换为 JSON 并写入 JS 文件
                    String jsFileContent = "window.I18N = " + JSONUtil.toJSON(map) + ";";
                    FileUtil.writeFile(jsFilePath, jsFileContent);
                }
            }
        }
    }
}

可见,通过读取 classpath 下 i18n 目录中所有的 properties 文件(Java 语言包),并遍历这些文件,然后生成对应的 js 文件(JS 语言包)。

这里用到了 I18NConstant 常量类,代码如下:

public interface I18NConstant {

    String SYSTEM_LANGUAGE = "system_language";
    String COOKIE_LANGUAGE = "cookie_language";

    String I18N_PATH = "i18n/";
    String I18N_PROPS_PATH = "/WEB-INF/classes/i18n/";
    String I18N_JS_PATH = "/www/asset/script/i18n/";

    boolean RELOAD_FLAG = true;
}

下面就是开发人员需要编写的 Java 语言包:

i18n_en_US.properties
i18n_zh_CN.properties
common.smart_sample=Smart Sample
common.copyright=Copyright Reserved © 2013
common.language=System Language
common.logout=Logout
common.logout_confirm=Do you want to logout system?
common.action=Action
common.edit=Edit
common.delete=Delete
common.save=Save
common.cancel=Cancel
product=Product
customer=Customer
customer.customer_list=Customer List
customer.new_customer=New Customer
customer.view_customer=View Customer
customer.edit_customer=Edit Customer
customer.customer_name=Customer Name
customer.description=Description
customer.delete_confirm=Do you want to delete customer {0}?
common.smart_sample=Smart 示例
common.copyright=版权所有 © 2013
common.language=系统语言
common.logout=注销
common.logout_confirm=你想注销系统吗?
common.action=操作
common.edit=编辑
common.delete=删除
common.save=保存
common.cancel=取消
product=产品
customer=客户
customer.customer_list=客户列表
customer.new_customer=新增客户
customer.view_customer=查看客户
customer.edit_customer=编辑客户
customer.customer_name=客户名称
customer.description=描述
customer.delete_confirm=你确定删除客户 {0} 吗?

下面就是 I18N 插件自动生成的 JS 语言包:

i18n_en_US.js
i18n_zh_CN.js
window.I18N = {
    "common.delete": "Delete",
    "customer.customer_list": "Customer List",
    "customer.new_customer": "New Customer",
    "common.language": "System Language",
    "common.logout": "Logout",
    "common.save": "Save",
    "customer": "Customer",
    "common.copyright": "Copyright Reserved © 2013",
    "common.action": "Action",
    "common.cancel": "Cancel",
    "common.logout_confirm": "Do you want to logout system?",
    "product": "Product",
    "customer.description": "Description",
    "customer.edit_customer": "Edit Customer",
    "customer.delete_confirm": "Do you want to delete customer {0}?",
    "customer.view_customer": "View Customer",
    "common.edit": "Edit",
    "common.smart_sample": "Smart Sample",
    "customer.customer_name": "Customer Name"
};
window.I18N = {
    "common.delete": "删除",
    "customer.customer_list": "客户列表",
    "customer.new_customer": "新增客户",
    "common.language": "系统语言",
    "common.logout": "注销",
    "common.save": "保存",
    "customer": "客户",
    "common.copyright": "版权所有 © 2013",
    "common.action": "操作",
    "common.cancel": "取消",
    "common.logout_confirm": "你想注销系统吗?",
    "product": "产品",
    "customer.description": "描述",
    "customer.edit_customer": "编辑客户",
    "customer.delete_confirm": "你确定删除客户 {0} 吗?",
    "customer.view_customer": "查看客户",
    "common.edit": "编辑",
    "common.smart_sample": "Smart 示例",
    "customer.customer_name": "客户名称"
};

可见,JS 语言包与 Java 语言包完全对应,只不过条目排列顺序不一致罢了(因为是根据 HashMap 生成的 JSON)。

现在 Java 与 JS 的语言包可以同步了,当然只能在启动 Tomcat 时,才会执行代码生成。那么就有以下 2 个问题:

  1. 如何让用户自由切换语言包,并且记住自己上次所做的选择?
  2. 为了提高开发效率,若修改了 properties 文件,如何实现 reload,并自动生成 js 文件?(注意:仅针对开发环境而言,对于生产环境无需这样处理)

在解决这两个问题之前,有必要先在 JSP 中引入 JSTL 的国际化标签库,因为它实在太有用了。

第二步:在 JSP 中使用 JSTL 国际化标签库

首先需要引入一个名为 fmt 的标签库:

<%@ taglib prefix="f" uri="http://java.sun.com/jsp/jstl/fmt" %>

不妨定义它的前缀为 f,其实就是该标签的简称。有些人喜欢命名为 fmt,这都无所谓了。

我们就可以在 JSP 中使用 f:message 标签来实现国际化支持了。用法如下:

<f:message key="common.smart_sample"/>

以上代码会在相应的 Java 语言包中根据 key 寻找 value,可能为 Smart Sample(英文环境)或 Smart 示例(中文环境)。

如何分辨是哪种语言环境呢?需要再使用 f:message 标签设置默认语言包:

<f:setBundle basename="i18n.i18n_${system_language}"/>

其中定义了一个 system_language 的变量,该变量是谁来负责初始化呢?我们不妨带着这个问题进入下一步吧。

第三步:提供一个切换系统语言的控件

我们可以将用户设置的系统语言会存入到 Cookie 中,也可以对 Cookie 设置一个有效期,不妨让它尽可能的长一些,比如一年。需要说明的是,无法设置为永久,因为 IE 不支持。

不妨在页面的 footer 处,提供一个系统语言切换的控件,如下图:

JSP 代码如下:

<%@ page pageEncoding="UTF-8" %>

<div id="footer">
    <div id="copyright">
        <span><f:message key="common.copyright"/></span>
    </div>
    <div id="language">
        <span><f:message key="common.language"/>:</span>
        <a href="#" data-value="zh_CN">中文</a>
        <span>|</span>
        <a href="#" data-value="en_US">English</a>
    </div>
</div>

谁负责将系统语言存入 Cookie?当然是 JS 了。不妨直接在 global.js 中扩展一下吧。

$(function() {
...
    // 切换系统语言
    $('#language').find('a').click(function() {
        var language = $(this).data('value');
        $.cookie('cookie_language', language, {expires: 365, path: '/'});
        location.reload();
    });
});

将 id 为 language 的元素下所有的 a(链接)绑定 click 事件,并将 a 中的 data-value 属性值放入到 Cookie 中,最后刷新页面,就会自动切换系统语言。

看似挺神奇的,在 JS 里只是告诉了 Cookie 当前的系统语言,就可以改变整个系统的语言环境了。后端应如何实现呢?

第四步:使用 Filter 获取系统语言并重新加载语言包

每当用户发出一个请求(比如上面进行 Cookie 操作后的刷新页面),我们都可获取用户当前所选择的系统语言,可以根据以下策略进行获取:

  1. 首先从 Cookie 中寻找
  2. 然后从浏览器中寻找
  3. 最后从操作系统中寻找
不妨提供一个 Filter 吧,让它拦截所有的请求。
@WebFilter("/*")
public class I18NFilter implements Filter {

    private static final String wwwPath = ConfigHelper.getStringProperty(Constant.APP_WWW_PATH);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 获取请求路径
        HttpServletRequest req = (HttpServletRequest) request;
        String requestPath = WebUtil.getRequestPath(req);
        if (!requestPath.startsWith(wwwPath)) {
            // 获取系统语言并放入 Request 中
            String systemLanguage = getSystemLanguage((HttpServletRequest) request);
            request.setAttribute(I18NConstant.SYSTEM_LANGUAGE, systemLanguage);
            // 判断是否重新
            if (I18NConstant.RELOAD_FLAG) {
                // 清理 ResourceBundle 缓存
                ResourceBundle.clearCache();
                // 生成 JS 语言包
                String appBasePath = req.getServletContext().getRealPath("/");
                I18NPlugin.generateJS(appBasePath);
            }
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }

    private static String getSystemLanguage(HttpServletRequest request) {
        // 先从 Cookie 中获取系统语言
        String language = WebUtil.getCookie(request, I18NConstant.COOKIE_LANGUAGE);
        if (StringUtil.isEmpty(language)) {
            // 若为空,则获取浏览器首语言
            language = request.getLocale().toString();
            if (StringUtil.isEmpty(language)) {
                // 若为空,则获取操作系统语言
                language = Locale.getDefault().toString();
            }
        }
        return language;
    }
}

在 doFilter 方法中,做了两件非常重要的事情,其中第一件事情是必须要做的,第二件事情是可选的。

  1. 第一件事情就是初始化系统语言,也就是首先获取系统语言,然后将其放入 Request 属性中,名称就是 system_language(放在 I18NConstant 常量中了)。
  2. 第二件事情其实是为我们开发人员做的,在开发阶段,我们可以将 RELOAD_FLAG 设为 true,这样每次请求都会清理 ResourceBundle 缓存并且生成 JS 语言包。

需要注意的是,Filter 会拦截所有的请求,包括:动态请求(Action 请求)、静态请求(JS、CSS、图片),所以我们需要过滤掉静态请求,也就是 requestPath 中前缀为 /www/ 的请求。

还需要注意 getSystemLanguage 方法,它是获取系统语言的具体算法,其中 Cookie 级别是最高的,然后是浏览器语言,最后才是操作系统语言。

通过以上步骤,JSP 的国际化已经可以完全支持了,因为有 JSTL 国际化标签,还有 Java 提供的 ResourceBundle 语言包技术。那么,JS 又如何实现国际化呢?

第五步:实现 JS 国际化支持

比如,有这样一个 JS 脚本:

if (confirm('Do you want to delete customer [' + customerName + ']?')) {
    ...
}

其中不仅有需要国际化处理的文字,而且还有参数。

对于这类情况,最好能够扩展一个 jQuery 函数,让它为我们简化开发过程,我们可以这样写:

if (confirm($.i18n('customer.delete_confirm', customerName))) {
    ...
}

在 $.i18n 函数中,第一个参数 customer.delete_confirm 为 JS 语言包中的 key,从第二个参数开始的都是动态参数,可使用它们来填充语言包中的占位符。我们不妨回头看一下 JS 语言包中对应的条目是怎样的:

window.I18N = {
...
    "customer.delete_confirm": "Do you want to delete customer {0}?",
...
};

这里就是通过 customerName 去填充 {0} 的,是不是很有意思呢?那么又是如何实现的呢?可在 global.js 中添加以下代码:

$(function() {
    $.extend($, {
        i18n: function() {
            var args = arguments;
            var key = args[0];
            var value = window['I18N'][key];
            if (value) {
                if (args.length > 0) {
                    value = value.replace(/\{(\d+)\}/g, function(m, i) {
                        return args[parseInt(i) + 1];
                    });
                }
                return value;
            } else {
                return key;
            }
        }
    });
...

若在 window.I18N 变量中能够通过 key 进行匹配,那么就通过正则表达式替换其中的 {X} 占位符。若匹配不上,则直接返回 key。

最后不要忘了在 JSP 中引入所生成的 js 文件:

<script type="text/javascript" src="${BASE}/www/asset/script/i18n/i18n_${system_language}.js"></script>

同样需要从 Request 中读取 system_language 属性,才能定位到相应的 JS 语言包。

最后一步:使用 Smart I18N 插件

在您的 Maven 配置文件中添加如下代码即可:

...
        <dependency>
            <groupId>com.smart</groupId>
            <artifactId>smart-plugin-i18n</artifactId>
            <version>1.0</version>
        </dependency>
...

总结

  1. 我们只需编写 Java 语言包(properties 文件)即可,Smart I18N 插件将自动生成 JS 语言包(一个存放 JSON 数据的 js 文件)。
  2. 使用 JSTL 的 fmt 标签实现 JSP 中的国际化,使用 $.i18n 函数实现 JS 中的国际化。
  3. 在开发过程中我们可以开启 RELOAD_FLAG 标志,它将自动 reload properties 文件并同步生成 js 文件,在生产环境下请关闭此标志。

还等什么呢?赶快去使用一下 Smart I18N 插件提供的国际化支持吧!

源码地址:http://git.oschina.net/huangyong/smart-plugin-i18n

贴两个界面展示一下最终的效果:

转载于:https://my.oschina.net/huangyong/blog/179171

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值