서비스 다국어화: Google Sheets 로 자동화하기

글로벌로 서비스하기 위해 번역이 고민되시나요? 플라멜은 Google Sheets를 활용해 로컬 유저에게 친화적인 언어로 번역을 자동화하고 있습니다 :)
Flamel Team's avatar
Dec 26, 2024
서비스 다국어화: Google Sheets 로 자동화하기
서비스를 개발하기 시작하면 이 서비스를 국내만을 타겟으로 할 것인가, 글로벌을 타겟으로 할 것인가를 결정해야합니다.
플라멜은 초기부터 글로벌을 타겟한 이미지 생성 AI 사이트입니다. 하지만 국내 유저에게 있어서 기존 이미지 생성 AI 서비스들이 영어로 되어있는 것이 큰 허들이었습니다. 이에 플라멜은 자연스러운 영어로 구현된 서비스임과 동시에, 국내 유저들이 한국어로 서비스를 사용할 때 자연스럽고 사용하기 쉽기를 바랐습니다.
이에 서비스 안의 모든 표현들은 로컬 유저에게 자연스럽도록 한국어와 영어 모두를 깊은 고민을 통해 직접 구성하고 유저 친화적인 방향으로 꾸준한 수정을 거치고 있습니다. 이를 Google Sheets를 활용해 자동화한 방법을 공개합니다.
 
방금 랜딩페이지 타이틀 문구를 고쳤는데, 잠시 후 따옴표를 추가해달라는 요청을 받은 프론트엔드 개발자의 모습
방금 랜딩페이지 타이틀 문구를 고쳤는데, 잠시 후 따옴표를 추가해달라는 요청을 받은 프론트엔드 개발자의 모습

도입 계기

문구 수정

제가 프론트엔드 개발자로서 일하며, PM 분들에게 가장 많이 요청받았던 일 중 하나는 문구 수정이었습니다. 프로젝트를 진행해본 분들이라면 많이들 공감 하시지 않을까 생각합니다. 기획 회의를 할 때마다 생기는 랜딩페이지 캐치프레이즈 수정 요청, 띄어쓰기 또는 쉼표 추가 등의 요청 말이죠. 하나의 기능을 개발하기 위한 PM과 개발자의 최소한의 소통은 개발 요청 - 리뷰 요청 - 배포 요청 - 배포 확인 으로, 최소 4단계를 거치게 되는데요. 문구 수정은 가장 빠르게 개발할 수 있는 요청사항 중 하나이지만, 불필요한 커뮤니케이션을 만들어내는 주범이기도 합니다. 스모어톡은 주 2회 신규 기능 배포를 진행할 만큼 빠른 개발 주기를 가지고 있는데요. 속도를 따라가기 위해서는 불필요한 소통을 줄여야했습니다.
 

다국어 지원

저희의 이미지 생성 서비스, 플라멜을 이용해주시는 해외 사용자들이 늘어남에 따라 사이트 국제화의 필요성을 느끼게 되었습니다. 지원하는 언어가 늘어남에 따라, 개발 및 번역 과정에 필요한 커뮤니케이션의 양이 언어의 종류에 비례해서 늘어났습니다. 실수를 야기하는 반복적인 수작업에 개발자와 PM 모두 지쳐갔고, 결국 이 과정을 자동화 해야겠다는 결정을 내리게 되었습니다.
 
스모어톡은 사이트 국제화를 위해 i18next 라이브러리를 사용하고 있습니다. i18next 에서는 각 텍스트의 위치를 하나의 key로 표현합니다. 사이트가 지원하는 언어마다 key: text 로 이루어진 json 파일을 준비해두고, 해당 위치를 포함한 컴포넌트가 렌더링 될 때 언어에 맞는 json 파일을 읽어오는 방식입니다.
// ko.json { "key": "텍스트", "flamel": "최고의 이미지 생성 서비스" } // sample.tsx <div> <p>{i18next.t('flamel')}</p> <p>최고의 이미지 생성 서비스</p> </div>
 
i18next를 사용하는 여러 방법 중, 저희는 nhn 클라우드의 Google sheets를 활용한 국제화 자동화 가이드를 참고하여 문구 수정 및 다국어 지원을 자동화하고 있습니다. PM 분들이 스프레드 시트에 각 key에 해당하는 텍스트 및 번역을 넣어주시면, 프론트엔드는 해당 시트를 다운로드 받아 사용하는 방식입니다.
 
이 과정은 총 3단계로 이루어집니다.
  1. 프론트엔드 개발자가 스프레드 시트에 번역이 필요한 텍스트들의 key를 기입
  1. 번역 담당자가 key에 해당하는 텍스트 및 번역을 기입
  1. 프론트엔드 개발자가 스프레드 시트를 다운받아, 언어 별 json 파일로 정제
 

자바스크립트를 이용한 자동화

모든 코드는 이 코드에 기반하여 직접 수정하였습니다.

패키지 설치

