TL;DR

국제화 처리를 위한 파일을 직접 작성 없이 구글 시트에 입력된 내용을 기반으로 파일을 생성하고, 나아가 파일이 없어도 국제화 처리를 할 수 있도록 예시를 들어 설명합니다.

샘플 웹 페이지

소스코드 저장소로 이동

들어가며

국제화를 고려한 웹 애플리케이션을 작성해야 하는 경우, 화면 내에 등장하는 모든 텍스트가 별도의 국제화 처리기를 통해 표현될 수 있도록 준비해야 합니다.

저는 현재 nuxt.js로 웹 앱을 주로 만들고 있고, 국제화 처리를 위해 nuxt-i18n 모듈을 함께 사용하고 있습니다.

이런 환경에서, 국제화 처리를 위해 별도로 key:value 형태로 텍스트를 모아 둔 파일(이하 사전으로 표현)을 다음과 같이 만들어 왔습니다.

// locales/ko/index.js 파일
export default {
  common: {
    title: '제목',
    // ...
  },
  // 다른 속성들
}

당연한 얘기이지만 위와 같은 형태로 작업하면, 번역된 텍스트가 변경 될 때마다 해당 텍스트 파일을 수정해야 하고, 그 이후에 별도로 배포를 해야 해당 환경에 적용 됩니다.

이렇게 수동으로 사전 파일을 작성해야 한다는 것은 매우 불편하고, 또 실수하기 쉬운 일이라고 생각했습니다.

그래서 이 파일을 손으로 직접 작성하지 않고, 구글 시트에 저장된 내용을 기반으로 파일이 만들어지도록 하는 컨셉에서, 더 나아가 아예 파일을 만들지 않고 구글 시트의 내용을 바로 끌어다 쓰면 어떨까 하는 생각에서 이 작업을 시작하게 되었습니다.

이를 구현하기 위해, 이전에 만들었던 public-google-sheets-parser 라이브러리를 기반으로 한 nuxt-google-sheets-parser 모듈을 통해 예시용 웹앱을 한번 작성 해 보려고 합니다.

실제로 얼마나 유용하게 쓰일 수 있을지는 잘 모르겠지만, 최소한 개발 단계에서는 사전용 파일을 만들지 않아도, 원하는 키에 원하는 값들을 편리하게 정의하고 사용할 수 있다는 점이 장점이 될 것이라 생각하며 글 작성을 시작합니다.

설계 과정

먼저, 구글 시트 문서를 다음과 같은 형태로 준비합니다.

ko en ja key key1 key2 key10
제목 title タイトル 수식 common title    
내용 Contents 内容 수식 common description    
버튼 button ボタン 수식 common button    
문서 경로 Documents path ドキュメントパス 수식 common sheetsPath    
  • 참고용 샘플 문서 링크

  • 위 문서는 ko 필드에만 한글 텍스트를 넣었고, en, ja 필드 값은 전부 GOOGLETRANSLATE 함수를 통해 구글이 번역해준 결과를 사용하도록 해 두었습니다. 실제 번역된 텍스트가 나오기 전 까지 임시로 사용할 수 있을 것이며, 이 부분은 최종적으로 검수를 마친 텍스트로 대체되어야 할 것입니다.

  • 입력이 불편해서 시트의 key 열이 key10의 오른쪽에 있었으면 하는 생각이 드신다면, 그렇게 이동하시고 위 함수의 참조 셀들의 값을 변경하시면 됩니다. 또한 key10이 너무 많다면, 불필요한 만큼 지우고 사용하셔도 됩니다.

각각의 key 셀에는 다음과 같은 함수를 넣어, key1 ~ key10까지의 값을 dot(.)을 기준으로 병합되여 표현되도록 했습니다. depth가 더 필요한 경우엔 그만큼 추가하거나, 감소할 수 있습니다.

