本文主要针对Egret开发的项目做详细介绍,对其它引擎开发的项目亦有参考价值。
需求背景:
1、对整个游戏项目进行多语言版本(国际化)开发,把游戏内的文字由中文换成英文;
2、尽量不要修改业务代码,至少不需要手动修改,减少工作量。
3、不要遗漏,保证准确性;
需求分析:
1、游戏内文本主要包括系统文字文本和图片文本,图片很好处理,每种语言一个文件夹,美术修改图片的文字替换即可,程序根据语言字段调用对应文件夹;
2、关键点是系统文字的文本处理,其中文本分布在 ts文件和exml文件中,得想办法批量处理这2部分文字;
方法介绍:
-
针对ts文件里面的文本: 写个脚本把项目中所有ts文件里面的汉字文本读出来汇总到一个Language.ts文件中,定义在枚举enum对象里面,并且同时批量修改源码把该汉字字符替换为枚举引用;
注意脚本需要很好的维护Language.ts文件,项目源码增加了文本或删除了文本,要保证Language.ts文件的准确和干净;
有朋友就要问了为什么用枚举,不能封装一个方法吗?答案是因为枚举可以有代码提示!这非常重要,让你的代码可读性大大提高。 -
针对exml文件里面的文本: 同样先写个脚本把项目中exml文件里面的汉字文本读出来汇总到一个Language2.ts文件中,定义在一个Object对象里面;
然后再修改类代码中的partAdded方法,判断如果是Label或Button,则把其text或label属性设置为对应的语言文本。
可是,这一步代码写好之后实测会发现问题,那就是一部分文本替换不超过,原因是egret引擎默认并不是所有Label都会调用partAdded方法,只有设置了id的对象才会进调用。因此还需要修改一下引擎代码。在eui解析EXML时判断如果Label没有设置id,就自动为其分配1个id,这样就能保证所有Label会触发partAdded方法了
通过以上方法,无需修改具体的业务代码,完美的实现了需求,剩下的就是把汇总的2个汉字文本提交给翻译了。
下面附代码:
public partAdded(partName: string,instance: any): void {
super.partAdded(partName,instance);
if(window["_LangExml"]){ //定义在主项目
let key;
if(instance instanceof eui.Label){
key = instance.text;
if(window["_LangExml"].hasOwnProperty(key))
instance.text = window["_LangExml"][key];
}
else if(instance instanceof eui.Button){
key = instance.label;
if(window["_LangExml"].hasOwnProperty(key))
instance.label = window["_LangExml"][key];
}
}
}
function changeEgretCodeForLange(){
if(!window["_LangExml"]) return;
console.error("启用了多语言版本,重写引擎 eui.sys.EXMLParser.prototype.addIds 方法");
let fun = eui.sys.EXMLParser.prototype.addIds;
eui.sys.EXMLParser.prototype.addIds = function(items){
if (!items) {
return;
}
var length = items.length;
for (var i = 0; i < length; i++) {
var node = items[i];
if (node.nodeType === 1 && node.attributes["text"] && !node.attributes["id"]) {
this.createIdForNode(node);
}
}
fun.apply(this, [items]);
}
}
脚本代码
/*
* 从项目文件夹 提取汉字字符串,生成汉字字典 -----------------
* 原理:遍历项目里面的汉字字符串,生成ts字典,再用enum枚举方式引用,可以在代码处直接查看枚举值,保证开发效率
*/
var fs = require('fs');
var path = require('path');
var filterFils = ["Language","Language2"];
var filterDirs = ["xx"];
var root_Url = "D:/xxxx/src";
var root_ExmlUrl = "D:/xxxx";
var outFilePath = "D:/xxxx/Language2.ts";
var outFilePath2 = "D:/xxxx/LanguageExml.ts";
var map = {};
var total = 0;
outLanguageTs(outFilePath);
outLanguageExml(outFilePath2);
//先把已经生成的读取出来,避免重复或覆盖
function outLanguageTs(outFilePath){
let langContent = fs.readFileSync(outFilePath, { encoding: "utf8" });
langContent = langContent.split("\n");
if(langContent > 2){
langContent.shift();
langContent.pop();
let str = ", //";
for(var i=0; i<langContent.length; i++){
if(i == langContent.length-1)
str = " //";
var item = (langContent[i].split(str)[0]).split(" = ");
map[ item[1] ] = Number(item[0].substr(" str".length));
if(i == langContent.length-1)
total = map[ item[1] ];
}
console.log("已有文案:", map);
}
//return;
fileDisplay(path.resolve(root_Url));
}
var timer;
//异步,不知道什么时候结束,用这个来判断
function reSetSave(reset){
clearTimeout(timer);
if(!reset){
timer = setTimeout(()=>{
let str = [];
for(var key in map){
console.log(key, map[key]);
str.push( {v:" str" + map[key] + " = " + key, key:key} );
}
let content = ["const enum Lang2 {"];
for(var i=0; i<str.length - 1; i++){
content.push(str[i].v + "," + " //" + str[i].key);
}
content.push(str[i].v + " //" + str[i].key); //最后1个不要逗号
content.push("}")
fs.writeFileSync(outFilePath, content.join("\n"), { encoding: "utf8" });
console.log("保存文件:" + outFilePath);
}, 1000);
}
}
/**
* 文件遍历方法
* @param filePath 需要遍历的文件路径
*/
function fileDisplay(filePath){
//根据文件路径读取文件,返回文件列表
fs.readdir(filePath,function(err,files){
if(err){
console.warn(err)
}else{
//遍历读取到的文件列表
files.forEach(function(filename){
//获取当前文件的绝对路径
var filedir = path.join(filePath,filename);
//根据文件路径获取文件信息,返回一个fs.Stats对象
fs.stat(filedir,function(eror,stats){
if(eror){
console.warn('获取文件stats失败');
}else{
var isFile = stats.isFile();//是文件
var isDir = stats.isDirectory();//是文件夹
if(isFile){
//console.log(filename,filedir);
let onlyName = filename.split(".")[0];
if(filename.indexOf(".ts") != -1 && filterFils.indexOf(onlyName) == -1){
changeDode(filedir);
}
}
if(isDir){
if(!filterDirs || filterDirs.indexOf(filename) == -1){
fileDisplay(filedir);//递归,如果是文件夹,就继续遍历该文件夹下面的文件
}
}
}
})
});
}
});
}
function changeDode(phppath){
//console.log("执行文件:" + phppath);
let phpContent = fs.readFileSync(phppath, { encoding: "utf8" });
//console.log(phpContent);
//['"][\xff-\uffff 0-9$]+['"]
let arr = phpContent.match(/['"][\xff-\uffff 0-9$]+['"]/g);
//console.log(arr);
if(arr){
let change, nstr, regExp0;
for(var i=0; i<arr.length; i++){
let str = arr[i];
if(str.match(/['"][\d$ ]+['"]/g)) continue; //纯数字、空格、$串,忽略
regExp0 = new RegExp("console\\.warn\\(" + str, 'gi');
if(phpContent.match(regExp0)) continue; //console打印的内容,忽略
regExp0 = new RegExp("console\\.log\\(" + str, 'gi');
if(phpContent.match(regExp0)) continue; //console打印的内容,忽略
regExp0 = new RegExp("console\\.error\\(" + str, 'gi');
if(phpContent.match(regExp0)) continue; //console打印的内容,忽略
//console.log(str);
if(!map[str]){
total++;
map[str] = total;
nstr = "Lang2.str" + total;
reSetSave(); //有改变,触发延时保存
}
else{
nstr = "Lang2.str" + map[str];
}
change = true;
regExp0 = new RegExp(str, 'gi');
phpContent = phpContent.replace(regExp0, nstr);
}
//console.log(phpContent);
if(change){
fs.writeFileSync(phppath, phpContent, { encoding: "utf8" });
//console.log("执行完成!--" + phppath);
}
}
}
function Trim(str, is_global) {
//console.log("==", str)
str = String(str);
var result;
result = str.replace(/(^\s+)|(\s+$)/g,"");
if(is_global && is_global.toLowerCase()=="g"){
result = result.replace(/\s/g,"");
}
return result;
}
function lTrim(str) {
str = String(str);
var result = str.replace(/(^\s+)/g,"");
return result;
}
//先把已经生成的读取出来,避免重复或覆盖
function outLanguageExml(outFilePath){
let langContent = fs.readFileSync(outFilePath, { encoding: "utf8" });
try{
langContent = langContent.substr('window["_LangExml"] = '.length);
langContent = langContent.replace(/\\n/g, "");
map = JSON.parse(langContent);
}catch(e){
}
console.log("已有文案:", map);
fileDisplay2(path.resolve(root_ExmlUrl));
}
//异步,不知道什么时候结束,用这个来判断
function reSetSave2(reset){
clearTimeout(timer);
if(!reset){
timer = setTimeout(()=>{
let str = JSON.stringify(map);
str = str.replace(/\\"/g, "");
str = str.replace(/","/g, '",\n"');
str = "{\n" + str.substr(1, str.length-2) + "\n}";
fs.writeFileSync(outFilePath2, 'window["_LangExml"] = ' + str, { encoding: "utf8" });
console.log("保存文件:" + outFilePath2);
}, 1000);
}
}
/**
* 文件遍历方法
* @param filePath 需要遍历的文件路径
*/
function fileDisplay2(filePath){
//根据文件路径读取文件,返回文件列表
fs.readdir(filePath,function(err,files){
if(err){
console.warn(err)
}else{
//遍历读取到的文件列表
files.forEach(function(filename){
//获取当前文件的绝对路径
var filedir = path.join(filePath,filename);
//根据文件路径获取文件信息,返回一个fs.Stats对象
fs.stat(filedir,function(eror,stats){
if(eror){
console.warn('获取文件stats失败');
}else{
var isFile = stats.isFile();//是文件
var isDir = stats.isDirectory();//是文件夹
if(isFile){
//console.log(filename,filedir);
let onlyName = filename.split(".")[0];
if(filename.indexOf(".exml") != -1 && filterFils.indexOf(onlyName) == -1){
changeDode2(filedir);
}
}
if(isDir){
if(!filterDirs || filterDirs.indexOf(filename) == -1){
fileDisplay2(filedir);//递归,如果是文件夹,就继续遍历该文件夹下面的文件
}
}
}
})
});
}
});
}
function changeDode2(phppath){
//console.log("执行文件:" + phppath);
let phpContent = fs.readFileSync(phppath, { encoding: "utf8" });
//console.log(phpContent);
//['"][\xff-\uffff 0-9]+['"]
let arr = phpContent.match(/['"][\xff-\uffff 0-9]+['"]/g);
//console.log(arr);
if(arr){
let change, nstr, regExp0;
for(var i=0; i<arr.length; i++){
let str = arr[i];
if(str.match(/['"][\d$ ]+['"]/g)) continue; //纯数字、空格、$串,忽略
str = str.substr(1, str.length-2);
//console.log(str);
if(!map.hasOwnProperty(str)){
map[str] = "";
reSetSave2(); //有改变,触发延时保存
}
}
}
}