ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Next] i18n 자동화 프로세스 도입 - (3) : key upload, download
    Next.js 2022. 12. 11. 12:20

    안녕하세요.
    이 글은 이전 글과 이어진 글입니다.

    https://uni-s-code.tistory.com/43

     

    [Next] i18n 자동화 프로세스 도입 - (1) : i18next-scanner를 통해 코드에서 key 값 추출하기

    이 글은 https://meetup.toast.com/posts/295 참고자료를 바탕으로 직접 적용해보면서 작성한 글입니다. 국제화(i18n) 자동화 가이드 : NHN Cloud Meetup 프런트엔드 개발을 하다 보면 국제화와 번역을 수작업과

    uni-s-code.tistory.com

    이전 글을 읽지 않으신 분들은, 이전 글을 참고해주시길 바랍니다.

     

    이 글은 https://meetup.toast.com/posts/295 참고자료를 바탕으로 직접 적용해보면서 작성한 글입니다.

     


     

    이제 구글 스프레드시트와 연동이 끝났다면, 이제 다음과 같은 절차만 진행되면 번역 프로세스를 자동화 할 수 있습니다.

     

    첫번째로, 구글 스프레드 시트에 번역이 필요한 key 값을 코드를 스캔하여 업로드한다.(개발자)

    두번째로, 구글 스프레드 시트에 key 값이 올라가면, 구글 스프레드 시트에 번역가는 번역을 기입한다.(번역가)

    세번째로, 지금까지 완료된 key값과 번역값에 대해 구글 스프레드를 next.js에서 i18n 번역 값이 필요한 위치에 다운로드 한다.(개발자)

    이때, next i18n 지원하므로, public/locales 폴더 아래에 다운로드 하면 된다.

    1. key upload

    미리 i18next-scanner를 이용하여 public/locale 폴더 아래에 각 언어별 key 값을 json 파일로 저장해두었습니다.

    구글 스프레드 파일로 업로드 할 때, 다음과 같은 과정이 진행됩니다.
      1)
    스프레드 파일을 가져온다
      2)
    언어별 및 namespace json 파일을 하나하나 가져온다.
      3)
    기존에 json 파일에 있는 Key: value를 저장하고, 만약 value값이 없으면 value :”” 상태로 저장한다.(object 형식)
      4)
    네임 스페이스별 스프레드 시트가 없다면, 새로운 시트를 생성한다. 기존에 시트가 있다면 그 시트를 가져온다.
      5)
    각 스프레드 시트에 row에 차례차례로 넣는다.

     

    코드는 다음과 같습니다.

    async function addNewSheet(doc, title, sheetId) {
      const sheet = await doc.addSheet({
        sheetId: sheetId,
        title: title,
        headerValues: headerValues,
      });
    
      return sheet;
    }
    
    async function updateTranslationsFromKeyMapToSheet(doc, keyMap, title) {
      //시트 타이틀
      // const title = "localization";
      let sheet = doc.sheetsById[sheetIdByNs[title]];
      if (!sheet) {
        sheet = await addNewSheet(doc, title, sheetIdByNs[title]);
      }
    
      const rows = await sheet.getRows();
      console.log(rows);
      // find exsit keys
      const exsitKeys = {};
      const addedRows = [];
    
      rows.forEach((row) => {
        const key = row[columnKeyToHeader.key];
        if (keyMap[key]) {
          exsitKeys[key] = true;
        }
      });
    
      //스프레트시트에 row 넣는 부분
      for (const [key, translations] of Object.entries(keyMap)) {
        // console.log(key, translations);
        if (!exsitKeys[key]) {
          const row = {
            [columnKeyToHeader.key]: key,
            ...Object.keys(translations).reduce((result, lng) => {
              const header = columnKeyToHeader[lng];
              result[header] = translations[lng];
    
              return result;
            }, {}),
          };
          // console.log(row);
          // console.log(1);
          addedRows.push(row);
        }
        // console.log(2);
      }
    
      // upload new keys
      await sheet.addRows(addedRows);
    }
    
    // key값에 따른 언어 value
    function toJson(keyMap) {
      const json = {};
    
      Object.entries(keyMap).forEach(([__, keysByPlural]) => {
        for (const [keyWithPostfix, translations] of Object.entries(keysByPlural)) {
          json[keyWithPostfix] = {
            ...translations,
          };
        }
      });
    
      return json;
    }
    
    //언어 key : value 값 저장
    function gatherKeyMap(keyMap, lng, json) {
      for (const [keyWithPostfix, translated] of Object.entries(json)) {
        const key = getPureKey(keyWithPostfix);
    
        if (!keyMap[key]) {
          keyMap[key] = {};
        }
    
        const keyMapWithLng = keyMap[key];
        if (!keyMapWithLng[keyWithPostfix]) {
          keyMapWithLng[keyWithPostfix] = lngs.reduce((initObj, lng) => {
            initObj[lng] = NOT_AVAILABLE_CELL;
    
            return initObj;
          }, {});
        }
    
        keyMapWithLng[keyWithPostfix][lng] = translated;
      }
    }
    
    async function updateSheetFromJson() {
      const doc = await loadSpreadsheet();
    
      fs.readdir(localesPath, (error, lngs) => {
        console.log(localesPath);
        if (error) {
          throw error;
        }
    
        nsList.forEach((ns) => {
          const keyMap = {};
          lngs.forEach((lng) => {
            const localeJsonFilePath = `${localesPath}/${lng}/${ns}.json`;
            console.log(localeJsonFilePath);
            //.json file read
            // eslint-disable-next-line no-sync
            const json = fs.readFileSync(localeJsonFilePath, "utf8");
    
            gatherKeyMap(keyMap, lng, JSON.parse(json));
          });
          keyMapbyNs = keyMap;
          console.log(toJson(keyMapbyNs));
          updateTranslationsFromKeyMapToSheet(doc, toJson(keyMapbyNs), ns);
        });
    
        //스프레드 시트에 업데이트
      });
    }
    
    updateSheetFromJson();

     

    npm 명령어를 통해 위의 함수를 실행시키면 연결된 구글 스프레드 시트에 키값이 업로드 된다.

     

     

    2. 번역가가 번역 완료

    번역가가 아래와 같이 키값에 대해 번역을 완료했다고 합시다.

     

    3. key download

    이제 번역가가 key 값에 대해 번역을 완료했다 가정한 후, 

    이제 마지막으로 엑셀 파일을 다운로드 하는 일이 남았습니다.
    참고로 Next.js
    에서는 번역 파일을 Json 형태로 지정된 위치에 다운로드 한다면, 추가적인 과정없이 key값에 대해 언어에 맞게 번역 data로 변환해준다.

      1) public/locales 아래에 폴더들이 없을 일도 있으므로 폴더(파일) 검사부터 체크를 한다. 
      2) 폴더(파일)가 없으면 mkdirp 명령어를 통해 폴더 및 파일을 생성한다. 
      3) 번역본 스프레드를 가져온다.
      4) 네임 스페이스별 스프레드 시트를 읽어오면서 json 형태로 변환한다.
      5) 읽어온 json 형태를 폴더 및 파일에 write 한다.

     

    코드는 다음과 같습니다.

    async function fetchTranslationsFromSheetToJson(doc, ns) {
      const sheet = doc.sheetsById[sheetIdByNs[ns]];
      if (!sheet) {
        return {};
      }
    
      const lngsMap = {};
      const rows = await sheet.getRows();
    
      rows.forEach((row) => {
        const key = row[columnKeyToHeader.key];
        lngs.forEach((lng) => {
          const translation = row[columnKeyToHeader[lng]];
         
          if (translation === NOT_AVAILABLE_CELL) {
            return;
          }
    
          if (!lngsMap[lng]) {
            lngsMap[lng] = {};
          }
    
          lngsMap[lng][key] = translation || ""; 
        });
      });
    
      return lngsMap;
    }
    
    //디렉토리 설정
    function checkAndMakeLocaleDir(dirPath, subDirs) {
      return new Promise((resolve) => {
        subDirs.forEach((subDir, index) => {
          console.log(`${dirPath}/${subDir}`);
         
          mkdirp.sync(`${dirPath}/${subDir}`);
          if (index === subDirs.length - 1) {
            resolve();
          }
        });
      });
    }
    
    //json 파일 업데이트
    async function updateJsonFromSheet() {
      console.log(localesPath);
      await checkAndMakeLocaleDir(localesPath, lngs);
    
      const doc = await loadSpreadsheet();
      nsList.forEach(async (ns) => {
        const lngsMap = await fetchTranslationsFromSheetToJson(doc, ns);
    
        fs.readdir(localesPath, (error, lngs) => {
          if (error) {
            throw error;
          }
    
          lngs.forEach((lng) => {
            const localeJsonFilePath = `${localesPath}/${lng}/${ns}.json`;
    
            const jsonString = JSON.stringify(lngsMap[lng], null, 2);
    
            fs.writeFile(localeJsonFilePath, jsonString, "utf8", (err) => {
              if (err) {
                throw err;
              }
            });
          });
        });
      });
    }
    
    updateJsonFromSheet();

     

    ~다운받은 후 실제 번역값 적용 결과~

     

    4. npm 명령어 설정

    "scan:i18n": "i18next-scanner --config i18next-scanner.config.js",
    "upload:i18n": "npm run scan:i18n && node translationTool/upload.js",
    "download:i18n": "node translationTool/download.js"

    위와 같이 명령어를 설정하면 한줄 치고 엔터함으로써 손쉽게 업로드 및 다운로드가 가능합니다~

     

     

     

    4. i18n 번역 자동화 프로세스의 의의(장점)

     I18n을 제공하는 서비스에서, 성공적으로 이끌기 위한 가장 큰 요소는 번역이라고 할 수 있습니다.

    이때, 번역을 서비스에 입히기 위해서는 개발자와 번역자 간 번역본을 주고 받을 것이고, 이 과정이 자동화 되지 않는다면, 불필요한 의사소통과 리소스 낭비가 늘어나게 됩니다.

    그래서 i18n 자동화 프로세스를 통하여, 기존의 단순하고 불필요한 의사소통의 문제점을 극복하고자 하였습니다.

     

    본 프로세스를 실제 서비스에 적용한 결과, 
    번역을 요청하고 받으며 매번 복사하고 붙여넣기를 하는 과정에 대해 최소화 할 수 있어 생산성 측면에서도 좋은 결과를 얻었습니다.
    또한 수많은 번역 데이터를 효율적으로 프론트엔드단에서 처리할 수 있을지에 대해서도 고민을 해보며 하나의 번역 시트에 관리를 하는 게 아니라 여러 번역 시트를 구성하여 namespace 별 관리하는 방안을 추가하면서, 더 쉽게 번역 data를 관리할 수 있어졌습니다. 



Designed by Tistory.