Seize the day

POST : Android Dev Study

안드로이드 다국어 문자열 관리 툴 개선 by Dart lang

https://dajkim76.tistory.com/481

 

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

앱 다국어 버전을 빨리 만들어야해서 다국어툴 개발을 서두르고있다. 지난번 values/strings.xml에서 xlsx 파일을 생성하는 자바스크립트를 일부 개선했다. StringsToXlsx.js 1. 개선의 요지는 & < > ' " \n 문

dajkim76.tistory.com

이전에 구글 독스에서 언어를 관리하고 편집하고,  툴을 이용해서 xlsx를 내려받아서 안드로이드의 res/values/strings.xml 파일을 자동으로 생성하는 툴(자바 스크립트)을 개발했었다.  이렇게 하는 이유는 언어 번역과 개발 적용을 분리하기 위해서이고, 번역자는 안드로이드 고유의 포맷 문자열 처리등의 개발 지식이 없더라도 쉽게 수정할 수 있는 방법이 필요했기 때문이다.  때문에 문자열에 ' " ${str} ${num} 과 사용자 친화적인 문자열로 입력하면 되고, 툴은 최종적으로 \' \" %s %d 와 같은 문자열로 치환하여 적용한다. 

이게 자바스크립트로 구현했는데 맥 미니에서 동작이 잘 안 되고 (오류를 수정할 수는 있겠지만 귀찮다. node버전과 관련이 있는듯.) 옛날에 Go 언어로 간단한 거 만든적 있고, 파이선도 해 봤지만 문법이 기억나지 않는다. 새로 뭔가를 배워야 한다면 dart 언어로 해 보면 어떨까 싶어서 새로 구현했다. Flutter도 어차피 공부해야해서 언어에 익숙해지는데 도움이 될 듯.

https://dart.dev/tutorials/server/cmdline

 

Write command-line apps

Basics for command-line apps.

dart.dev

프로젝트 생성

dart create lt_lang_utils

 

필요한 라이브러리 추가 xml, excel
excel 최신버전은 xml 최신 버전과 충돌이 있기 때문에 xml의 버전을 5.4.1 로 다운그레이드한다. 

cd lt_lang_utils
dart pub add xml
dart pub add excel

 

스크린 샷 사용자가 ar-strings.xml 파일을 번역해 왔는데 이 파일은 Google doc로 해당 Row에 맞게 올라가야지 나중에 관리가 된다. 따라서  일단 이것을 같은 Row에 해당 스트링을 찾아서 옆에 ar 스트링을 자동으로 입력하고 그것을 output.xlsx로 만들어서 구글 독스로 붙여넣기를 하기로 했다.  

import 'dart:io';
import 'package:xml/xml.dart';
import 'package:excel/excel.dart';


//  안드로이드 strings.xml 에서 <string name="ok">OK</string>  ok-> OK 의 map을 만들어 리턴한다.
Map<String, String> loadStringsXml(String filename) {
  final Map<String, String> map = {};

  final file = File(filename);
  final document = XmlDocument.parse(file.readAsStringSync());
  final resources = document.getElement('resources')!;

    for (var node in resources.children) {
      final key = node.getAttribute('name');
      if (key?.isNotEmpty == true) {
        final text = node.text;
        map.putIfAbsent(key!, () => text);
      }
    }

  return map;
}


