크롬 확장 다국어(i18n) 구현하기: 런타임 언어 전환까지

크롬 확장에서 다국어를 지원하려면 Chrome i18n API만으로는 부족합니다. 런타임 언어 전환, 선언적 번역 적용, 여러 페이지 동기화까지 지원하는 커스텀 i18n 모듈을 구현했습니다.

크롬 확장 다국어(i18n) 구현하기: 런타임 언어 전환까지

크롬 확장 다국어(i18n) 구현하기: 런타임 언어 전환까지

Chrome i18n API의 한계를 넘어, 페이지 새로고침 없이 언어를 전환하는 커스텀 i18n 모듈 구현

작성일: 2026-01-23
프로젝트: 사이드 프로젝트 (크롬 확장 프로그램)
기술 스택: Chrome Extension Manifest V3, JavaScript, Chrome Storage API
키워드: 크롬 확장 다국어, chrome.i18n, i18n 구현, 런타임 언어 전환, 다국어 지원


1. 문제 상황

요구사항: 다국어 지원

크롬 확장 프로그램을 개발하면서 다국어 지원이 필요해졌습니다. 최소한 영어와 한국어를 지원해야 하고, 사용자가 언어를 선택할 수 있어야 합니다.

Chrome i18n API: 공식 솔루션

크롬 확장에는 공식 i18n API가 있습니다:

extension/
  _locales/
    en/
      messages.json
    ko/
      messages.json
  manifest.json
// _locales/en/messages.json
{
  "appName": {
    "message": "My Extension",
    "description": "Extension name"
  },
  "buttonSave": {
    "message": "Save",
    "description": "Save button text"
  }
}
// 사용법
chrome.i18n.getMessage('appName');  // "My Extension"

Chrome i18n API의 한계

공식 API를 테스트해보니 몇 가지 한계가 있었습니다:

1. 런타임 언어 전환 불가

// Chrome i18n은 브라우저 언어 설정을 따름
chrome.i18n.getUILanguage();  // "ko" (변경 불가)

// 사용자가 앱 내에서 영어로 바꾸고 싶어도
// 브라우저 전체 언어를 바꿔야 함

2. 페이지 새로고침 필요

// 언어 변경 후 반영하려면
location.reload();  // 전체 페이지 새로고침 필요

3. HTML에서 직접 사용 불가

<!-- 이런 문법 지원 안 함 -->
<button>__MSG_buttonSave__</button>

<!-- manifest.json과 CSS에서만 __MSG_key__ 사용 가능 -->
// HTML 텍스트는 JS에서 일일이 설정해야 함
document.getElementById('saveBtn').textContent =
  chrome.i18n.getMessage('buttonSave');

목표: 더 나은 i18n

  1. 런타임 언어 전환: 페이지 새로고침 없이 즉시 반영
  2. HTML 선언적 사용: data-i18n 속성으로 간편하게
  3. 자동 감지 + 수동 선택: 브라우저 언어 자동 감지, 사용자 선택 우선
  4. 여러 페이지 동기화: popup, options 등에서 언어 설정 공유

2. 아키텍처 설계

Chrome i18n API vs 커스텀 모듈

기능 Chrome i18n API 커스텀 모듈
런타임 언어 전환 ❌ 불가 ✅ 가능
HTML 선언적 사용 ❌ 제한적 ✅ data-i18n
브라우저 언어 감지 ✅ 자동 ✅ getUILanguage() 활용
manifest.json 번역 ✅ 지원 ❌ 불가
번역 파일 관리 별도 폴더 JS 내장 or 별도 파일
빌드 도구 필요

결론: 두 가지를 조합하여 사용

  • Chrome i18n API: manifest.json의 확장 이름/설명 번역
  • 커스텀 모듈: 앱 내 UI 텍스트 번역 (런타임 전환 지원)

전체 구조

extension/
  _locales/               # Chrome i18n (manifest용)
    en/messages.json
    ko/messages.json
  shared/
    i18n.js               # 커스텀 i18n 모듈
  popup/
    popup.html            # data-i18n 속성 사용
    popup.js              # i18n.init() 호출
  options/
    options.html
    options.js

