/** * i18n (국제화) 시스템 * 지원 언어: ko(한국어), en(영어), zh(중국어), ja(일본어) */ (function () { 'use strict'; const SUPPORTED_LANGS = ['ko', 'en', 'zh', 'ja']; const DEFAULT_LANG = 'ko'; const STORAGE_KEY = 'mingle-lang'; const i18n = { currentLang: DEFAULT_LANG, translations: {}, cache: {}, ready: false, /** * 초기화: 언어 감지 → JSON 로드 */ init() { // URL 파싱을 통해 현재 경로 언어 감지 const path = window.location.pathname; const parts = path.split('/').filter(Boolean); const pathLang = (parts.length > 0 && SUPPORTED_LANGS.includes(parts[0]) && parts[0] !== DEFAULT_LANG) ? parts[0] : DEFAULT_LANG; // 1. URL이 가장 높은 우선순위를 가짐 let lang = pathLang; if (pathLang === DEFAULT_LANG) { // 2. 루트 경로(/)에 왔을 때는 기존처럼 브라우저/로컬 환경 감지 lang = this.detectLanguage(); } else { // URL에 명시되어 있다면 해당 언어 환경 저장 localStorage.setItem(STORAGE_KEY, lang); } this.currentLang = lang; document.documentElement.lang = lang; // 만약 서버에서 미리 번역된 HTML(SSG)이 아니라 클라이언트 사이드에서 변경이 필요한 경우 if (lang !== DEFAULT_LANG && pathLang === DEFAULT_LANG) { return this.loadLang(lang).then(() => { this.ready = true; }); } else { this.ready = true; return Promise.resolve(); } }, /** * 언어 감지 우선순위: localStorage → 브라우저 언어 → 기본값(ko) */ detectLanguage() { // 1. localStorage에 저장된 언어 const saved = localStorage.getItem(STORAGE_KEY); if (saved && SUPPORTED_LANGS.includes(saved)) { return saved; } // 2. 브라우저 언어 const browserLang = (navigator.language || navigator.userLanguage || '').toLowerCase(); const langCode = browserLang.split('-')[0]; if (SUPPORTED_LANGS.includes(langCode) && langCode !== DEFAULT_LANG) { return langCode; } // 3. 기본값 return DEFAULT_LANG; }, /** * JSON 번역 파일 로드 */ async loadLang(lang) { if (lang === DEFAULT_LANG) { this.translations = {}; return; } // 캐시 확인 if (this.cache[lang]) { this.translations = this.cache[lang]; return; } try { const response = await fetch(`/i18n/${lang}.json`); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); this.cache[lang] = data; this.translations = data; } catch (error) { console.warn(`[i18n] Failed to load ${lang}.json:`, error); this.translations = {}; } }, /** * 번역 키로 텍스트 가져오기 * @param {string} key - 점(.) 구분 키 (예: "header.studioName") * @param {string} fallback - 폴백 텍스트 * @returns {string} */ t(key, fallback) { if (this.currentLang === DEFAULT_LANG) { return fallback || key; } const keys = key.split('.'); let value = this.translations; for (const k of keys) { if (value && typeof value === 'object' && k in value) { value = value[k]; } else { return fallback || key; } } return typeof value === 'string' ? value : (fallback || key); }, /** * DOM 전체 번역 적용 * @param {Element} root - 번역 대상 루트 (기본: document) */ translateDOM(root) { const container = root || document; const isKorean = this.currentLang === DEFAULT_LANG; // data-i18n: 텍스트 콘텐츠 번역 container.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); // 원본 한국어 텍스트 저장 (최초 1회) if (!el.hasAttribute('data-i18n-ko')) { el.setAttribute('data-i18n-ko', el.innerHTML); } if (isKorean) { el.innerHTML = el.getAttribute('data-i18n-ko'); } else { const translated = this.t(key, null); if (translated && translated !== key) { if (translated.includes('<')) { el.innerHTML = translated; } else { el.textContent = translated; } } } }); // data-i18n-html: HTML 콘텐츠 번역 (명시적) container.querySelectorAll('[data-i18n-html]').forEach(el => { const key = el.getAttribute('data-i18n-html'); if (!el.hasAttribute('data-i18n-ko')) { el.setAttribute('data-i18n-ko', el.innerHTML); } if (isKorean) { el.innerHTML = el.getAttribute('data-i18n-ko'); } else { const translated = this.t(key, null); if (translated && translated !== key) { el.innerHTML = translated; } } }); // data-i18n-placeholder: placeholder 번역 container.querySelectorAll('[data-i18n-placeholder]').forEach(el => { const key = el.getAttribute('data-i18n-placeholder'); if (!el.hasAttribute('data-i18n-ko-placeholder')) { el.setAttribute('data-i18n-ko-placeholder', el.placeholder); } if (isKorean) { el.placeholder = el.getAttribute('data-i18n-ko-placeholder'); } else { const translated = this.t(key, null); if (translated && translated !== key) { el.placeholder = translated; } } }); // data-i18n-aria: aria-label 번역 container.querySelectorAll('[data-i18n-aria]').forEach(el => { const key = el.getAttribute('data-i18n-aria'); if (!el.hasAttribute('data-i18n-ko-aria')) { el.setAttribute('data-i18n-ko-aria', el.getAttribute('aria-label') || ''); } if (isKorean) { el.setAttribute('aria-label', el.getAttribute('data-i18n-ko-aria')); } else { const translated = this.t(key, null); if (translated && translated !== key) { el.setAttribute('aria-label', translated); } } }); // data-i18n-title: title 속성 번역 container.querySelectorAll('[data-i18n-title]').forEach(el => { const key = el.getAttribute('data-i18n-title'); if (!el.hasAttribute('data-i18n-ko-title')) { el.setAttribute('data-i18n-ko-title', el.title || ''); } if (isKorean) { el.title = el.getAttribute('data-i18n-ko-title'); } else { const translated = this.t(key, null); if (translated && translated !== key) { el.title = translated; } } }); // 메타 태그 번역 this.translateMeta(); // 언어 스위처 활성 상태 업데이트 this.updateSwitcher(); }, /** * 메타 태그 번역 (title, description, OG 등) */ translateMeta() { const isKorean = this.currentLang === DEFAULT_LANG; const pageName = this.getPageName(); // 원본 메타 저장 (최초 1회) if (!this._metaOriginals) { this._metaOriginals = { title: document.title, description: document.querySelector('meta[name="description"]')?.content || '', ogTitle: document.querySelector('meta[property="og:title"]')?.content || '', ogDescription: document.querySelector('meta[property="og:description"]')?.content || '' }; } if (isKorean) { document.title = this._metaOriginals.title; const metaDesc = document.querySelector('meta[name="description"]'); if (metaDesc) metaDesc.content = this._metaOriginals.description; const ogTitleEl = document.querySelector('meta[property="og:title"]'); if (ogTitleEl) ogTitleEl.content = this._metaOriginals.ogTitle; const ogDescEl = document.querySelector('meta[property="og:description"]'); if (ogDescEl) ogDescEl.content = this._metaOriginals.ogDescription; return; } // title const titleKey = `${pageName}.meta.title`; const title = this.t(titleKey, null); if (title && title !== titleKey) { document.title = title; } // meta description const descKey = `${pageName}.meta.description`; const desc = this.t(descKey, null); if (desc && desc !== descKey) { const metaDesc = document.querySelector('meta[name="description"]'); if (metaDesc) metaDesc.content = desc; } // OG tags const ogTitleKey = `${pageName}.meta.ogTitle`; const ogTitle = this.t(ogTitleKey, null); if (ogTitle && ogTitle !== ogTitleKey) { const ogTitleEl = document.querySelector('meta[property="og:title"]'); if (ogTitleEl) ogTitleEl.content = ogTitle; } const ogDescKey = `${pageName}.meta.ogDescription`; const ogDesc = this.t(ogDescKey, null); if (ogDesc && ogDesc !== ogDescKey) { const ogDescEl = document.querySelector('meta[property="og:description"]'); if (ogDescEl) ogDescEl.content = ogDesc; } }, /** * 현재 페이지 이름 추출 */ getPageName() { const path = window.location.pathname; const page = path.split('/').pop().replace('.html', '') || 'index'; return page; }, /** * 언어 전환 */ setLang(lang) { if (!SUPPORTED_LANGS.includes(lang)) return; if (lang === this.currentLang) return; localStorage.setItem(STORAGE_KEY, lang); // SEO 친화적인 URL 리다이렉션 수행 const currentPath = window.location.pathname; const parts = currentPath.split('/').filter(Boolean); // 만약 현재 경로의 첫 번째 파트가 지원하는 언어라면(ko 제외), 제거 if (parts.length > 0 && SUPPORTED_LANGS.includes(parts[0]) && parts[0] !== DEFAULT_LANG) { parts.shift(); } const baseRoute = '/' + parts.join('/'); const newPath = lang === DEFAULT_LANG ? baseRoute : `/${lang}${baseRoute === '/' ? '' : baseRoute}`; // URL 변경 window.location.href = newPath || '/'; }, /** * 언어 스위처 UI 활성 상태 업데이트 */ updateSwitcher() { const currentEl = document.querySelector('.lang-current'); if (currentEl) { currentEl.textContent = this.currentLang.toUpperCase(); } document.querySelectorAll('.lang-dropdown button[data-lang]').forEach(btn => { btn.classList.toggle('active', btn.getAttribute('data-lang') === this.currentLang); }); }, /** * 언어 스위처 이벤트 바인딩 */ initSwitcher() { const switcher = document.querySelector('.lang-switcher'); if (!switcher) return; const btn = switcher.querySelector('.lang-btn'); const dropdown = switcher.querySelector('.lang-dropdown'); if (btn && dropdown) { btn.addEventListener('click', (e) => { e.stopPropagation(); dropdown.classList.toggle('open'); btn.classList.toggle('open'); }); dropdown.querySelectorAll('button[data-lang]').forEach(langBtn => { langBtn.addEventListener('click', (e) => { e.stopPropagation(); const lang = langBtn.getAttribute('data-lang'); this.setLang(lang); dropdown.classList.remove('open'); btn.classList.remove('open'); }); }); // 외부 클릭 시 닫기 document.addEventListener('click', () => { dropdown.classList.remove('open'); btn.classList.remove('open'); }); } this.updateSwitcher(); } }; window.i18n = i18n; })();