void writeXlsxFromStringsXml() {
  final Map<String, String> arStringMap = loadStringsXml('ar-strings.xml');
  final  outputExcel = Excel.createExcel();
  final  outputSheet = outputExcel['Sheet1']; // default sheet

  var file = "Screenshot_touch_translation.xlsx";
  var bytes = File(file).readAsBytesSync();
  var excel = Excel.decodeBytes(bytes);

  //Screenshot_touch_translation has below datas
  //flutter: [Data([common], 0, 7, null, sheet1), null, null, null, null, null, null, null, null, null, null]
  //flutter: [Data(ok, 0, 8, null, sheet1), null, null, null, Data(OK, 4, 8, null, sheet1), Data(확인, 5, 8, null, sheet1), Data(ОК, 6, 8, null, sheet1), Data(OK, 7, 8, null, sheet1), Data(TAMAM, 8, 8, null, sheet1), null, Data(Aceptar, 10, 8, null, sheet1)]
  //flutter: [Data(cancel, 0, 9, null, sheet1), null, null, null, Data(Cancel, 4, 9, null, sheet1), Data(취소, 5, 9, null, sheet1), Data(Отмена, 6, 9, null, sheet1), Data(Abbrechen, 7, 9, null, sheet1), Data(Vazgeç, 8, 9, null, sheet1), null, Data(Cancelar, 10, 9, null, sheet1)]
  var skipRow = true;
  for (var table in excel.tables.keys) {
    print(table); //sheet Name
    final t = excel.tables[table]!;
    print(t.maxCols);
    print(t.maxRows);
    for (var row in t.rows) {
      final keyData = row.elementAt(0);
      if (keyData == null ) continue;
      final key = keyData.value.toString();

      if (key == "[common]" || key == "[app]") {
        skipRow = false;
        continue;
      }
      if (skipRow) continue;

      final valueData = row.elementAt(4);
      if (valueData == null) continue;
      final value = valueData.value.toString();

      int rowIndex = keyData.rowIndex;
      outputSheet.updateCell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: rowIndex), key);
      outputSheet.updateCell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: rowIndex), value);
      if (arStringMap.containsKey(key)) {
        String? arValue = arStringMap[key];
        outputSheet.updateCell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: rowIndex), arValue);
      } else {
        // string-ar/ value not exists.
        print("$key-> $value");
      }
    }
  } //for

  // save result excel
  var fileBytes  = outputExcel.save()!;
  File("output.xlsx")
    ..createSync(recursive: true)
    ..writeAsBytesSync(fileBytes);

    print("save OK");
}

 

두 번째 구글 독스에서 xlsx를 내려받아서 res/values-XX에 strings.xml 파일을 자동으로 만드는 코드다. 


class StringItem {
  String key;
  String text;
  StringItem({required this.key, required this.text});
}


void writeStringsFromXlsx() {
  var file = "Screenshot_touch_translation.xlsx";
  var bytes = File(file).readAsBytesSync();
  var excel = Excel.decodeBytes(bytes);
 
    // first table
    final t = excel.tables["sheet1"]!;
    print(t.maxCols);
    print(t.maxRows);

    const langRow = 6;
    const firstLangCol = 4;
    const maxLangColumn = 10; //ar


    int column = firstLangCol;
    while(column <= maxLangColumn)  {
      var data =  t.cell(CellIndex.indexByColumnRow(columnIndex:column, rowIndex:langRow));
      String langCode = data.value.toString();
      print("langCode=$langCode");

      List<StringItem> baseStringList = List.empty(growable: true);
      List<StringItem> appStringList = List.empty(growable: true);

      int state = 0;
      for (var row in t.rows) {
        final keyData = row.elementAt(0);
        if (keyData == null) continue;
        final key = keyData.value.toString();

        if (key == "[common]") {
          state = 1;
          continue;
        }
        if (key == "[app]") {
          state = 2;
          continue;
        }
        if (key == "--end--") {
          break;
        }
        if (state == 0) continue;

        final valueData = row.elementAt(column);
        if (valueData == null) continue;
        final value = valueData.value.toString();

        // 반드시 포함 되어야 하는 데이타 체크
        final includeData = row.elementAt(3);
        if (includeData != null) {
          String includeStrings = includeData.value.toString();
          if (includeStrings.isNotEmpty) {
            for(var inc in includeStrings.split(' ')) {
              if (!value.contains(inc)) {
                print("ERROR: $langCode $key $inc $value");
              }
            }
          }
        }

        if (state == 1) {
          baseStringList.add(StringItem(key: key, text: value));
        } else {
          appStringList.add(StringItem(key: key, text: value));
        }
      }
      writeStringsXml(baseStringList, appStringList, langCode);
      column ++;
    }
  } // for  