3. 커스텀 i18n 모듈 구현

3.1 기본 구조

// shared/i18n.js
const i18n = (function () {
  'use strict';

  // 번역 메시지 저장소
  const messages = {
    en: {
      appName: 'My Extension',
      buttonSave: 'Save',
      buttonCancel: 'Cancel',
      settingsTitle: 'Settings',
      // ... 더 많은 메시지
    },
    ko: {
      appName: '내 확장 프로그램',
      buttonSave: '저장',
      buttonCancel: '취소',
      settingsTitle: '설정',
      // ... 더 많은 메시지
    },
  };

  let currentLang = 'en';  // 현재 언어

  // 번역 가져오기
  function t(key) {
    return messages[currentLang]?.[key]
        || messages.en[key]  // fallback to English
        || key;              // fallback to key itself
  }

  return { t };
})();

3.2 Fallback 체인

번역이 없을 때를 대비한 fallback 전략:

function t(key) {
  // 1. 현재 언어에서 찾기
  const current = messages[currentLang]?.[key];
  if (current) return current;

  // 2. 기본 언어(영어)에서 찾기
  const fallback = messages.en?.[key];
  if (fallback) return fallback;

  // 3. 키 자체를 반환 (개발 중 누락 발견용)
  console.warn(`[i18n] Missing translation: ${key}`);
  return key;
}

Fallback이 중요한 이유:

// 한국어 번역 누락 시
messages.ko.newFeature = undefined;
messages.en.newFeature = 'New Feature';

i18n.t('newFeature');  // "New Feature" (영어로 fallback)

// 둘 다 누락 시
i18n.t('typoKey');  // "typoKey" (키 자체 반환, 디버깅에 유용)

3.3 HTML 선언적 번역 (data-i18n)

JS에서 일일이 텍스트를 설정하는 대신, HTML에서 선언적으로 사용:

<!-- HTML -->
<h1 data-i18n="settingsTitle">Settings</h1>
<button data-i18n="buttonSave">Save</button>
<input data-i18n-placeholder="searchPlaceholder" placeholder="Search...">
<button data-i18n-title="tooltipHelp" title="Help">?</button>
// i18n.js - 번역 적용 함수
function applyTranslations() {
  // 텍스트 콘텐츠 번역
  document.querySelectorAll('[data-i18n]').forEach((el) => {
    const key = el.getAttribute('data-i18n');
    const text = t(key);
    if (text) {
      el.textContent = text;
    }
  });

  // placeholder 번역
  document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
    const key = el.getAttribute('data-i18n-placeholder');
    const text = t(key);
    if (text) {
      el.placeholder = text;
    }
  });

  // title(툴팁) 번역
  document.querySelectorAll('[data-i18n-title]').forEach((el) => {
    const key = el.getAttribute('data-i18n-title');
    const text = t(key);
    if (text) {
      el.title = text;
    }
  });

  // 페이지 타이틀 번역
  const titleEl = document.querySelector('title[data-i18n]');
  if (titleEl) {
    const key = titleEl.getAttribute('data-i18n');
    document.title = t(key);
  }
}

장점:

  1. HTML만 보고 어떤 텍스트가 번역되는지 파악 가능
  2. 번역 키 변경 시 HTML만 수정
  3. 기본값(영어)이 HTML에 있어서 번역 로드 전에도 표시됨

3.4 브라우저 언어 자동 감지

Chrome i18n API의 getUILanguage()를 활용:

function detectBrowserLanguage() {
  // Chrome 확장 환경
  if (typeof chrome !== 'undefined' && chrome.i18n) {
    const browserLang = chrome.i18n.getUILanguage();
    // "ko", "ko-KR", "en", "en-US" 등의 형태

    // 한국어 계열이면 'ko', 아니면 'en'
    return browserLang.startsWith('ko') ? 'ko' : 'en';
  }

  // 일반 웹 환경 (테스트용)
  return navigator.language.startsWith('ko') ? 'ko' : 'en';
}