// D2 셀 기준, 그 아래 셀은 이 수식을 복사하여 사용
=CONCATENATE(
  E2,
  IF(F2 <> "", "."&F2, ""),
  IF(G2 <> "", "."&G2, ""),
  IF(H2 <> "", "."&H2, ""),
  IF(I2 <> "", "."&I2, ""),
  IF(J2 <> "", "."&J2, ""),
  IF(K2 <> "", "."&K2, ""),
  IF(L2 <> "", "."&L2, ""),
  IF(M2 <> "", "."&M2, ""),
  IF(N2 <> "", "."&N2, ""),
  IF(O2 <> "", "."&O2, "")
)

이렇게 만들어진 key와 locale(ko, en, ja, …) 셀에 담긴 값들을 기준삼아 Javascript의 Object로는 다음과 같이 표현되기를 기대했습니다.

// key필드에 담긴 값이 'common.title'이고, ko 필드에 담긴 값이 '제목'인 경우
{
  common: {
    title: '제목'
  }
}

위 형태로 얻은 결과를 사전으로 사용할 수 있도록, locale/${locale}/index.js 파일을 다음과 같은 형태로 작성했습니다.

// locale/base.js
import set from 'lodash/set'

export default async (context, locale) => {
  // https://docs.google.com/spreadsheets/d/1TZu5G5VxPRoXeCjY7-OQbHFp09y73wGTiDsyzk6UwPQ/edit
  const sheetId = '1TZu5G5VxPRoXeCjY7-OQbHFp09y73wGTiDsyzk6UwPQ'
  const sheetName = 'dictionary'
  const dictionary = {}

  const response = await context.$gsparser.parse(sheetId, sheetName)
  response.forEach((item) => set(dictionary, item.key, item[locale]))

  return Promise.resolve(dictionary)
}

// locale/ko/index.js
import base from '../base'

export default (context) => {
  return base(context, 'ko')
}

// locale/en/index.js
import base from '../base'

export default (context) => {
  return base(context, 'ko')
}

// 그 외 필요한 만큼 생성..

이렇게 준비한 뒤, nuxt.config.js 파일의 nuxt-i18n 설정을 다음과 같이 했습니다.

// nuxt.config.js
{
  // ...다른 설정 생략...

  modules: [
    [
      'nuxt-i18n',
      {
        locales: [
          { code: 'ko', iso: 'ko-KR', file: 'ko/index.js' },
          { code: 'en', iso: 'en-US', file: 'en/index.js' },
          { code: 'ja', iso: 'ja-JP', file: 'ja/index.js' },
        ],
        langDir: 'locales/',
        lazy: true,
        defaultLocale: 'ko',
        strategy: 'prefix_and_default',
        vuex: {
          moduleName: 'i18n',
          syncLocale: true,
          syncMessages: true,
          syncRouteParams: true,
        },
      },
    ],
    // 반드시 nuxt-i18n 모듈 등록 이후에 등록되어야 합니다.
    'nuxt-google-sheets-parser',
    // 다른 모듈 생략
  ],

  // ...다른 설정 생략...
}

그리고 이렇게 연결된 상태라면, 다음과 같이 template 내에서 사용할 수 있습니다.

<template>
  <div>
    <!-- key가 common.title인 국제화 텍스트가 알맞게 표현됩니다. -->
    <h1 :text="$t('common.title')" />
  </div>
</template>

이처럼 연결시켜 두면, 별도의 파일을 만들지 않고도 국제화 텍스트를 쉽게 표현할 수 있습니다.

하지만 이렇게 두면 구글 시트 API에 장애가 발생했을 때, 사이트의 모든 텍스트가 깨질 수 있다는 치명적인 단점이 있습니다.

이 부분은 구글 시트 파일을 기반으로 사전용 파일을 자동 생성하여 소스코드에 포함되도록 해 주는 방식으로 커버할 수 있을 것이라고 생각했습니다.

