Seize the day

POST : Android Dev Study

안드로이드 다국어 번역툴 #4

앱 다국어 버전을 빨리 만들어야해서 다국어툴 개발을 서두르고있다. 지난번 values/strings.xml에서 xlsx 파일을 생성하는 자바스크립트를 일부 개선했다. 

StringsToXlsx.js

translation_tools.zip

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



top

posted at

2018. 12. 26. 21:00


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크