3.5 사용자 설정 저장 및 로드

사용자가 선택한 언어를 chrome.storage에 저장:

const SETTINGS_KEY = 'settings';

// 언어 설정 가져오기
async function getLanguage() {
  try {
    const data = await chrome.storage.local.get(SETTINGS_KEY);
    const settings = data[SETTINGS_KEY] || {};
    const saved = settings.language;

    // 저장된 설정이 있고, 'auto'가 아니면 그대로 사용
    if (saved && saved !== 'auto') {
      return saved;
    }

    // 'auto'이거나 설정 없으면 브라우저 언어 감지
    return detectBrowserLanguage();
  } catch (e) {
    console.error('[i18n] Failed to get language:', e);
    return 'en';  // 에러 시 영어로 fallback
  }
}

// 언어 설정 저장
async function saveLanguage(lang) {
  const data = await chrome.storage.local.get(SETTINGS_KEY);
  const settings = data[SETTINGS_KEY] || {};
  settings.language = lang;
  await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
}

4. 런타임 언어 전환

4.1 즉시 반영

언어 변경 시 페이지 새로고침 없이 즉시 반영:

async function setLanguage(lang) {
  if (lang === 'auto') {
    currentLang = detectBrowserLanguage();
  } else {
    currentLang = lang;
  }

  // DOM에 번역 즉시 적용
  applyTranslations();
}

4.2 여러 페이지 동기화

popup에서 언어를 바꾸면 options 페이지에도 반영되어야 합니다:

// 초기화 시 storage 변경 리스너 등록
async function init() {
  // 저장된 언어 로드
  currentLang = await getLanguage();
  applyTranslations();

  // 다른 페이지에서 언어 변경 시 감지  ← 핵심
  chrome.storage.onChanged.addListener((changes, area) => {
    if (area === 'local' && changes[SETTINGS_KEY]) {
      const newSettings = changes[SETTINGS_KEY].newValue || {};
      if (newSettings.language) {
        setLanguage(newSettings.language);
      }
    }
  });

  return currentLang;
}

동작 흐름:

[Options 페이지]                    [Popup 페이지]
     |                                   |
  언어 변경 (ko → en)                    |
     |                                   |
  storage.set({language: 'en'})          |
     |                                   |
     +------- storage.onChanged -------->|
                                         |
                                    setLanguage('en')
                                         |
                                    applyTranslations()
                                         |
                                    UI 즉시 업데이트

4.3 언어 선택 UI

<!-- options.html -->
<select id="languageSelect">
  <option value="auto">Auto (Browser Default)</option>
  <option value="en">English</option>
  <option value="ko">한국어</option>
</select>
// options.js
const languageSelect = document.getElementById('languageSelect');

// 초기값 설정
async function initLanguageSelect() {
  const settings = await chrome.storage.local.get('settings');
  const lang = settings.settings?.language || 'auto';
  languageSelect.value = lang;
}

// 변경 이벤트
languageSelect.addEventListener('change', async () => {
  const lang = languageSelect.value;

  // 설정 저장
  const data = await chrome.storage.local.get('settings');
  const settings = data.settings || {};
  settings.language = lang;
  await chrome.storage.local.set({ settings });

  // 현재 페이지에 즉시 적용
  i18n.setLanguage(lang);
});

// 초기화
initLanguageSelect();

5. 동적 텍스트 처리

5.1 플레이스홀더 치환

번역 메시지에 동적 값을 삽입해야 할 때:

// 번역 메시지
messages.en.itemCount = '$1 items selected';
messages.ko.itemCount = '$1개 선택됨';

// 치환 함수
function t(key, ...args) {
  let msg = messages[currentLang]?.[key] || messages.en[key] || key;

  // $1, $2, ... 치환
  args.forEach((arg, i) => {
    msg = msg.replace(`$${i + 1}`, arg);
  });

  return msg;
}

