앱 다국어 버전을 빨리 만들어야해서 다국어툴 개발을 서두르고있다. 지난번 values/strings.xml에서 xlsx 파일을 생성하는 자바스크립트를 일부 개선했다.
StringsToXlsx.js
1. 개선의 요지는 & < > ' " \n 문자를 추가적으로 치환 처리했다.
2. <![CDATA 태그로 묶인 스트링은 특수 문자 치환을 하지 않도록 했다.
xlsx에서 strings.xml로 가져올 때도 <![CDATA로 묶인 것은 <![CDATA를 추가해야 하므로 결국에는 CDATA 처리여부를 알아야 하는데, 스트링 아이디에 _cdata 라는 suffix를 붙이는 것으로 판단하는게 간단했다.
개선된 코드는 아래와 같다.
"use strict"; const xml2js = require("xml2js"); const hashmap = require("hashmap"); const fs = require("fs"); const Workbook = require("xlsx-workbook").Workbook; const workbook = new Workbook(); const sheet = workbook.add("sheet1"); sheet[0][0] = "Screenshot touch translation"; const replaceMap = new hashmap.HashMap(); replaceMap.set("%d", "${num}"); replaceMap.set("%s", "${str}"); replaceMap.set("%1$d", "${num1}"); replaceMap.set("%2$d", "${num2}"); replaceMap.set("%3$d", "${num3}"); replaceMap.set("%4$d", "${num4}"); replaceMap.set("%1$s", "${str1}"); replaceMap.set("%2$s", "${str2}"); replaceMap.set("%3$s", "${str3}"); replaceMap.set("%4$s", "${str4}"); replaceMap.set("&", "&"); replaceMap.set("<", "<"); replaceMap.set(">", ">"); replaceMap.set("\\n", "\n"); replaceMap.set("\\'", "'"); replaceMap.set('\\"', '"'); const replaceKeys = replaceMap.keys(); const langColumnIndexMap = new hashmap.HashMap(); langColumnIndexMap.set("en", 4); langColumnIndexMap.set("ko", 5); langColumnIndexMap.set("ru", 6); langColumnIndexMap.set("de", 7); let maxLangColumnIndex = 8; sheet[7][0] = "[common]"; function read_resouce_dir(res_dir) { console.log("\n" + res_dir); fs.readdirSync(res_dir).forEach(filename => { if (filename == "values" || filename.startsWith("values-")) { const langCode = filename == "values" ? "en" : filename.substr(7); if (langCode.length > 0) { let langColIndex = 0; if (langColumnIndexMap.has(langCode)) { langColIndex = langColumnIndexMap.get(langCode); } else { langColIndex = maxLangColumnIndex; maxLangColumnIndex++; } const xmlPath = res_dir + filename + "/strings.xml"; if (fs.existsSync(xmlPath)) { console.log(filename + " -> " + langCode); sheet[6][langColIndex] = langCode; write_language(langCode, langColIndex, xmlPath); } } } }); } const stringId2RowIndexMap = new hashmap.HashMap(); let nextStringIdRowIndex = 8; function write_language(langCode, langColIndex, xmlPath) { const xmlString = fs.readFileSync(xmlPath); const parser = new xml2js.Parser(); parser.parseString(xmlString, function(err, result) { const len = result.resources.string.length; for (let i = 0; i < len; i++) { const item = result.resources.string[i]; const id = item.$.name; let row; if (stringId2RowIndexMap.has(id)) { row = stringId2RowIndexMap.get(id); } else { row = nextStringIdRowIndex++; stringId2RowIndexMap.set(id, row); sheet[row][0] = id; } write_string(langCode, id, row, langColIndex, item._); } }); } const mustHaveColIndex = 3; function write_string(langCode, id, row, col, value) { if (typeof value == "undefined") { console.log("undefined value: lang=" + langCode + " id=" + id); return; } let mustHave = ""; // cdata는 replace하지 않는다. if (!id.endsWith("_cdata")) { for (let i = 0; i < replaceKeys.length; i++) { const str0 = replaceKeys[i]; while (true) { const index = value.indexOf(str0); if (index < 0) { break; } if (index == 0 || (index > 0 && value[index - 1] != "%")) { const str1 = replaceMap.get(str0); if (str1.startsWith("$")) { mustHave += " " + str1; } value = value.replace(str0, str1); } } } } if (langCode == "en" && mustHave.length > 0) { sheet[row][mustHaveColIndex] = mustHave.substr(1); } sheet[row][col] = value; } const android_base_res_dir = "..\\android_base\\res\\"; const app_res_dir = "..\\app\\src\\main\\res\\"; read_resouce_dir(android_base_res_dir); sheet[nextStringIdRowIndex++][0] = "[app]"; read_resouce_dir(app_res_dir); sheet[nextStringIdRowIndex++][0] = "--end--"; workbook.save("Screenshot_touch"); console.log("[DONE]");
XlsxToStrings.js
google docs에 올려진 스프레드쉬트를 xlsx파일로 내려받아서, strings폴더에 리소스 파일을 생성하는 스크립트를 구현했다. xml-writer 모듈이 필요하다.
npm install xml-writer
특별히 어려운 것은 없는데, CDATA가 필요하는 것은 id가 _cdata로 끝나는지로 체크했다. & < > 문자의 치환은 주석처리했는데 xml-writer모듈에서 해 주기때문이다.
xml-writer 모듈의 소스도 일부 수정했다. > 문자는 안드로이드에서 치환할 필요가 없어서 그대로 나오도록 치환하는 코드를 제거했고, 각 라인 추가는 \n으로 되어 있는 것을 \r\n으로 바꿨다. <!CDATA로 감쌀 때 불필요한 indent가 생기지 않도록 수정했다.
UTF8-BOM으로 저장되 파일은 UTF8로 다시 저장했고, XML 헤더도 수정했다. 요지는 strings 에서 xlsx로 변환하고, xlsx에서 다시 strings.xml로 변환했을 때 strings.xml 파일에 변경사항이 생기지 않도록 만들었다.
"use strict"; const xml2js = require("xml2js"); const hashmap = require("hashmap"); const fs = require("fs"); const Workbook = require("xlsx-workbook").Workbook; const workbook = new Workbook("output.xlsx"); const sheet = workbook["sheet1"]; console.log(sheet[0][0]); // const workbook = require("xlsx").readFile("output.xlsx"); // const sheet = workbook.Sheets["sheet1"]; // console.log(sheet["A1"].w); var XMLWriter = require("xml-writer"); const replaceMap = new hashmap.HashMap(); replaceMap.set("${num}", "%d"); replaceMap.set("${str}", "%s"); replaceMap.set("${num1}", "%1$d"); replaceMap.set("${num2}", "%2$d"); replaceMap.set("${num3}", "%3$d"); replaceMap.set("${num4}", "%4$d"); replaceMap.set("${str1}", "%1$s"); replaceMap.set("${str2}", "%2$s"); replaceMap.set("${str3}", "%3$s"); replaceMap.set("${str4}", "%4$s"); //replaceMap.set("&", "&"); //replaceMap.set("<", "<"); //replaceMap.set(">", ">"); replaceMap.set("\n", "\\n"); replaceMap.set("'", "\\'"); replaceMap.set('"', '\\"'); const replaceKeys = replaceMap.keys(); const langRow = 6; const langCol = 4; const android_base_res_dir = "..\\android_base\\res\\"; const app_res_dir = "..\\app\\src\\main\\res\\"; function main() { let column = langCol; while (true) { let langCode = sheet[langRow][column]; if (typeof langCode == "undefined") { break; } if (langCode === "") { break; } let row = langRow + 2; //skip [common] row = write_strings(android_base_res_dir, column, row); write_strings(app_res_dir, column, row + 1); column++; } console.log("[DONE]"); } main(); function write_strings(basedir, column, row) { const langCode = sheet[langRow][column]; console.log("langcode=" + langCode); const valuseDir = basedir + "values" + (langCode === "en" ? "" : "-" + langCode); console.log("values dir=" + valuseDir); if (!fs.existsSync(valuseDir)) { return; } const stringsPath = valuseDir + "\\strings.xml"; console.log("strings path=" + stringsPath); var ws = fs.createWriteStream(stringsPath); ws.on("close", function() { //console.log(fs.readFileSync("strings.xml", "UTF-8")); }); var xw = new XMLWriter(true, function(string, encoding) { ws.write(string, encoding); }); xw.startDocument("1.0", "UTF-8"); let res = xw.startElement("resources"); while (true) { const id = sheet[row][0]; if (typeof id == "undefined") { break; } console.log("id=" + id); if (id == "[app]" || id == "--end--") { break; } let value = sheet[row][column]; if (typeof value == "undefined") { console.log("undefined value id=" + id); row++; continue; } // repalce value if (!id.endsWith("_cdata") && value.length > 0) { for (let i = 0; i < replaceKeys.length; i++) { let startIndex = 0; const str0 = replaceKeys[i]; const str1 = replaceMap.get(str0); const index = value.indexOf(str0, startIndex); if (index >= 0) { value = replaceAll(value, str0, str1); } } } res.startElement("string"); res.writeAttribute("name", id); if (id.endsWith("_cdata")) { res.writeCData(value); } else { res.text(value); } res.endElement(); row++; } xw.endElement(); xw.endDocument(); ws.end(); return row; } function replaceAll(str, searchStr, replaceStr) { return str.split(searchStr).join(replaceStr); }
------ 추가 -----
CDATA로 깜싸지더라도 ' " 문자등은 \' \"로 치환이 필요하다. 위 코드는 일부 수정이 필요하다.
download.cmd : 구글 docs에서 xlsx 포맷으로 다운로드 받아서 res폴더에 업데이트까지 자동으로 한다.
curl https://docs.google.com/spreadsheets/d/1W9HOGd0qWNtLpvgKzsrT0Gf6RlKxrknHUVULRooNYJg/export?format=xlsx > output.xlsx
node XlsxToStrings.js