글로벌한 인터넷 세상인만큼 다국어를 지원하는 서비스가 많기에 저도 한번 제공해보려 합니다.
다국어를 지원하려면 문장을 여러 언어로 미리 번역해놔야 하는 만큼 모두 수기로 관리하기에는 번거롭기에 자동화 하는 과정까지 해보려 합니다.
먼저 다국어를 지원하는데 특화되어있는 라이브러리인 i18n을 사용하여 다국어를 지원할 예정입니다.
npm install next-i18next
저는 next
환경에서 다국어를 지원하려 하기에, next
버전으로 설치하도록 하겠습니다. 일반 i18next
와 다르게 hooks
를 통해 조작이 가능합니다.
// 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
의 파일 명은 변경되면 안됩니다.
// _app.tsx
import { appWithTranslation } from "next-i18next";
const App = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
export default appWithTranslation(App);
마지막으로 _app
에서 주입해줍시다.
번역 정보를 담은 json
파일을 생성해야 합니다.
next-i18next.config.js
에서 번역파일 리소스를 저장할 경로를 지정할 수 있으나, 따로 설정하지 않았을시 public/locales
로 지정됩니다.
// public/locales/ko/common.json
{
"hello": "안녕하세요"
}
// public/locales/jp/common.json
{
"hello": "こんにちは"
}
저는 한국어와 일본어를 제공할 예정이기에 2개의 파일을 만들어 주었습니다.
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
를 통해서 사용하게 되며, 여기서 common
은 namespace
입니다.
serverSideTranslations
를 통해 locale
을 공급해주어여 하며, 모든 namespace
를 사용하는 경우 두번째 인자가 필요하지 않으며, 저는 common
만 사용할 예정이기에 따로 지정해주었습니다.
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
파일을 생성해줍니다.
먼저 시도한 방법은 i18next-scanner
를 사용해 시도하였으나, hooks
을 사용해야하는 next
의 특성과 안맞는 부분이 있었습니다.
const { t } = useTranslation('common')
hooks
의 경우 위처럼 인자로 namespace
를 지정하게 되어있으나, 라이브러리에서 이 namespace
를 인식하지 못하는 이슈가 있었고,
const { t } = useTranslation()
t('common:hello');
위와 같은 방법으로 namespace
를 지정해주어야 라이브러리가 인식할 수 있었습니다.
이 라이브러리는 확인해본 결과 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
는 내가 지원할 언어의 목록이며, output
은 json
파일을 생성할 경로, input
은 내가 탐색할 폴더들 이며, !
의 경우 탐색하지 않는다는 명령어 입니다.
// package.json
"scripts": {
"parser": "i18next --config i18next-parser.config.js"
}
script
를 추가해줍니다.
npm run parser
이제 위 명령어를 통해 json
파일을 자동으로 생성할 수 있게 되었습니다.
혼자 진행하는 프로젝트일 경우 json
파일만으로 관리하기에 나쁘지 않을 수 있습니다. 하지만 협업을 진행하기에는 아직 불편한 부분이 많기에, 구글 스프레드시트에 연결해 여러명이 같은 데이터를 바라보고 작업할 수 있게끔 구성해보려 합니다.
npm install google-spreadsheet google-auth-library -D
링크에서 라이브러리에 대해 확인하실 수 있으며, 스프레드시트를 사용할 때 도움을 주는 라이브러리 입니다.
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
아이디의 이메일을 초대하여 권한을 부여해 줍니다.
// 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
파일로 변환하는 코드입니다.
createDir
: 내가 저장하고자 하는 경로에 빈 폴더를 생성createJson
: 시트를 json
파일로 변환파일로 저장할 때 내가 저장하고자 하는 경로에 폴더가 존재하지 않을 경우 오류가 발생하기에 먼저 빈 폴더를 생성한 후 json
파일로 변환해주었습니다.
node src/globals/download.js
이제 node
를 통해 download.js
를 실행시켜 구글 시트의 데이터를 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
파일을 해석해 구글 시트에 업로드하는 코드입니다.
readDir
: 해당 경로에 어떤 파일, 폴더가 존재하는지 반환합니다.readFile
: 해당 경로에 파일을 가져와 반환합니다.addRows
: 해당 시트에 맞는 파일을 callback
의 인자로 넘기며, callback
의 반환값을 시트에 추가합니다.위와 같은 함수를 포함하고 있으며, 처음 만들어지는 시트와 이미 작성되어있는 시트의 로직을 구분해 실행시켜주었습니다.
개발자가 json
에서 직접 수정해 업로드를 해도 결과가 시트에 반영되지 않기에 반드시 구글 시트를 통해 데이터를 수정해야합니다.
이제 npm run download
, npm run parser
, npm run upload
스크립트를 통해 구글 시트에 맞춰 데이터를 동기화할 수 있게 되었습니다.
감사합니다. 이런 정보를 나눠주셔서 좋아요.