우선 필요한 패키지들을 설치해줍니다.
npm install -S i18next // 작성 당시 버전 23.4.6 npm install -S i18next-parser // 작성 당시 버전 8.12.0 npm install -S google-spreadsheet // 작성 당시 버전 4.1.1
 
i18next-parser는 소스코드에서 i18n.t(), i18next.t() 와 같은 지정된 패턴을 스캔하여 key를 추출하고 언어 별 json 파일을 만들어줍니다.
google-spreadsheet는 Google Sheets API를 자바스크립트에서 사용할 수 있게 해주는 라이브러리입니다. 우리는 json 파일을 파싱하여 스프레드 시트를 채우고, 스프레드 시트를 다운받아 json 파일로 변환할 것입니다.
 

환경 설정

스크립트 추가

package.json에 필요한 스크립트들을 추가합니다.
// package.json "scan:i18n": "i18next 'src/**/*.tsx' 'src/**/*.ts' ", "upload:i18n": "npm run scan:i18n && node translate/upload.js", "download:i18n": "node translate/download.js", "build": "tsc && npm run download:i18n && vite build --logLevel error"
플라멜은 타입스크립트를 사용하고 있기에, scan:i18n 명령어로 src 폴더 내부의 모든 tsx 파일과 ts 파일을 검사하게 하였습니다. 스캔을 통해 얻은 json 파일을 upload:i18n 명령어를 통해 스프레드 시트에 업로드합니다. download:i18n 명령어를 통해 스프레드 시트를 다운받아 텍스트와 번역을 업데이트할 수 있고, 프로젝트를 빌드할 때마다 스프레드시트를 다운로드 받게 해두었습니다.
 

구글 스프레드 시트 설정

google-spreadsheet 를 사용하여 특정 스프레드시트에 접근하기 위해서는 접근 권한이 부여된 구글 계정이 필요하다. 여기를 참고하여 봇 계정(service account)을 설정할 수 있다. 이 계정을 이용하여 JWT 방식을 통해 스프레드시트에 접근할 것입니다.
 
JWT 토큰 발급에 필요한 credential을 얻는 방법은 여기에 나와있습니다. 다음과 같이 생긴 json 파일입니다.
// Flamel-IAM.sjon { "type": "service_account", "project_id": "...", "private_key_id": "...", "private_key": "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----\n", "client_email": "...", "client_id": "...", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "...", "universe_domain": "googleapis.com" }
 
준비된 credential을 갖고 jwt 토큰을 발급받습니다.
// translate/index.js import { JWT } from "google-auth-library"; import creds from "./credentials/Flamel-IAM.json" with { type: "json" }; const SCOPES = [ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive.file', ]; const jwt = new JWT({ email: creds.client_email, key: creds.private_key, scopes: SCOPES, });
 
발급받은 jwt 토큰을 이용해 스프레드시트를 다운로드하는 방법은 아래와 같습니다.
spreadsheetDocId는 스프레드시트 URL에 나와있습니다.
  • https://docs.google.com/spreadsheets/d/:spreadsheetDocId/edit?gid=:sheetId