// 사용
i18n.t('itemCount', 5);  // "5 items selected" 또는 "5개 선택됨"

5.2 복수형 처리 (간단한 방식)

// 영어는 단수/복수 구분 필요
messages.en.itemCount = '$1 item(s) selected';

// 또는 별도 키로 관리
messages.en.itemCountSingle = '1 item selected';
messages.en.itemCountMultiple = '$1 items selected';

function tPlural(keySingle, keyMultiple, count) {
  const key = count === 1 ? keySingle : keyMultiple;
  return t(key, count);
}

// 사용
tPlural('itemCountSingle', 'itemCountMultiple', 1);  // "1 item selected"
tPlural('itemCountSingle', 'itemCountMultiple', 5);  // "5 items selected"

5.3 JavaScript에서 동적 번역

data-i18n으로 처리할 수 없는 동적 콘텐츠:

// 토스트 메시지
function showToast(messageKey) {
  const text = i18n.t(messageKey);
  // 토스트 표시 로직...
}

showToast('toastSaved');  // "Saved" 또는 "저장됨"

// 에러 메시지
function showError(errorKey, details) {
  const text = i18n.t(errorKey, details);
  alert(text);
}

showError('errorRateLimit', 30);  // "Too many attempts. Try again in 30s."

6. Chrome i18n API 병행 사용

6.1 manifest.json 번역

manifest.json의 확장 이름과 설명은 Chrome i18n API로만 번역 가능:

// manifest.json
{
  "name": "__MSG_extName__",
  "description": "__MSG_extDescription__",
  "default_locale": "en",
  // ...
}
// _locales/en/messages.json
{
  "extName": {
    "message": "My Extension"
  },
  "extDescription": {
    "message": "A helpful browser extension"
  }
}

// _locales/ko/messages.json
{
  "extName": {
    "message": "내 확장 프로그램"
  },
  "extDescription": {
    "message": "유용한 브라우저 확장 프로그램"
  }
}

6.2 번역 파일 동기화 전략

Chrome i18n과 커스텀 모듈의 번역을 동기화하는 방법:

방법 1: 커스텀 모듈을 Single Source of Truth로

// shared/i18n.js 내 messages 객체가 원본
// _locales/*.json은 manifest용으로만 최소한 유지

방법 2: _locales를 Single Source of Truth로

// _locales/en/messages.json을 빌드 시 i18n.js로 변환
// 단점: 빌드 스텝 필요

방법 3: 독립적으로 관리 (권장)

_locales/
  en/messages.json    # manifest용 (extName, extDescription만)
  ko/messages.json

shared/
  i18n.js             # 앱 UI용 (나머지 모든 메시지)

manifest용 번역은 거의 변경되지 않으므로 독립 관리가 실용적입니다.


7. 완성된 i18n 모듈

전체 코드

