운영 중인 닷컴 서비스에 **i18n(국제화)**을 도입을 하게 되었다. 단순히 번역 파일을 작성하는 것이 아닌, i18next-scanner로 번역 키를 자동 추출하고, 구글 스프레드시트와 연동하여 각 팀이 협업할 수 있는 워크플로우를 구축한 과정을 정리 해보려고한다.
서비스가 글로벌 유저를 타깃으로 확장되면서 걱정되었던 문제는 언어 지원이었다. 영어와 일본어,중국어등 최소5개 국어에 대한 언어를 도입하겠다는 계획에 번역 관리의 필요성에 대한 고민이 커졌다.
체감한 문제들:
이에 i18next 도입을 결정했다.
수동으로 번역 키를 추출하고 JSON에 정리하는 방법은 곧 한계에 다다랐다. 그래서 i18next-scanner를 도입했다.
var fs = require('fs');
var chalk = require('chalk');
const path = require('path');
// JavaScript와 TypeScript 파일 확장자
const COMMON_EXTENSIONS = '/**/*.{js,jsx,ts,tsx}';
module.exports = {
// 스캔 대상 입력 파일/디렉토리
input: [`pages${COMMON_EXTENSIONS}`, `components${COMMON_EXTENSIONS}`],
options: {
defaultLng: 'ko', // 기본 언어 설정: 'ko-KR'
createNamespace: true,
lngs: ['ko', 'en', 'ja', 'zh'], // 지원 언어 목록
trans: {
component: 'Trans',
i18nKey: 'i18nKey',
defaultsKey: 'defaults',
extensions: ['.js', '.jsx'],
fallbackKey: function (ns, value) {
return value;
},
},
resource: {
// 번역 파일 경로. {{lng}}, {{ns}}는 언어 및 네임스페이스로 대체
loadPath: 'public/locales/{{lng}}/{{ns}}.json',
savePath: 'public/locales/{{lng}}/{{ns}}.json',
jsonIndent: 2,
lineEnding: '\n',
},
ns: ['common', 'footer', 'event'],
defaultNs: 'common',
defaultValue(lng, ns, key) {
return '';
},
nsSeparator: ':',
keySeparator: false,
removeUnusedKeys: true, // 사용하지 않는 키 자동 제거
},
transform: function customTransform(file, enc, done) {
'use strict';
const parser = this.parser;
const content = fs.readFileSync(file.path, enc);
let ns;
const match = content.match(/useTranslation\(.+\)/);
if (match) ns = match[0].split(/(\'|\")/)[2];
let count = 0;
parser.parseFuncFromString(content, { list: ['t'] }, function (key, options) {
parser.set(
key,
Object.assign({}, options, {
ns: ns ? ns : 'common',
nsSeparator: ':',
keySeparator: false,
removeUnusedKeys: true, // 사용하지 않는 키 자동 제거
}),
);
++count;
});
if (count > 0) {
console.log(`i18next-scanner: count=${chalk.cyan(count)}, file=${chalk.yellow(JSON.stringify(file.relative))}`);
}
done();
},
};
input: pages와 components 폴더 내의 모든 .js, .jsx, .ts, .tsx 파일을 스캔.options: 기본 언어와 지원 언어, 네임스페이스, 사용하지 않는 키 자동 제거 설정 등..transform: 파일을 읽고 t() 함수 호출을 파싱하여 namespace에 맞게 JSON에 반영.이로써 코드 내 모든 번역 키를 자동으로 추출하고, namespace별로 정리할 수 있어 유지보수가 용이할 수 있도록 하였다.
개발자가 아닌 팀원들도 번역을 편하게 수정할 수 있어야 했다. json파일에 쉽게 접근할 수 없기에 구글 스프레드시트 사용을 선택하였다.
프로젝트 내 json형식으로 되어있는 번역 키 파일을 파싱하여 스프레드시트로 업로드해 팀원들이 수정할 수 있도록 동기화한다.
class I18nUploader {
constructor(spreadsheetId, credentials) {
this.spreadsheetId = spreadsheetId;
this.credentials = credentials;
}
async upload(namespace, data) {
const doc = new GoogleSpreadsheet(this.spreadsheetId);
await doc.useServiceAccountAuth(this.credentials);
await doc.loadInfo();
const sheet = doc.sheetsByTitle[namespace];
if (!sheet) return;
// 데이터의 키와 값을 시트에 업데이트
// 존재하지 않으면 새 행 추가
... 생략
}
}
스프레드시트에서 최신 번역 데이터를 json 형식으로 파싱하여 프로젝트로 가져올 수 있도록 동기화한다.
const { GoogleSpreadsheet } = require('google-spreadsheet');
const creds = require('./credentials.json');
const SPREADSHEET_ID = '스프레드시트-ID';
class I18nDownloader {
constructor(spreadsheetId, credentials) {
this.spreadsheetId = spreadsheetId;
this.credentials = credentials;
}
async download() {
const doc = new GoogleSpreadsheet(this.spreadsheetId);
await doc.useServiceAccountAuth(this.credentials);
await doc.loadInfo();
for (const sheet of doc.sheetsByIndex) {
const rows = await sheet.getRows();
// 각 시트의 데이터를 언어별 JSON로 변환 및 저장
... 생략
}
}
}
프로젝트에서 새로 추가된 번역 키를 스프레드시트에 반영.
스프레드시트 API를 통해 행 데이터를 가져오고, 언어별 JSON 파일을 생성.
비개발자인 팀원들이 스프레드시트에서 번역을 바로 수정할 수 있도록 함.
팀원들은 스프레드시트에서 바로 텍스트를 수정하고, 개발자는 스크립트 실행만으로 최신 번역을 반영할 수 있도록 하였다.
명령어:
"i18n:scan": "i18next-scanner --config i18next-scanner.config.js"
역할:
pages/**/*.js, ts, jsx, tsx 파일에서 t("...") 같은 번역 키를 자동으로 추출
결과:
i18n/locales/{{lng}}/{{ns}}.json 파일 업데이트 (키 추가)
명령어:
i18n:export: "node i18n/uploadSheets.js"
역할:
로컬 locales/ko/translation.json, locales/en/translation.json → Google Spreadsheet에 업로드
목적:
협업자가 구글 시트에서 번역을 쉽게 관리 가능
업로드 프로세스 (JSON → 구글 시트)
"i18n:export": "node i18n/uploadSheets.js"
flow:
translation.json (ko/en)
↓
Node.js 스크립트
↓
GoogleSpreadsheet 객체 생성
↓
useServiceAccountAuth(creds)
↓
loadInfo() → 시트 정보 로드
↓
시트 clear & setHeaderRow(['Key','Korean','English'])
↓
addRows(rows) → 구글 시트에 업로드
"i18n:import": "node i18n/downloadSheets.js"
flow:
Node.js 스크립트
↓
GoogleSpreadsheet 객체 생성
↓
useServiceAccountAuth(creds)
↓
loadInfo() → 시트 정보 로드
↓
sheet.getRows() → 모든 행 가져오기
↓
각 행을 Ko / En 객체에 매핑
↓
locales/ko/translation.json, locales/en/translation.json 저장
디렉토리 없으면 생성
JSON.stringify(..., null, 2)로 가독성 확보 (들여쓰기)
로컬 개발 환경에서 바로 실행 가능
A --> [Source Code pages/**] -->|i18n:scan| --> [Locales JSON]
B --> |i18n:import| google sheet --> 프로젝트 JSON 파싱
C --> |i18n:export| 프로젝트 JSON --> google sheet
D --> [Next.js Dev / Build]
E --> [Production 서비스]
예시 common 네임스페이스:
// public\locales\ko\common.json
{
"welcomeMessage": "환영합니다!",
"logoutButton": "로그아웃",
"profileTitle": "프로필"
}
// public\locales\en\common.json
{
"welcomeMessage": "welcome!",
"logoutButton": "logout",
"profileTitle": "profile"
}
-------------------
// public\locales\ko\footer.json
{
"terms": "이용약관",
"policy": "개인정보취급방침",
"businessInfo": "사업자정보"
}
// public\locales\en\footer.json
{
"terms": "Terms",
"policy": "Policy",
"businessInfo": "BusinessInfo"
}
...
스프레드시트에서 키와 번역 값을 관리하면 각 네임스페이스별('common.json', 'footer.json' ...등) 언어별(ko.json, en.json, ...등 ) JSON 파일이 자동 생성된다.
이번 i18n 도입은 팀 전체가 효율적으로 번역을 관리할 수 있는 시스템을 고민할 수 있는 경험이 되었다. 글로벌 서비스 준비에 있어 자동화와 협업 중심 접근은 필수적임을 확인했다. 또한 구글시트를 json 파싱할때에 생기는 실수 혹은 오류들에 대한 검증 프로세스도 준비해야 할 것 같다. 각 행에 대한 빈값이나, 허용되지 않는 문자 등.. 정책을 정하고 해당 정책에 대한 검증 로직을 추가 예정이다.