1. 程式人生 > >使用angularjs1.x構建前臺開發框架(三)——國際化

使用angularjs1.x構建前臺開發框架(三)——國際化

通常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)。

在選單檢視menu.html中新增一個顯示詞條的按鈕(第15行):
<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.頁面顯示資訊時根據上一步生成的國際化檔案路徑來顯示資訊。

另外,當我們進行國際化切換時,整個應用的語言都會發生切換,包括所有路由的子頁面,而不僅僅是主選單。至此,動態控制國際化語言的功能就完成了。