通常web应用需要支持中英文两种语言,而高级的应用则可能需要支撑更多,本章主要介绍如何让之前提供的前台开发框架支持国际化。
web应用的国际化分为后台国际化和前台国际化,下面简单介绍一下笔者的理解:
前台国际化:
1.前台界面显示的固定词条(页面固定显示的字符串)、枚举词条(页面明确需要显示的字符串集合,但显示哪一个枚举值由后台传递的枚举值id决定),比如csdn写blog的页签和文章类型(这里只是举例说明这样的词条可以在前台进行国际化,至于csdn网站是否真的是在前台进行国际化则未知):
2.前台显示的请求错误信息(通常大型的web应用即使后台发生错误,为了让用户能明确感知具体是什么原因导致请求无法正常响应,会在响应中携带具体的错误消息或错误消息id,如果响应消息携带的是错误消息id,然后由前台来翻译,则也算作前台国际化),下面给出一个这样的例子:
后台servlet(当接收到请求的时候返回一个错误消息id,当然,在实际的应用开发过程中,接口返回错误消息id一般是在httpcode为500的时候,本例中为了方便说明国际化直接在httpcode为200的时候返回了错误消息id):
package test;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet implementation class getData
*/
@WebServlet("/getData")
public class getData extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public getData() {
super();
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"errorId\":\"1\"}");
writer.flush();
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
}
}
前台html(访问后台servet,判断错误消息id,并转换成真正的错误消息,然后写入页面的imput框,这个转换过程是在前台完成的,所以为前台国际化):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="lib/jquery-3.0.0.min.js"></script>
</head>
<body>
<input type="text" id="test" value="">
</body>
</html>
<script>
$.ajax({
type: "GET",
url: "getData",
dataType: "json",
success: function(data){
console.log(JSON.stringify(data));
if(data.errorId == "1"){
$("#test").val("zookeeper连接失败");
}
},
error: function(){
console.log("error");
}
});
</script>
当服务运行时,访问对应的html,可以看到在input框中显示了错误消息“zookeeper连接失败”:
后台国际化:
如前例中错误消息id的转换过程在后台完成,把错误消息字符串直接返回给前台,则算作后台国际化,当然,后台国际化需要前台在请求的时候携带语言类型参数(接口应支持并解析)。另一种情况是请求的返回信息为非枚举值(如请求的信息是用户之前录入的数据,前台无法进行枚举翻译),也算作后台国际化。后台国际化不在本章的范围内,不多赘述。
接下来介绍国际化功能在框架中是如何应用的——实现页面显示的字符串从国际化文件中获取,首先看一下国际化文件在项目代码结构的位置:
i18n即为国际化文件目录,里面按中英文分为两个目录,每个目录都包含一个词条文件和一个错误码文件(这样的文件结构也方便后续语种的扩展,增加支持一个语种,只需要增加一个语种目录并在语种切换功能上稍作调整即可)。
然后看一下中文的词条文件内容(language.js,由于词条文件和错误码文件的内容和用法都类似,下面就以中文词条文件的内容和用法为例来进行说明):
/**
* Created by 李庆 on 2016/10/16.
*/
define([],function(){
var language = {
"login_tip":"登录"
};
return language;
});
定义了一个language对象,对象的属性即为词条id和翻译后的字符串,当然,在定义词条的时候必须保证所有的词条id是不重复的,否则会导致词条相互覆盖。另外要注意一点,这个词条文件必须以utf-8的格式保存,否则在使用时中文会显示为乱码。
注意:在实际的开发过程中,由于没有好的监督机制,当词条数量达到一定规模时,开发者在新增一个词条的时候往往不会去检索一下这个词条是否已经被定义过(或有意义相近的词条)而直接新增(当然会保证词条id不重复),结果就导致词条文件中存在大量的冗余词条,即多个不同的词条id对应了相同的翻译后字符串,这就需要git的代码检视人多加留意,因为一旦发生这种问题,整改会耗费极大的人力。
接下来看一下词条文件在main.js中的定义(paths部分新增language的加载路径,第10行):
/**
* Created by 李庆 on 2016/10/6.
*/
require.config({
"baseUrl":"lib",
"paths":{
"angular":"angular",
"jquery":"jquery-3.1.1",
"angular-ui-router":"angular-ui-router",
"language":"../i18n/zh/language"
},
在框架控制器frameworkCtrl.js中声明对词条文件的依赖,并把词条文件内容纳入自己的作用域(第4行和第9行):
/**
* Created by 李庆 on 2016/10/6.
*/
define(["language"],function(i18n){
var frameworkControl=["$rootScope","$scope",function($rootScope,$scope){
$rootScope.menus={
"url":"framework/views/menu.html"
};
$scope.i18n = i18n;
}];
return frameworkControl;
})
注意:这里把词条文件纳入作用域的行为,在视图中要使用词条时是必要的(视图中无法直接使用控制器依赖的js,只能使用作用域内定义的变量),如果仅在控制器内部使用词条,则无需纳入(控制器内部可以直接使用i18n,而不是使用$scope.i18n)。
<div style="min-width: 1280px; max-width: 1440px; margin: 58px auto 0; border:1px solid #F00">
<div>
<ul>
<li>
<a ui-sref="login">login</a>
</li>
<li>
<a ui-sref="c1">c1</a>
</li>
<li>
<a ui-sref="c2">c2</a>
</li>
</ul>
</div>
<button>{{i18n.login_tip}}</button>
</div>
然后访问index.html,在菜单界面显示了一个登陆按钮,这表示读取并显示词条文件对应的词条成功:
国际化带来的好处:
1.页面多处使用的相同词条,如发生变更无需逐一修改,只需要调整词条文件即可;
2.只需要切换一下词条文件的引用路径即可完成语言的切换(对应上例即为修改paths中的language加载路径)。
当然,上面说明的语言切换必须修改main.js中paths对象的language属性,显然在实际的应用中通过修改代码来实现语言切换是不合适的,接下来看一下如何在web中动态切换语言:之前我们在主菜单界面增加了一个显示登录词条的按钮,然后我们在主菜单页面上再增加两个用于语言切换的按钮,分别对应把中文切换成英文和把英文切换成中文两个功能,当点击时,对应之前给出的登录按钮信息会发生变化(当然,实际的web应用一般会使用下拉框来做语言切换,用按钮只是为了方便说明功能)。
在menu.html增加两个按钮,分别对应把语言切换为英文和把语言切换为中文功能(第17、18行):
<div style="min-width: 1280px; max-width: 1440px; margin: 58px auto 0; border:1px solid #F00">
<div>
<ul>
<li>
<a ui-sref="login">login</a>
</li>
<li>
<a ui-sref="c1">c1</a>
</li>
<li>
<a ui-sref="c2">c2</a>
</li>
</ul>
</div>
<button>{{i18n.login_tip}}</button>
<button ng-click="change('en')">english</button>
<button ng-click="change('zh')">中文</button>
</div>
在frameworkCtrl.js中增加change函数的定义(第11~16行):
/**
* Created by 李庆 on 2016/10/6.
*/
define(["language"],function(i18n){
var frameworkControl=["$rootScope","$scope",function($rootScope,$scope){
$rootScope.menus={
"url":"framework/views/menu.html"
};
$scope.i18n = i18n;
$scope.change = function(language){
var expires = new Date(9999,12,31).toUTCString();
document.cookie = "lan="+language+"; "+expires;
location.reload();
}
}];
return frameworkControl;
})
change函数根据传递的参数改变cookie中的lan变量(cookie中的lan用于保存当前使用的语言类型,在change函数中还为lan设置了永不超期),并执行页面的重载。
在main.js中增加读取cookie的操作,并根据读取到的语言类型来决定使用哪一套语言(第4~18行以及26行):
/**
* Created by 李庆 on 2016/10/6.
*/
var lan = "";
var name = "lan=";
var ca = document.cookie.split(";");
for(var i=0;i<ca.length;i++){
var c = ca[i];
while(c.charAt(0) == " "){
c = c.substring(1);
}
if(c.indexOf(name) != -1){
lan = "../i18n/"+ c.substring(name.length, c.length)+"/language";
}
}
if(lan === ""){
lan = "../i18n/zh/language";
}
require.config({
"baseUrl":"lib",
"paths":{
"angular":"angular",
"jquery":"jquery-3.1.1",
"angular-ui-router":"angular-ui-router",
"language":lan
},
"shim":{
"angular":{
"deps":["jquery"],
"exports":"angular"
},
"angular-ui-router":{
"deps":["angular"]
},
"jquery":{
"exports":"$"
}
}
});
require(["../framework/framework"],function(framework){
var injector = angular.bootstrap($("html"),[framework.name]);
});
第4~18行功能为读取cookie中的lan,并根据lan生成语言路径,如果是首次加载,cookie中没有lan,则默认设置语言为中文;
第26行则是设置当前使用的语言路径(之前是写死为../i18n/zh/language);
前面的章节已经说过main.js是主函数,现在我们增加的是全局性的国际化配置,因此放在main.js是合适的。
然后重新访问index.html(首次访问时),按钮显示为登录:
点击english按钮,页面发生重载,按钮显示为login:
这个功能简单来说可以分以下几步:
1.通过按钮点击改变cookie中的语言类型,并触发页面重载;
2.页面重载时读取cookie并实时生成需要加载的国际化文件路径;
3.页面显示信息时根据上一步生成的国际化文件路径来显示信息。
另外,当我们进行国际化切换时,整个应用的语言都会发生切换,包括所有路由的子页面,而不仅仅是主菜单。至此,动态控制国际化语言的功能就完成了。