다국어 서비스 지원하기

깡스·2023년 8월 2일
1

글로벌한 인터넷 세상인만큼 다국어를 지원하는 서비스가 많기에 저도 한번 제공해보려 합니다.

다국어를 지원하려면 문장을 여러 언어로 미리 번역해놔야 하는 만큼 모두 수기로 관리하기에는 번거롭기에 자동화 하는 과정까지 해보려 합니다.

i18next

먼저 다국어를 지원하는데 특화되어있는 라이브러리인 i18n을 사용하여 다국어를 지원할 예정입니다.

1. 설치

npm install next-i18next

저는 next환경에서 다국어를 지원하려 하기에, next버전으로 설치하도록 하겠습니다. 일반 i18next와 다르게 hooks를 통해 조작이 가능합니다.

2. config 설정

// next-i18next.config.js
module.exports = {
  i18n: {
    locales: ["ko", "jp"],
    defaultLocale: "ko",
  }
};

위와 같은 파일 명을 가진 설정 파일을 만들어 줍니다.
위 옵션 외에 다양한 옵션을 지원하고 있으며 링크에서 확인할 수 있습니다.

// next.config.js
const { i18n } = require("./next-i18next.config");

const nextConfig = {
  i18n,
  reactStrictMode: true,
};

module.exports = nextConfig;

작성한 설정 파일을 추가해 줍니다. 참고로 설정 파일의 next-i18next.config의 파일 명은 변경되면 안됩니다.

3. 주입

// _app.tsx
import { appWithTranslation } from "next-i18next";

const App = ({ Component, pageProps }: AppProps) => {
  return <Component {...pageProps} />;
};

export default appWithTranslation(App);

마지막으로 _app에서 주입해줍시다.

4. 다국어 파일 생성

번역 정보를 담은 json파일을 생성해야 합니다.

next-i18next.config.js에서 번역파일 리소스를 저장할 경로를 지정할 수 있으나, 따로 설정하지 않았을시 public/locales로 지정됩니다.

// public/locales/ko/common.json
{
  "hello": "안녕하세요"
}

// public/locales/jp/common.json
{
  "hello": "こんにちは"
}

저는 한국어와 일본어를 제공할 예정이기에 2개의 파일을 만들어 주었습니다.

5. 사용

import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

export default function Home() {
  const { t } = useTranslation("common");
  
  return <div>{t('hello')}<div>
}

export async function getServerSideProps({ locale }: { locale: string }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, ["common"]))
    }
  };
}

사용은 기본적으로 useTranslation를 통해서 사용하게 되며, 여기서 commonnamespace입니다.

serverSideTranslations를 통해 locale을 공급해주어여 하며, 모든 namespace를 사용하는 경우 두번째 인자가 필요하지 않으며, 저는 common만 사용할 예정이기에 따로 지정해주었습니다.

6. 언어 변경

const { t, i18n } = useTranslation("common");

i18n.changeLanguage('jp');

react의 경우 hooks를 통해 변경하면 리렌더링이 발생하기에 자연스럽게 언어가 변경되지만, next의 경우 getServerSideProps를 통해 주입해주고 있기에 리렌더링이 발생해도 변경되지 않습니다.

<button
  onChange={() => {
    router.push(router.pathname, router.asPath, { locale: "jp" });
  }}
>
  일본어
</button>

그렇기에 next의 경우 위와 같은 방법으로 router를 이용해 언어를 변경할 수 있습니다. 참고

이제 언어를 변경할 수 있게 되었습니다.


다국어 파일 생성 자동화

개발자가 json파일을 모두 수기로 관리하기에는 너무나 리소스가 많이 들어가기에, script를 통해 자동으로 생성해보려 합니다.

방법을 탐색하던 중 i18next-scanner, i18next-parser 두개의 라이브러리를 알게되었습니다.

두 라이브러리 모두 지정한 파일을 스캔해 i18n을 사용한 부분을 찾아 자동으로 json파일을 생성해줍니다.

1. i18next-scanner

먼저 시도한 방법은 i18next-scanner를 사용해 시도하였으나, hooks을 사용해야하는 next의 특성과 안맞는 부분이 있었습니다.

const { t } = useTranslation('common')