async function loadSpreadsheet() { // eslint-disable-next-line no-console console.info( "\u001B[32m", "=====================================================================================================================\n", "# i18next auto-sync using Spreadsheet\n\n", " * Download translation resources from Spreadsheet and make /assets/locales/{{lng}}/{{ns}}.json\n", " * Upload translation resources to Spreadsheet.\n\n", `The Spreadsheet for translation is here (\u001B[34mhttps://docs.google.com/spreadsheets/d/${spreadsheetDocId}/#gid=${sheetId}\u001B[0m)\n`, "=====================================================================================================================", "\u001B[0m" ); // spreadsheet key is the long id in the sheets URL const doc = new GoogleSpreadsheet(spreadsheetDocId, jwt); await doc.loadInfo(); // loads document properties and worksheets return doc; }
 
스프레드 시트에 업로드/다운로드 하는 코드는 이 코드를 참고하시길 바랍니다.
 

언어 자동 감지

플라멜은 URL과 사용자 브라우저 설정에 맞는 언어로 웹사이트를 보여주고 있습니다. 자동으로 언어를 감지하기 위해서는 별도의 설정이 필요합니다.
npm install -S i18next-browser-languagedetector // 작성 당시 버전 7.2.0
 

i18next 사용하기

i18next를 사용하기 위해서는 i18next 인스턴스를 초기화 해주어야합니다.
각 옵션에 대한 설명은 i18next 공식 문서i18next-browser-languagedetector 공식 문서를 참고해주세요.
export function initializeI18next() { return i18next .use(LanguageDetector) .use(initReactI18next) .init({ detection: { order: ["path", "navigator"] }, debug: process.env.NODE_ENV !== "production", fallbackLng: "ko-KR", nonExplicitSupportedLngs: true, supportedLngs: ["ko", "en"], keySeparator: false, nsSeparator: false, parseMissingKeyHandler(key) { /* eslint-disable-next-line no-console */ // console.warn("parseMissingKeyHandler", `'key': '${key}'`); const keySeparator = "~~"; const value = key.includes(keySeparator) ? key.split(keySeparator)[1] : key; return value; }, resources: getResources(lngs) }); } export const i18n = i18next;
 
웹페이지가 렌더링 되기 전에 i18next 인스턴스를 초기화해두면, 렌더링 과정에서 i18next 인스턴스에 접근하여 원하는 텍스트를 보여줄 수 있습니다.
// main.tsx import { i18n, initializeI18next } from "translate/i18next"; initializeI18next() .then(() => { console.log("init i18n", i18n.language); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( <App /> ); }) .catch(console.error);
 

interpolation

사용자의 플랜, 플랜 별 이미지 최대 선택 개수를 하드코딩 하는 건 번거롭다
사용자의 플랜, 플랜 별 이미지 최대 선택 개수를 하드코딩 하는 건 번거롭다
텍스트를 입력하다보면, 문구의 내용을 렌더링 도중 결정해야하는 때가 생깁니다. i18next는 렌더링 과정에서 key에 해당하는 텍스트를 Interpolate 하는 기능을 지원하고 있습니다.
이를 위해 i18next 인스턴스 초기화 과정에 다음 옵션을 추가합니다.
i18next.init({ ... interpolation: { prefix: "%{", suffix: "}" }, ... })
번역 담당자 분께서 변수에 해당하는 위치는 %{}로 표기해주시면, interpolation이 가능합니다. 실제 위 토스트 알림의 경우 %{planName} 플랜은 최대 %{maxMultiSelect}개까지 동시에 작업할 수 있어요 라고 표기되어있습니다.
 

HTML 지원

특정 부분만 볼드체로 강조하기
특정 부분만 볼드체로 강조하기
특정 부분을 이탤릭으로 표기하거나 볼드체로 표기하고 싶을 수도 있습니다. react-i18next는 HTML을 지원하기에, <i> 또는 <b> 와 같은 기본 태그들을 이용할 수 있습니다.
이를 위해 i18next 인스턴스 초기화 과정에 다음 옵션을 추가합니다.
i18next.init({ ... react: { transKeepBasicHtmlNodesFor: [ "br", "strong", "i", "p", "s", "b", "a", "small" ] }, ... })
transKeepBasicHtmlNodesFor 배열 속에 허용하고싶은 html 태그를 추가합니다.
 
이 기능을 사용하기 위해서는, 반드시 react-i18next의 <Trans>를 사용해야합니다. 실제 위 사진 속 모달 제목은 다음과 같이 렌더링되고 있습니다.
/** ko-KR.json { ... StyleClipModalTitle: "<strong>내 스타일</strong>에 저장할까요?" ... } */ <p className="text-balance text-start text-[22px] leading-[160%] [&>strong]:font-bold [&>strong]:text-flamel-purple-700"> <Trans i18nKey="StyleClipModalTitle" /> </p>
 
 

번역 파일 자동 동기화

필요한 코드를 모두 준비해두었습니다. 이제 npm run upload:i18n을 실행하면 페이지 내 모든 컴포넌트의 key가 스프레드시트에 다음과 같은 형식으로 업로드됩니다.
영어
한글
GenerateTitle
MyImg_Noti_MaxSelectError_Text
이제 담당자 분에게 번역을 요청드리면 됩니다. Interpolation 또는 HTML을 사용했다면 기획안에 맞게 한글을 채워두면 번역 담당자 분이 형식에 맞게 기입하는데 도움이 되겠죠?
영어
한글
GenerateTitle
Generate images with <strong>consistent tone and style</strong>
<strong>톤앤매너를 유지한</strong> 이미지를 만들어보세요
MyImg_Noti_MaxSelectError_Text
%{planName} 플랜은 최대 %{maxMultiSelect}개까지 동시에 작업할 수 있어요
 

결론

 
항상 내가 멘션된 슬랙은 무섭다.
항상 내가 멘션된 슬랙은 무섭다.
만약 기획 회의가 진행된 이후, 다음과 같은 슬랙이 왔다면 어떨까요? 예전 같았다면, 빠르게 처리할 수 있는 일이니 하던 (1)작업을 멈추고, (2)직접 컴포넌트를 찾은 뒤, (3)텍스트와 번역을 수정하고, (4)베타에 배포한 뒤 (5)리뷰 요청을 드렸을 겁니다. 하지만, 이제 우리는 key 값과 스프레드시트 주소만 전달드리면 됩니다. 문구는 다음 배포 때 알아서 수정될 것입니다. 빌드할 때마다 npm run download:i18n 이 실행될 테니까요!
 
불필요한 소통을 자동화를 통해 줄여 기분이 좋아진 프론트엔드 개발자의 모습
불필요한 소통을 자동화를 통해 줄여 기분이 좋아진 프론트엔드 개발자의 모습
이게 끝입니다. 프론트엔드 개발자는 더 이상 번역 및 문구 수정 요청에 대해 신경 쓸 필요가 없습니다!
 
Share article
Write your description body here.

Flamel's blog