Seize the day

SEARCH RESAULT : 글 검색 결과 - 전체 글 (총 469개)

POST : Backend study

Nest.js 서버에 중요 서버 에러 Discord 메신저로 보내기

github에서 어떤 코드를 보다가 예전에 트위터로 중요 서버 에러를 남기는 기능에 대한 힌트를 얻었다.   요즘에는 Discord 메신저를 많이 이용하나 보다.  (DISCORD_WEBHOOK_URL 검색

빠르게 테스트를 해 보았는데 의외로 너무 잘되서 여기에 스터디 로그를 남긴다. 

1. Discord Web Hook URL 만들기.  

가입이 이제는 초대없이 되나보다.  가입부터 하고, Hook URL 만들기는 너무 간단했다.   참고 URL 

 

2. Node js 에서 에러 보내기

axios 추가

bun add axios

보내기..

    import axios from 'axios';
    
    const content = {
      code: status,
      message: exception.message,
      path: request.url,
    };

    axios.post(DISCORD_WEBHOOK_URL, {
      content: "<@123414024347770> " + JSON.stringify(content),
    });

 

3. 디스코드 User id 구하기

<@특정 사용자 ID> 로 보내면 해당 사용자가 멘션이 되는데 이 아이디를 찾는 법을 구글링했다.  멘션을 하지 않아도 앱 푸시 알림은 오기 때문에 혼자만 있는 방이라면 굳이 멘션하지 않아도 된다.

개발이 이렇게 쉬워도 되는건가 싶다. 

top

posted at

2024. 4. 29. 00:07


POST : Backend study

Nest.js 서버에 fastify, https, gzip 적용하기

1. Fastify

fastify 적용은 딱히 어려움은 없었다. 일찍 적용해서 개발 단계에서 충분히 테스트하는 것이 낫겠다. 나중에 fastify로 바꿔서 문제가 생기면 찾기도 어렵고 그것 자체가 장애라서 곤란하겠지..

https://docs.nestjs.com/techniques/performance 를 참고하면 크게 무리가 없다. 
https://github.com/NGuard-Security/security_api/pull/7/commits/1f52ab480a02a26b69c1ee0cca3335d662b48e62 여기 수정을 참고하면 큰 문제 없다. 

특이한 점은 express를 사용할 때는 request.ip 가 undefined로 나온다는 것이다. (node.js가 아니라 bun으로 실행해서 그럴수도.. 확인은 못 함) 몇 시간 구글링해도 remote ip를 얻는 방법을 찾지 못했는데 fastify를 바꿨더니 문제가 없어졌다. 

 

2. 서버를 실행하고 종료시키는 스크립트를 만들었다. 

kill로 강제 종료하는 게 맞는지 모르겠다. 

dev_server.sh

echo "bun --watch ./src/main.ts"
bun --watch ./src/main.ts &
echo "server pid is $!"
echo $! > nest_server.pid

kill_server.sh

pid=$(<nest_server.pid)
echo "stop nest_server (pid is $pid)"
kill -9 $pid
rm nest_server.pid

 

3. https 적용 테스트

테스트 인증서 설치는 
https://growth-msleeffice.tistory.com/129
https://velog.io/@haru/how-to-local-testing-by-https 를 참고했다. mkcert 설치는 brew install mkcert 로 했다. 

fastify의 경우는 적용이 살짝 다른데..  https://docs.nestjs.com/faq/multiple-servers 를 참고했다.  
locoalhost와 특정 도메인의 테스트 인정서는 적용하는데 문제 없었다. 다만 localhost에서의  접속은 문제가 없었지만 안드로이드 앱으로 접속하는 것은 verification 실패 예외가 발생하여 실패하였다. 정상적인 인증서를 사용하면 이것도 문제가 없을 듯 하다.  결론은 개발은 인증서 없이 http로, 운영서버는 실제 인증서(LetsEncrypt)를 가지고 https로 구동시킬 계획이다. 

import fs from "fs";

const httpsOptions = {
  key: fs.readFileSync('./dev.mdiwebma.com+2-key.pem'),
  cert: fs.readFileSync('./dev.mdiwebma.com+2.pem'),
};

async function bootstrap() {
...
    new FastifyAdapter({https: httpsOptions}),
...

 

4. gzip 적용하기

https://docs.nestjs.com/techniques/compression 를 참고해서 구현.. 1줄만 추가하면 gzip이 지원된다니 놀라울지경이다... nginx로 gzip을 지원하려고 쌩쇼를 했었는데 그때도 서버 응답에서는 gzip으로 응답하기가 가능했는데, 앱에서 gzip 압축해서 요청할 경우에는 처리하지 못했었다. 그런데 nest.js에서    await app.register(compression);  이 1줄 추가로 요청과 응답에서 gzip이 잘 동작했다. 

Android app에서 테스트 코드..

            // gzip으로 압축하여 요청
            // TODO body의 크기가 일정 length보다 크면 gzip으로 압축하도록 수정
            val data: ByteArray = jsonObject.toString().toByteArray()
            val arr = ByteArrayOutputStream()
            val zipper: OutputStream = GZIPOutputStream(arr)
            zipper.write(data)
            zipper.close()

            val body = RequestBody.create(JSON_CONTENT_TYPE, arr.toByteArray())

            //val body = RequestBody.create(JSON_CONTENT_TYPE, jsonObject.toString())
            val request = Request.Builder()
                .url(getUrl(path))
                .header(JSON_WEB_TOKEN, jwt)
                .header("Content-Type", "application/json; charset=utf-8")
                .header("Content-Encoding", "gzip")
                .header("Accept-Encoding", "gzip")
                .post(body)
                .build()
            val response = httpClient.newCall(request).execute()
            
            
            
            // gzip 응답처리..            
            var bodyString: String?
            // gzip 디코딩
            if (response.header("Content-Encoding") == "gzip" ||
                response.header("content-encoding") == "gzip"
            ) {
                val responseBody = response.body() ?: throw IOException("body is null")
                BufferedInputStream(GZIPInputStream(responseBody.byteStream())).use { input ->
                    ByteArrayOutputStream().use { baos ->
                        val ba = ByteArray(1024)
                        while (true) {
                            val len = input.read(ba)
                            if (len == -1) break
                            baos.write(ba, 0, len)
                        }

                        bodyString = String(baos.toByteArray())
                    }
                }
            } else {
                bodyString = response.body()?.string()
            }

 

 

 

top

posted at

2024. 4. 27. 23:17


POST : Backend study

자바스크립트 런타임 Bun을 써볼까나..

구글 GCP에 서버 하나를 3년째 돌리고 있다. 한 달에 5000원씩 내고 있다. 앱을 하나 개발하는게 있는데 와이프랑 데이타를 주고 받아야 해서 어쩔 수 없이 서버가 필요하다. 집에 PC를 하나 돌릴 수도 있을텐데 전기료랑 GCP 돌리는 비용이랑 비슷할 것 같아서 그냥 그렇게 하고 있다. 이 앱은 사용자가 단 2명 뿐인데도 꾸준히 거의 매일 쓰고 있다. 마지막 개발은 거의 1년 전이라 개선을 하고 싶기는 한데 한 마디로 너무 귀찮다. 독푸딩단계에서는 2명의 요구수준을 거의 만족을 하고 있는데, 출시하기 위해서는 암호화나 권한 관리, 탈퇴처리, 어뷰징 방지 등의 개선과 복잡한 구현이 여러 남아 있는 상태다.  

백엔드 서비스를 구현하면서 여러 실수를 많이 했다. 현재의 백엔드 상태를 보면..

User App ---> Nginx(https, gzip) ---> Deno sever --> Mongo db 

Nginx부터는 하나의 물리적 서버에서 돌아간다. Deno라는 js 런타임을 채택하면서 Nginx를 추가해야만 했다. 왜냐하면 Deno 런타임에서 사용하는 oak 라이브러리가 https와 gzip을 지원하지 않기 때문이다. 따라서 Nginx가 https와 gzip을 핸들링하고 Gateway역할을 하고 있다. 운영하면서 미묘한 문제를 몇 번 겪었는데 서버가 가끔 다운되는 현상이 있었다. 백엔드 서비스가 응답하지 않아서 VM에 SSH 접속을 시도했을 때 연결이 되지 않는 경우가 4번 정도 있었다. GCP에서 가상머신은 ACTIVE 상태인데 ssh 접속이 안 되는 경우라서 VM을 재시작하는 수 밖에는 없었다.  (원인파악도 되지 않는다 서비스에 로그를 남기는 기능까지는 구현되어 있지 않아서다. 서비스 문제 같지는 않지만..) 따라서 일단 GCP에서 AWS로 이동하려고 한다. 경험치를 늘리기위한 것도 있다. 장기 계약하면 비용이 싸지는 것도 있기 때문에 AWS의 EC2를 사용할 계획이다. (1년전에 OCP를 한 달 정도 운영서버로 사용했었는데 이상하게도 OCP에서 아무런 통지도 없이 계정을 자르는 바람에 서버 데이타도 백업하지 못했다. 그때 app내 데이타가지고 db복구한다고 생고생했다. )

그리고 Deno를 포기해야 겠다. Nest.js 같은 좋은 프레임 워크가 없어서 피처 진행을 어떻게 해야할 지 엄두가 안 난다. 따라서 원활히 개발 진도를 뺄려면 Node.js 런타임에 Nest.js 프레임웍을 사용하는 것이 최신이라고 생각했다. nest.js에서는 기본으로 제공해주는 기능을 일일이 구현하고 있을 수는 없기때문이다. 

이런 와중에 우연히 Bun을 알게됬다. 개발 트렌드를 확인할 겸 가는 사이트가 있는데 거기서 Nodejs 와 하위호환이 되는 차세대 js 런타임으로 Bun이 소개되었다.  처음에 Deno를 선택한 이유가 좀더 빠른 속도와 원활한 TypeScript 사용이었는데 Bun은 이 두 개를 충족하면서 nodejs를 그대로 돌릴 수 있다니 관심이 갔다. nestjs와의 궁합도 확인해보니 Bun 자체에서 지원이 되는 것 같았다. 라이브러리 설치 속도, 실행 속도, 초당 요청수도 nodejs에 비해서 압도적으로 우수한 것으로 보인다.

bun create nest test_bun_nest
cd test_bun_nest
bun --watch ./src/main.ts

시작이 좋다. nest.js를 돌릴 수 있고, 소스 파일 저장시 서버를 자동으로 재시작해주는 --watch 옵션도 자체 지원하고 있다. 그래서 최종적인 백엔드 구성을 이렇게 해 볼려고 한다. 

User app1 --> Bun server(Nest.js (support https, gzip, file logging, ...) with fastify) --> Mongo db

nestjs는 express보다 더 빠르다는 fastify로 돌리고 https와 gzip을 자체적으로 지원한다. 모든 요청과 응답 로깅을 아파치 처럼 log 파일로 저장하면서 warning이나 error 로그는 별도 log파일로 저장하고, 서버 internal error도 날짜별로 파일을 만들어서 저장한다.  그리고 Bun과 mongodb는 docker를 이용하여 실행한다. 비정상적인 요청의 차단이나, 서버 다운과 같은 것은 운영자에게 노티할 필요가 있는데 이런건 어떻게 만들어야 할지 고민이다. 예전에 nodejs기반의 한 프로젝트에서는 중요 이벤트 예를들면 서버가 죽는 경우 등일 때 트위터로 메시지를 남기는 기능을 만들었는데 잘 동작했었다. 해당 트위터 계정을 구독하면 노티를 받을 수 있지 않을까 싶다. 

 

top

posted at

2024. 4. 27. 03:06


POST : Flutter study

첫 플러터 앱 출시하다.

Flutter 로 만든 앱을 출시했다. https://play.google.com/store/apps/details?id=com.mdiwebma.good_timer

 

Pomodoro Flow - Google Play 앱

간단한 Pomodoro 앱

play.google.com

 

Flutter를 4년만에 다시 시작한 것 치고는  별 문제없이 UX, 개발, 테스트를 동시에 진행하며 개발을 완료했다.  2주 동안 개발하는 내내 몰입하는 즐거움을 느꼈다. 개발에 특별히 엄청난 기술이 들어가지는 않았다. Realm과 Table calendar를 써 본 정도다.  선언형 UI 개발 방식이 명령형 방식보다 개발하기 더 좋은 느낌이다. 느낌이지 확실한 결론은 아니다.  개발 속도도 빠른 것 같고, 적어도 버그의 개수는 줄어들것 같다. 소스코드를 저장만 해도 동작중인 앱에 반영이 되는 것은 큰 장점이다. 그리고 역시나 네이티브를 잘 알고 있는 것이 중요한 것 같다. 플러그인 없이는 괜찮은 기능을 만들기 어렵다. 

top

posted at

2023. 12. 13. 23:01


POST : Flutter study

Flutter 공부 다시 해 볼련다..

22년 6월 부터 사실상 놀고 있다.  이 블로그의 글도 거의 업데이트 하지 않고 있다. 업데이트 하지 않는다는 것은 개발도 하지 않고 있다는 뜻이다. 내가 퇴사한 이유도 근본 원인은 번아웃인데 이게 참 무섭다. 아무것도 하지 않고 몇 달을 지낸적도 있다. 나는 개발이 하고 싶어질 때 까지 계속 기다렸다. 23년도가 2달 남은 최근에서야 잠들기 전에 머리속에서 뭔가가 계속 멤돈다. 플러터를 다시 해야겠다는 생각과 몇 년간 독푸딩하고 있는 앱을 결국에는 출시를 해야한다는 생각이다. 두 가지는 확고한 결심이다. 

완벽주의 성향이 있는 나로써는 뭔가가 충분히 준비되지 않으면 시작하지 못하는 문제가 있다. 안 좋은 습관이다. Flutter 공부도 시작도 전에 계속 뭔가 그럴듯한 앱을 만들 생각부터 했다. 공부도 되고 또 나와 다른 사람에게 유용한 제품도 되는 것을 찾다보니 뭘 만들지 결정하지 못하고 그래서 시작하지 못하는 것 같다.  그러다 오늘 그냥 시작했다. 계산기나 메모장, 체크리스트, 알람 앱 등을 생각하다가 결국에는 뽀모도로라는 것을 만들기로 했다.  내가 매일 쓸 것 같고, 요구사항이 간단하고, 서버가 필요없고, 글로벌로 런칭할 수 있기 때문에 이게 적당해 보인다. 

오늘 3시간 동안 가칭 "내가 만든 뽀모도로"라는 앱을 만들었고 가장 최소한의 기능만 구현하여 폰에 설치하며 독푸딩해봤다. 오랜만이라 오피셜 문서 위주로 다시 들여다 봐야했다.  릴리스 모드 apk가 18메가라니.. 이건 어쩔수 없나보다. 

요구사항
- 시작버튼을 누르면 25분 타이머가 시작된다. 시간은 붉은 색으로 표시된다. 
- 25분이 지나면 소리가 나고 5분 타이머가 시작된다.  시간표시는 검정색으로 바뀐다.
- 5분이 지나면 또 다시 다른 소리가 나고, 다시 25분 타이머가 시작된다. 
- 타이머가 동작중에는 화면이 꺼지지 않는다. 
- 종료버튼을 누르면 대기 상태가 되고, 대기시간이 지나면 화면도 꺼진다.
이 요구사항이 기본 생성 코드에서 40줄 정도 추가하거나 수정하니 잘 동작했다.

wav파일 재생을 위해서 audioplayers  플러그인을 사용했고, 화면 잠기지 않도록 wakelock 플러그인을 사용했다. 

TODO
- 화면이 꺼지면 타이머 멈추는 문제
- 뒤로가기시 종료 확인 뜨게

소스코드
- https://github.com/dajkim76/good_timer

11/24일 업데이트

- only dark theme
- make full screen
- disable back key(WillPopScope),
- add close button to appBar
- apply app name, app icon


- https://github.com/dajkim76/good_timer/commit/a8a99747f2b74a313927c35e29d81822b3c9813d

top

posted at

2023. 11. 23. 03:52


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
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크