크롬 확장 다국어(i18n) 구현하기: 런타임 언어 전환까지
크롬 확장에서 다국어를 지원하려면 Chrome i18n API만으로는 부족합니다. 런타임 언어 전환, 선언적 번역 적용, 여러 페이지 동기화까지 지원하는 커스텀 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
- 런타임 언어 전환: 페이지 새로고침 없이 즉시 반영
- HTML 선언적 사용:
data-i18n속성으로 간편하게 - 자동 감지 + 수동 선택: 브라우저 언어 자동 감지, 사용자 선택 우선
- 여러 페이지 동기화: 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);
}
}
장점:
- HTML만 보고 어떤 텍스트가 번역되는지 파악 가능
- 번역 키 변경 시 HTML만 수정
- 기본값(영어)이 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-save→buttonSave) - [ ] 문장 중간 자르기 (문법 구조가 언어마다 다름)
번역 키 네이밍 규칙
// 좋은 예: 영역 + 요소 + 상태/용도
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. 참고 자료
- Chrome i18n API Documentation
- Chrome Storage API
- MDN: Internationalization (i18n)
- Unicode CLDR - 로케일 데이터 표준
12. 다음 단계
이 글에서는 크롬 확장의 다국어 지원을 런타임 언어 전환까지 구현했습니다. 이 시리즈의 다른 글들도 확인해보세요.
시리즈 목차:
- JavaScript URL 비교와 정규화
- Web Crypto API로 안전한 해싱 구현하기
- CSS 변수와 다크 모드 구현하기
- 크롬 확장 프로젝트 구조 정리하기
- 크롬 확장 다국어(i18n) 구현하기 ← 현재 글