hooks의 경우 위처럼 인자로 namespace를 지정하게 되어있으나, 라이브러리에서 이 namespace를 인식하지 못하는 이슈가 있었고,

const { t } = useTranslation()

t('common:hello');

위와 같은 방법으로 namespace를 지정해주어야 라이브러리가 인식할 수 있었습니다.

2. i18next-parser

이 라이브러리는 확인해본 결과 hooks방식의 namespace도 잘 인식하기에 사용하기로 결정하였습니다.

npm install i18next-parser

먼저 위 명령어를 통해 설치해주세요.

// i18next-parser.config.js
const path = require("path");

const COMMON_EXTENSIONS = "**/*.{ts,tsx,html}";

module.exports = {
  locales: ["ko", "jp"],
  output: path.join(__dirname, "public", "locales", "$LOCALE", "$NAMESPACE.json"),

  input: [
    `src/components/${COMMON_EXTENSIONS}`,
    `src/pages/${COMMON_EXTENSIONS}`,
    "!**/node_modules/**",
    "!.next/**"
  ]
};

위와 같은 설정 파일을 만들어줍니다.
locales는 내가 지원할 언어의 목록이며, outputjson파일을 생성할 경로, input은 내가 탐색할 폴더들 이며, !의 경우 탐색하지 않는다는 명령어 입니다.

// package.json
"scripts": {
  "parser": "i18next --config i18next-parser.config.js"
}

script를 추가해줍니다.

npm run parser

이제 위 명령어를 통해 json파일을 자동으로 생성할 수 있게 되었습니다.


구글 스프레드시트와 동기화

혼자 진행하는 프로젝트일 경우 json파일만으로 관리하기에 나쁘지 않을 수 있습니다. 하지만 협업을 진행하기에는 아직 불편한 부분이 많기에, 구글 스프레드시트에 연결해 여러명이 같은 데이터를 바라보고 작업할 수 있게끔 구성해보려 합니다.

1. 설치

npm install google-spreadsheet google-auth-library -D

링크에서 라이브러리에 대해 확인하실 수 있으며, 스프레드시트를 사용할 때 도움을 주는 라이브러리 입니다.

2. 구글 시트 접근

const { JWT } = require("google-auth-library");
const { GoogleSpreadsheet } = require("google-spreadsheet");
const account = require("./account.json");

const GOOGLE_SHEET_ID = "YOUR_GOOGLE_SHEET_ID";
const BASE_PATH = "public/locales";

const loadGoogleSheet = async () => {
  const auth = new JWT({
    email: account.client_email,
    key: account.private_key,
    scopes: ["https://www.googleapis.com/auth/spreadsheets"]
  });

  const doc = new GoogleSpreadsheet(GOOGLE_SHEET_ID, auth);

  await doc.loadInfo();

  return doc;
};

module.exports = {
  loadGoogleSheet,
  BASE_PATH
};

위처럼 스프레드시트에 접근할 수 있으며, account는 구글의 계정 정보를 담고있는 json형태의 파일입니다. 발급 방법은 여기서 다루지 않겠습니다.

스프레드시트의 아이디는 url에서 확인할 수 있습니다.
그 후 공유 버튼을 통해 내가 발급받은 google아이디의 이메일을 초대하여 권한을 부여해 줍니다.

3. 구글 시트 JSON파일로 변환

// download.js
const fs = require("fs");
const { loadGoogleSheet, BASE_PATH } = require("./index");

const createDir = (path) => {
  return new Promise((resolve, reject) => {
    fs.mkdir(path, { recursive: true }, (error) => {
      if (error) reject(error);
      else resolve();
    });
  });
};

const createJson = (path, json) => {
  fs.writeFile(path, JSON.stringify(json), "utf8", (error) => {
    if (error) throw error;
  });
};

const sheetToJson = async (sheet) => {
  const rows = await sheet.getRows();

  const data = rows.reduce((acc, row) => {
    const rowData = row.toObject();
    const [[, key], ...languages] = Object.entries(rowData);

    languages.forEach(([language, value]) => {
      acc[language] = {
        ...acc[language],
        [key]: value ?? ""
      };
    });

    return acc;
  }, {});

  Object.entries(data).forEach(async ([language, values]) => {
    await createDir(`${BASE_PATH}/${language}`);

    createJson(`${BASE_PATH}/${language}/${sheet.title}.json`, values);
  });
};