그래서 아래와 같은 스크립트를 통해 locale별 fallback.json 파일이 자동으로 생성될 수 있도록 했습니다.

// makeDictionary.js
const fs = require('fs')
const _ = require('lodash')
const PublicGoogleSheetsParser = require('public-google-sheets-parser')

const parser = new PublicGoogleSheetsParser()
const sheetId = '1TZu5G5VxPRoXeCjY7-OQbHFp09y73wGTiDsyzk6UwPQ'
const sheetName = 'dictionary'

const targetLanguages = ['ko', 'en', 'ja']

parser.parse(sheetId, sheetName).then((rows) => {
  const dictionary = {}

  // 언어별 사전을 생성합니다. { [locale]: { key: value, key: value, ... } } 형태가 되도록 합니다.
  rows.forEach((row) => targetLanguages.forEach((lang) => _.set(dictionary, `${lang}.${row.key}`, row[lang])))

  // 필요한 언어들에 대한 fallback 파일을 생성합니다.
  targetLanguages.forEach((lang) => fs.writeFileSync(`./locales/${lang}/fallback.json`, JSON.stringify(dictionary[lang])))

  console.log(`${targetLanguages.length} files created.`)
})

이제 필요할 때 마다 위 스크립트를 실행하면, 언어별 fallback 파일이 작성되도록 준비가 되었습니다. 터미널에서 아래 명령을 실행하면, locales 디렉토리에 각각의 fallback 파일이 생성됩니다.

$ node makeDictionary
# 3 files created.

위 부분을 package.json에 넣어서 사용해도 되고, 필요할 때 마다 그냥 호출해서 사용할 수 있을 것입니다.

추가된 fallback 파일의 지원을 위해, 위에 작성했던 base.js파일에 response가 falsy한 경우 fallback 파일을 읽어와서 사전으로 사용하도록 처리를 추가합니다.

import set from 'lodash/set'

export default async (context, locale) => {
  // https://docs.google.com/spreadsheets/d/1TZu5G5VxPRoXeCjY7-OQbHFp09y73wGTiDsyzk6UwPQ/edit
  const sheetId = '1TZu5G5VxPRoXeCjY7-OQbHFp09y73wGTiDsyzk6UwPQ'
  const sheetName = 'dictionary'
  const dictionary = {}

  const response = await context.$gsparser.parse(sheetId, sheetName)
  if (response.length) {
    response.forEach((item) => set(dictionary, item.key, item[locale]))
  } else {
    const { default: fallbackDictionary } = require(`./${locale}/fallback.json`)
    Object.assign(dictionary, fallbackDictionary)
  }

  return Promise.resolve(dictionary)
}

이렇게 하면, API로부터 제대로된 응답을 돌려받지 못하게 되는 경우, fallback 파일을 사용할 수 있게 됩니다.

이런 방식으로 국제화를 구현하게 되면 개발 중에는 최대한 편하게 진행할 수 있는 것 같습니다.

만약 사전에 더 이상 추가될 것도, 수정될 것도 없다면, 파일만 가져와서 동작하도록 base.js 파일의 내용을 변경해서 사용할 수 있을 것 같습니다.

결론

json파일을 신경쓰지 않고, 공개 권한을 가진 스프레드시트만으로 최대한 간단하게 국제화 처리를 구현할 수 있는 방법을 소개 해 보았습니다.

개발 단계이거나, 아직 안정화가 덜 되어 문서를 수시로 수정해야 하는 경우에 특히 요긴하게 사용할 수 있을 거라고 생각했습니다.

운영 환경에서는 동적으로 가져오지 않고, 등록된 fallback.json 파일만 사용하겠다면 그런 방식으로 설정해서 사용할 수도 있습니다.

이 내용을 간단히 정리하여 공개 저장소에 등록 해 두었습니다.

읽어주신 분들에게 조금이라도 도움이 되는 내용이었으면 합니다.

읽어주셔서 감사합니다!