728x90

소개

💡 i18n은 국제화 프레임워크로서 다양한 프레임워크나 플랫폼에서 활용 가능하다.

공식 웹사이트 : https://www.i18next.com/

 

Introduction | i18next documentation

 

www.i18next.com

여기서는 해당 프레임워크를 Nuxt3와 결합해 프론트엔드 텍스트들을 구글 시트와 통합하여 빌드 시 마다 현지화 텍스트를 자동으로 동기화하고 적용하는 방법을 다룬다. 이 방식을 통해 번역된 텍스트를 더욱 효율적으로 관리 및 유지보수가능하다.


목차


필요 라이브러리 설치

Front-end

  • 다국어 처리 프레임워크 i18n
    npx nuxi@latest module add i18n
  • 구글 인증
    yarn add google-auth-library
  • 구글 스프레드시트
    yarn add google-spreadsheet

구글 시트 API 사용 설정

  1. 구글 클라우드 콘솔 에서 프로젝트 생성
  2. Google Sheets API 검색 후 사용 설정
  3. API/서비스 세부정보에서 사용자 인증정보 만들기 → 서비스 계정 생성
  4. 계정 클릭 → "키" 탭 → 새 키 생성 → JSON 다운로드

예시 JSON:

{
  "type": "service_account",
  "project_id": "liahnson",
  "private_key": "-----BEGIN PRIVATE KEY-----\ntesttesttest\n——END PRIVATE KEY-----\n",
  "client_email": "service-account@test.iam.gserviceaccount.com",
  "client_id": "1234",
  "token_uri": "https://oauth2.googleapis.com/token"
}

프로젝트 기본 구조화

  • locales 폴더 생성
  • 다운받은 JSON 파일을 credentials.json 으로 이름 변경 → locales 폴더에 위치
  • sync.js 파일 작성 (구글시트 ↔ JSON 동기화)

sync.js 예제

// 모듈 및 전역 변수 선언
const { GoogleSpreadsheet } = require('google-spreadsheet');
const { JWT } = require('google-auth-library');
const credentials = require('./credentials.json'); // 서비스 계정 인증 파일 경로
const fs = require('fs');
const path = require('path'); // 경로 조작을 위한 모듈 추가
const _ = require('lodash');

// 전역 설정
const localesPath = 'locales';
const lngs = ['ko', 'en']; // 언어 목록
const spreadsheetDocId = 'doc_id_test';
const fileNm = 'translation';

// Google API 인증
const serviceAccountAuth = new JWT({
    email: credentials.client_email,
    key: credentials.private_key,
    scopes: ['<https://www.googleapis.com/auth/spreadsheets>'],
});

// 스프레드시트 로드 함수
async function loadSpreadsheet() {
    console.info(
        '\\u001B[32m',
        '=====================================================================================================================\\n',
        `(\\u001B[34mhttps://docs.google.com/spreadsheets/d/${spreadsheetDocId}/#gid=0\\u001B[0m)\\n`,
        '=====================================================================================================================',
        '\\u001B[0m'
    );

    const doc = new GoogleSpreadsheet(spreadsheetDocId, serviceAccountAuth);
    await doc.loadInfo(); // 문서 정보 로드
    return doc;
}

// 시트 데이터 매핑 함수
async function makeTranslationsMapFromSheet(sheet, lngsMap) {
    if (!sheet) {
        console.error('Sheet not found');
        return lngsMap;
    }

    const rows = await sheet.getRows();

    rows.forEach((row) => {
        const combinedKey = row._rawData[5]; // Combined key는 해당 위치에 있다고 가정

        if (combinedKey) {
            lngs.forEach((lang, langIndex) => {
                let translation = row._rawData[langIndex + 3]; // ko, en 필드에 접근

                if (translation) {
                    translation = translation.replace(/\\\\n/g, '\\n'); // 이스케이프된 \\n 변환
                    _.set(lngsMap, `${lang}.${combinedKey}`, translation);
                } else {
                    console.warn(`No translation found for ${lang} in row with combined key: ${combinedKey}`);
                }
            });
        }
    });

    return lngsMap;
}