// 안드로이드 res 폴더에 strings.xml 파일을 생성합니다. 
void writeStringsXml(List<StringItem> baseStringList, List<StringItem> appStringList, String langCode) {
  String langCodeSuffix;
  if (langCode == "en") {
    langCodeSuffix = ""; 
  }else {
    langCodeSuffix = "-$langCode";
  }

  final appResdir = "../../screenshot/app/src/main/res/values$langCodeSuffix";
  final baseResdir = "../../screenshot/android_base/res/values$langCodeSuffix";

  // android_base
  {
    final builder = XmlBuilder();
      builder.processing('xml', 'version="1.0" encoding="UTF-8"');
      builder.element("resources", nest: () {
        for (var element in baseStringList) {
          builder.element("string", nest: () {
            builder.attribute("name", element.key);
            final newText = touchText(langCode, element.key, element.text);
            if (element.key.endsWith("_cdata")) {
              builder.cdata(newText);
            } else {
              builder.text(newText);
            }
          });
        }
      });
      //
      final document = builder.buildDocument();
      String xmlString = document.toXmlString(pretty: true, indent: '    ', newLine: '\n');
      String path = "$baseResdir/strings.xml";
      File(path).writeAsStringSync(xmlString); 
      print(path);
  }
  // app
  {
    final builder = XmlBuilder();
      builder.processing('xml', 'version="1.0" encoding="UTF-8"');
      builder.element("resources", nest: () {
        for (var element in appStringList) {
          builder.element("string", nest: () {
            builder.attribute("name", element.key);
            final newText = touchText(langCode, element.key, element.text);
            if (element.key.endsWith("_cdata")) {
              builder.cdata(newText);
            } else {
              builder.text(newText);
            }
          });
        }
      });
      //
      final document = builder.buildDocument();
      String newLine = langCode == "en" ? "\r\n" : "\n";
      String xmlString = document.toXmlString(pretty: true, indent: '    ', newLine: newLine);
      String path = "$appResdir/strings.xml";
      File(path).writeAsStringSync(xmlString); 
      print(path);
  }
}

String touchText(String langCode, String key, String text) {
  Map<String, String> replaceMap = {};
  replaceMap["\${num}"] = "%d";
  replaceMap["\${str}"] = "%s";
  replaceMap["\${num1}"] = "%1\$d";
  replaceMap["\${num2}"] = "%2\$d";
  replaceMap["\${num3}"] = "%3\$d";
  replaceMap["\${num4}"] = "%4\$d";
  replaceMap["\${str1}"] = "%1\$s";
  replaceMap["\${str2}"] = "%2\$s";
  replaceMap["\${str3}"] = "%3\$s";
  replaceMap["\${str4}"] = "%4\$s";  
  
  replaceMap["'"] = "\\'";
  replaceMap['"'] = '\\"';
  
  if (langCode != "ar") {
    replaceMap["\n"] = "\\n";    
  }

  if (text.contains("\r\n")) {
    text = text.replaceAll("\r\n", "\\n");
  }

  if (key.endsWith("_cdata") == false && text.contains("%")) {
    text = text.replaceAll("%", "%%");
  }

  replaceMap.forEach((key, value) {
    if (text.contains(key)) {
      text = text.replaceAll(key, value);
    }
  });

  return text;
}

 

실행해 보기

curl -Ls https://docs.google.com/spreadsheets/d/blabla/export\?format\=xlsx > Screenshot_touch_translation.xlsx 
dart run lt_lang_utils.dart

 

https://dart.dev/tools/dart-compile 로 실행파일을 만들어서 실행 할 수도 있겠다. 

 

top

posted at

2022. 11. 29. 21:28


CONTENTS

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