// shared/i18n.js
const i18n = (function () {
  'use strict';

  // ============================================
  // 번역 메시지
  // ============================================
  const messages = {
    en: {
      // 공통
      appName: 'My Extension',
      buttonSave: 'Save',
      buttonCancel: 'Cancel',
      buttonDelete: 'Delete',
      buttonConfirm: 'Confirm',

      // 설정
      settingsTitle: 'Settings',
      settingsLanguage: 'Language',
      settingsLanguageAuto: 'Auto (Browser Default)',

      // 메시지
      toastSaved: 'Saved successfully',
      toastDeleted: 'Deleted',
      toastError: 'An error occurred',
      errorRateLimit: 'Too many attempts. Try again in $1s.',
      itemCount: '$1 items selected',

      // ... 더 많은 메시지
    },
    ko: {
      // 공통
      appName: '내 확장 프로그램',
      buttonSave: '저장',
      buttonCancel: '취소',
      buttonDelete: '삭제',
      buttonConfirm: '확인',

      // 설정
      settingsTitle: '설정',
      settingsLanguage: '언어',
      settingsLanguageAuto: '자동 (브라우저 기본값)',

      // 메시지
      toastSaved: '저장되었습니다',
      toastDeleted: '삭제되었습니다',
      toastError: '오류가 발생했습니다',
      errorRateLimit: '시도 횟수 초과. $1초 후 다시 시도하세요.',
      itemCount: '$1개 선택됨',

      // ... 더 많은 메시지
    },
  };

  const SETTINGS_KEY = 'settings';
  let currentLang = 'en';

  // ============================================
  // 언어 감지 및 설정
  // ============================================

  function detectBrowserLanguage() {
    if (typeof chrome !== 'undefined' && chrome.i18n) {
      const browserLang = chrome.i18n.getUILanguage();
      return browserLang.startsWith('ko') ? 'ko' : 'en';
    }
    return navigator.language.startsWith('ko') ? 'ko' : 'en';
  }

  async function getLanguage() {
    try {
      const data = await chrome.storage.local.get(SETTINGS_KEY);
      const settings = data[SETTINGS_KEY] || {};
      const saved = settings.language;

      if (saved && saved !== 'auto') {
        return saved;
      }
      return detectBrowserLanguage();
    } catch (e) {
      console.error('[i18n] Failed to get language:', e);
      return 'en';
    }
  }

  // ============================================
  // 번역 함수
  // ============================================

  function t(key, ...args) {
    let msg = messages[currentLang]?.[key]
           || messages.en[key]
           || key;

    // 플레이스홀더 치환 ($1, $2, ...)
    args.forEach((arg, i) => {
      msg = msg.replace(`$${i + 1}`, arg);
    });

    return msg;
  }

  // ============================================
  // DOM 번역 적용
  // ============================================

  function applyTranslations() {
    // 텍스트 콘텐츠
    document.querySelectorAll('[data-i18n]').forEach((el) => {
      const key = el.getAttribute('data-i18n');
      el.textContent = t(key);
    });

    // placeholder
    document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
      const key = el.getAttribute('data-i18n-placeholder');
      el.placeholder = t(key);
    });

    // title (툴팁)
    document.querySelectorAll('[data-i18n-title]').forEach((el) => {
      const key = el.getAttribute('data-i18n-title');
      el.title = t(key);
    });

    // 페이지 타이틀
    const titleEl = document.querySelector('title[data-i18n]');
    if (titleEl) {
      document.title = t(titleEl.getAttribute('data-i18n'));
    }
  }

  // ============================================
  // 언어 전환
  // ============================================

  async function setLanguage(lang) {
    if (lang === 'auto') {
      currentLang = detectBrowserLanguage();
    } else {
      currentLang = lang;
    }
    applyTranslations();
  }

  // ============================================
  // 초기화
  // ============================================

  async function init() {
    currentLang = await getLanguage();
    applyTranslations();

    // 다른 페이지에서 언어 변경 시 동기화
    chrome.storage.onChanged.addListener((changes, area) => {
      if (area === 'local' && changes[SETTINGS_KEY]) {
        const newSettings = changes[SETTINGS_KEY].newValue || {};
        if (newSettings.language) {
          setLanguage(newSettings.language);
        }
      }
    });

    return currentLang;
  }

  // ============================================
  // 유틸리티
  // ============================================

  function getCurrentLang() {
    return currentLang;
  }

  function getAvailableLanguages() {
    return [
      { code: 'auto', name: 'Auto (Browser Default)' },
      { code: 'en', name: 'English' },
      { code: 'ko', name: '한국어' },
    ];
  }

  // ============================================
  // Public API
  // ============================================

  return {
    init,
    t,
    setLanguage,
    getLanguage,
    getCurrentLang,
    getAvailableLanguages,
    applyTranslations,
  };
})();

// 전역 노출
if (typeof window !== 'undefined') {
  window.i18n = i18n;
}

사용 예시

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
  <title data-i18n="appName">My Extension</title>
  <script src="../shared/i18n.js"></script>