// JSON 파일로 저장 함수
function saveTranslationsToFile(lngsMap) {
    lngs.forEach((lng) => {
        const localeJsonFilePath = path.join(__dirname, `${lng}.json`);
        const jsonString = JSON.stringify(lngsMap[lng] || {}, null, 2); // JSON 포맷으로 저장

        fs.writeFile(localeJsonFilePath, jsonString, 'utf8', (err) => {
            if (err) {
                throw err;
            }
            console.log(`File written successfully: ${localeJsonFilePath}`);
        });
    });
}

// 메인 업데이트 함수
async function updateJsonFromSheet() {
    const doc = await loadSpreadsheet();

    let lngsMap = lngs.reduce((acc, lng) => {
        acc[lng] = {}; // 언어별 빈 객체 초기화
        return acc;
    }, {});

    for (const sheet of doc.sheetsByIndex) {
        console.log(`Processing sheet: ${sheet.title}`);
        lngsMap = await makeTranslationsMapFromSheet(sheet, lngsMap);
    }

    saveTranslationsToFile(lngsMap);
}

// 함수 실행
updateJsonFromSheet();

// 모듈 내보내기
module.exports = {
    localesPath,
    lngs,
    loadSpreadsheet,
    updateJsonFromSheet,
    fileNm,
};

 

위 스크립트에 맞춰 아래와 같은 구조로 구글 시트를 작성한다.

 

페이지명 대상 원문 Key(N) ko
(한글번역)
en
(영문번역)
combinedKey
(key1 + 2 + 3)
Home 버튼 Login - 로그인 Login home.button.login
Home 메뉴 My Page - 마이페이지 My Page home.menu.mypage
Manager 타이틀 Main Manager - 메인관리자 Main Manager manager.title.main

페이지명, 대상, 원문, key1,2,3 : 사용자가 참고하기 위한 데이터

실질적으론 아래의 값이 번역에 사용된다.

ko : 한글번역 데이터

en : 영문번역 데이터

key : i18n 에서 참고하는 키 값 (위 스크립트의 combinedKey에 해당)

 

그럼 작성한 sync.js 스크립트를 실행하면 아래와 같이 현지화 파일이 생성 혹은 갱신될 것이다.

빌드 시에 현지화 데이터 업데이트 및 빌드를 같이 진행하려면 아래와 같이 스크립트를 작성하면 될 것이다.

package.json script 예시

"scripts": {
  "build": "npm run lang:sync && nuxt build",
  "lang:sync": "node ./locales/sync.js"
}

프론트엔드 현지화 예시

템플릿에서는 $t('key'), 스크립트에서는 t('key')로 사용한다.

언어 변경 시에는 반드시 setLocale 함수를 호출하여 변경하도록 한다.

 

템플릿 :

{{ $t(mobileTitle) }}

스크립트 : 

    $alert(t('client.common.passwordHasBeenModified'));

layout/manager.vue

<template>
    <div id="manager">
	    <span>{{ $t(mobileTitle) }}</span>
	  </div>
</template>
const { setLocale, t } = useI18n();
const mainStore = useMainStore();

const selectedLanguage = computed({
  get: () => mainStore.lang,
  set: (value) => {
    mainStore.setLang(value);
    setLocale(value);
    document.documentElement.lang = value;
  }
});

const mobileTitle = computed(() => {
  switch (route.path) {
    case '/manager/home':
      return 'manager.home.Home';
  }
});

const updatePassword = async () => {
  try {
    await useCustomFetch.put("/common/password", { body: { newPassword, target, email } });
    $alert(t('client.common.passwordHasBeenModified'));
  } catch (e) {
    $toastError(e.message || t('common.toastError.An error occurred while processing the request'));
  }
};

+ Recent posts