const download = async () => {
  const doc = await loadGoogleSheet();

  Array(doc.sheetCount)
    .fill(0)
    .forEach(async (_, index) => {
      if (index === 0) return;

      const sheet = doc.sheetsByIndex[index];
      sheetToJson(sheet);
    });
};

download();

구글 시트를 불러와 json파일로 변환하는 코드입니다.

  1. createDir : 내가 저장하고자 하는 경로에 빈 폴더를 생성
  2. createJson : 시트를 json파일로 변환

파일로 저장할 때 내가 저장하고자 하는 경로에 폴더가 존재하지 않을 경우 오류가 발생하기에 먼저 빈 폴더를 생성한 후 json파일로 변환해주었습니다.

node src/globals/download.js

이제 node를 통해 download.js를 실행시켜 구글 시트의 데이터를 json파일로 변환할 수 있습니다.

4. JSON파일을 구글 시트에 업로드

const fs = require("fs");
const { loadGoogleSheet, BASE_PATH } = require("./index");

const readDir = (path) => {
  return new Promise((resolve, reject) => {
    fs.readdir(path, (error, fileList) => {
      if (error) reject(error);
      else resolve(fileList);
    });
  });
};

const readFile = (path) => {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (error, file) => {
      if (error) reject(error);
      else resolve(file);
    });
  });
};

const addRows = async (sheet, language, callback) => {
  const sheetName = sheet.title;
  const languageFile = await readFile(`${BASE_PATH}/${language}/${sheetName}.json`);
  const languageData = JSON.parse(languageFile);
  const languageDataArray = Object.entries(languageData);

  const rows = callback(languageDataArray);

  await sheet.addRows(rows);
};

const convertLanguageFile = async (doc, language, languages, file) => {
  const sheetName = file.split(".")[0];
  const sheet = doc.sheetsByTitle[sheetName];

  if (sheet) {
    const rows = await sheet.getRows();

    await addRows(sheet, language, (data) =>
      data.reduce((_data, [key]) => {
        const hasRow = rows.findIndex((row) => row.get("key") === key);

        return hasRow === -1 ? [..._data, { key }] : _data;
      }, [])
    );
  } else {
    const newSheet = await doc.addSheet({
      title: sheetName,
      headerValues: ["key", ...languages]
    });

    await addRows(newSheet, language, (data) =>
      data.reduce((_data, [key]) => [..._data, { key }], [])
    );
  }
};

const convertLanguageFolder = async (doc, language, languages) => {
  const files = await readDir(`${BASE_PATH}/${language}`);

  for (const file of files) {
    await convertLanguageFile(doc, language, languages, file);
  }
};

const upload = async () => {
  const doc = await loadGoogleSheet();

  const languages = await readDir(BASE_PATH);

  for (const language of languages) {
    await convertLanguageFolder(doc, language, languages);
  }
};

upload();

json파일을 해석해 구글 시트에 업로드하는 코드입니다.

  1. readDir: 해당 경로에 어떤 파일, 폴더가 존재하는지 반환합니다.
  2. readFile: 해당 경로에 파일을 가져와 반환합니다.
  3. addRows: 해당 시트에 맞는 파일을 callback의 인자로 넘기며, callback의 반환값을 시트에 추가합니다.

위와 같은 함수를 포함하고 있으며, 처음 만들어지는 시트와 이미 작성되어있는 시트의 로직을 구분해 실행시켜주었습니다.

개발자가 json에서 직접 수정해 업로드를 해도 결과가 시트에 반영되지 않기에 반드시 구글 시트를 통해 데이터를 수정해야합니다.

5. 결과

이제 npm run download, npm run parser, npm run upload 스크립트를 통해 구글 시트에 맞춰 데이터를 동기화할 수 있게 되었습니다.

참고 : https://meetup.nhncloud.com/posts/295

1개의 댓글

comment-user-thumbnail
2023년 8월 2일

감사합니다. 이런 정보를 나눠주셔서 좋아요.

답글 달기

관련 채용 정보