</head>
<body>
  <h1 data-i18n="settingsTitle">Settings</h1>

  <input
    type="text"
    data-i18n-placeholder="searchPlaceholder"
    placeholder="Search..."
  >

  <button data-i18n="buttonSave">Save</button>
  <button data-i18n="buttonCancel">Cancel</button>

  <script src="popup.js"></script>
</body>
</html>
// popup.js
document.addEventListener('DOMContentLoaded', async () => {
  // i18n 초기화 (번역 적용 + 리스너 등록)
  await i18n.init();

  // 이제 동적 번역도 사용 가능
  showToast(i18n.t('toastSaved'));
});

8. 베스트 프랙티스

DO (해야 할 것)

  • [x] 모든 UI 텍스트를 번역 키로 관리
  • [x] HTML에 data-i18n 속성으로 선언적 번역
  • [x] 영어를 기본(fallback) 언어로 설정
  • [x] chrome.storage.onChanged로 페이지 간 동기화
  • [x] 플레이스홀더($1, $2)로 동적 값 처리
  • [x] 번역 누락 시 콘솔 경고 출력 (개발 중 발견용)

DON'T (하지 말아야 할 것)

  • [ ] 하드코딩된 텍스트 방치
  • [ ] 언어 전환 시 location.reload() 사용
  • [ ] 번역 파일을 외부 서버에서 로드 (확장 번들에 포함해야 함)
  • [ ] 번역 키에 특수문자 사용 (button-savebuttonSave)
  • [ ] 문장 중간 자르기 (문법 구조가 언어마다 다름)

번역 키 네이밍 규칙

// 좋은 예: 영역 + 요소 + 상태/용도
const goodKeys = {
  buttonSave: 'Save',
  buttonCancel: 'Cancel',
  settingsTitle: 'Settings',
  settingsLanguage: 'Language',
  toastSaved: 'Saved successfully',
  errorRateLimit: 'Too many attempts',
  modalConfirmTitle: 'Confirm',
  modalConfirmText: 'Are you sure?',
};

// 나쁜 예
const badKeys = {
  btn1: 'Save',           // 의미 없는 이름
  'button-save': 'Save',  // 특수문자 사용
  SAVE: 'Save',           // 일관성 없는 케이스
  settingsPageMainTitle: 'Settings',  // 너무 긴 이름
};

번역 품질 체크리스트

  • [ ] 모든 키가 양쪽 언어에 존재하는가?
  • [ ] 플레이스홀더 개수가 일치하는가? ($1, $2)
  • [ ] 맥락에 맞는 번역인가? (같은 단어도 맥락에 따라 다름)
  • [ ] 길이가 비슷한가? (UI 레이아웃 깨짐 방지)
  • [ ] 특수문자, 이모지가 올바르게 표시되는가?

9. 트러블슈팅

문제 1: 번역이 적용되지 않음

증상: data-i18n 속성을 추가했는데 텍스트가 바뀌지 않음

원인 및 해결:

// 원인 1: init()을 호출하지 않음
document.addEventListener('DOMContentLoaded', async () => {
  await i18n.init();  // ← 반드시 호출
});

// 원인 2: 스크립트 로드 순서
// i18n.js가 먼저 로드되어야 함
<!-- 올바른 순서 -->
<script src="../shared/i18n.js"></script>
<script src="popup.js"></script>

문제 2: 언어 변경이 다른 페이지에 반영 안 됨

증상: Options에서 언어 바꿨는데 Popup에는 안 바뀜

원인 및 해결:

// storage.onChanged 리스너가 등록되어 있는지 확인
async function init() {
  // ...

  // 이 리스너가 있어야 함
  chrome.storage.onChanged.addListener((changes, area) => {
    if (area === 'local' && changes[SETTINGS_KEY]) {
      const newSettings = changes[SETTINGS_KEY].newValue || {};
      if (newSettings.language) {
        setLanguage(newSettings.language);
      }
    }
  });
}

문제 3: 동적으로 추가된 요소가 번역 안 됨

증상: JavaScript로 추가한 요소에 data-i18n이 있는데 번역 안 됨

해결:

// 요소 추가 후 수동으로 번역 적용
function addNewItem() {
  const item = document.createElement('div');
  const span = document.createElement('span');
  span.setAttribute('data-i18n', 'itemLabel');
  span.textContent = 'Label';  // 기본값
  item.appendChild(span);
  container.appendChild(item);

  // 새 요소에 번역 적용  ← 핵심
  i18n.applyTranslations();
}

// 또는 개별 요소만 번역
function addNewItem() {
  const item = document.createElement('div');
  const span = document.createElement('span');
  span.textContent = i18n.t('itemLabel');  // 직접 번역
  item.appendChild(span);
  container.appendChild(item);
}

문제 4: 플레이스홀더가 치환되지 않음

증상: $1 items selected가 그대로 표시됨

원인 및 해결:

// 잘못된 사용
element.textContent = i18n.t('itemCount');  // "$1 items selected"

// 올바른 사용
element.textContent = i18n.t('itemCount', 5);  // "5 items selected"

10. FAQ

Q: Chrome i18n API 대신 커스텀 모듈을 쓰는 이유는?

A: Chrome i18n API는 런타임 언어 전환을 지원하지 않습니다. 사용자가 앱 내에서 언어를 바꾸려면 페이지 새로고침이 필요하고, 그마저도 브라우저 전체 언어 설정을 따르기 때문에 완전한 제어가 불가능합니다. 커스텀 모듈은 즉시 전환과 사용자 선호 저장을 지원합니다.

Q: 번역 메시지를 별도 JSON 파일로 분리해도 되나요?

A: 가능하지만, 크롬 확장에서는 fetch()로 JSON을 로드하는 것보다 JavaScript 객체로 번들링하는 것이 더 안정적입니다. 외부 파일 로드 시 타이밍 이슈, 캐싱 문제, CSP(Content Security Policy) 제약 등이 발생할 수 있습니다.

Q: 새 언어를 추가하려면?

A: messages 객체에 새 언어 키를 추가하고, getAvailableLanguages()에 옵션을 추가하면 됩니다:

const messages = {
  en: { /* ... */ },
  ko: { /* ... */ },
  ja: {  // 일본어 추가
    appName: '私の拡張機能',
    buttonSave: '保存',
    // ...
  },
};

function getAvailableLanguages() {
  return [
    { code: 'auto', name: 'Auto' },
    { code: 'en', name: 'English' },
    { code: 'ko', name: '한국어' },
    { code: 'ja', name: '日本語' },  // 추가
  ];
}

Q: 번역 품질 관리는 어떻게 하나요?

A: 간단한 스크립트로 검증할 수 있습니다:

// 번역 키 검증 스크립트
function validateTranslations() {
  const langs = Object.keys(messages);
  const baseKeys = Object.keys(messages.en);

  langs.forEach(lang => {
    baseKeys.forEach(key => {
      if (!messages[lang][key]) {
        console.warn(`Missing: ${lang}.${key}`);
      }
    });
  });
}

Q: SSR/Prerendering 환경에서는?

A: 크롬 확장은 클라이언트 사이드만 존재하므로 SSR 고려가 필요 없습니다. 일반 웹앱에서 이 패턴을 사용한다면, 초기 HTML에 기본 언어 텍스트를 넣어두고 클라이언트에서 번역을 적용하면 됩니다.


11. 참고 자료


📚 크롬 확장 개발 시리즈 (9부작)

  1. JavaScript URL 비교와 정규화
  2. Web Crypto API로 안전한 해싱 구현하기
  3. CSS 변수와 다크 모드 구현하기
  4. 크롬 확장 프로젝트 구조 정리하기
  5. 크롬 확장 공유 모듈 설계
  6. Chrome Storage로 실시간 상태 동기화
  7. 크롬 확장 다국어(i18n) 구현하기 (현재 글)
  8. Chrome Alarms API로 자동 잠금 타이머
  9. 크롬 확장 보안 강화: CSP와 최소 권한