/** * AlaskahQR - Internationalization (i18n) * * Refactored to dynamically load language modules. */ const LANG_NAMES = { ko: '한국어', en: 'English', ja: '日本語', zh: '中文', es: 'Español', fr: 'Français', pt: 'Português', de: 'Deutsch', vi: 'Tiếng Việt', th: 'ไทย' }; const SUPPORTED = ['ko', 'en', 'ja', 'zh', 'es', 'fr', 'pt', 'de', 'vi', 'th']; const CONTACT_EMAIL = 'alaskahpdf@gmail.com'; let currentLang = 'ko'; let translations = {}; // Cache for loaded language modules /** * Gets a translation for a given key. * @param {string} key The translation key. * @returns {string} The translated string or the key itself if not found. */ function t(key) { return ( translations[currentLang]?.[key] || translations.en?.[key] || translations.ko?.[key] || key ); } /** * Sets the application language, loading the language module if necessary. * @param {string} lang The language code (e.g., 'en', 'ko'). */ async function setLang(lang) { if (!SUPPORTED.includes(lang)) { lang = 'ko'; } currentLang = lang; localStorage.setItem('alaskahqr_lang', lang); document.documentElement.lang = lang; // Load translation module if not already loaded if (!translations[lang]) { try { const module = await import(`./${lang}.js`); translations[lang] = module[lang]; } catch (error) { console.error(`Could not load language file for ${lang}:`, error); // Fallback to Korean if the selected language fails to load if (lang !== 'ko' && !translations['ko']) { const fallbackModule = await import('./ko.js'); translations['ko'] = fallbackModule.ko; } currentLang = 'ko'; document.documentElement.lang = 'ko'; } } applyAll(); updateLangSelector(); } /** * Determines the initial language from localStorage or browser settings. * @returns {string} The detected language code. */ function getLang() { const saved = localStorage.getItem('alaskahqr_lang'); if (saved && SUPPORTED.includes(saved)) { return saved; } const browser = (navigator.language || 'ko').slice(0, 2); return SUPPORTED.includes(browser) ? browser : 'ko'; } /* ══════════════════════════════════════════ TRANSLATION APPLICATION FUNCTIONS ══════════════════════════════════════════ */ function applyAll() { applyDataI18n(); applyNavLinks(); applyFooter(); applyPageHero(); applyPageSpecific(); } function applyDataI18n() { document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.dataset.i18n; const val = t(key); if (el.dataset.i18nHtml !== undefined || key.includes('hero_title')) { el.innerHTML = val; } else { el.textContent = val; } }); document.querySelectorAll('[data-i18n-html]').forEach(el => { el.innerHTML = t(el.dataset.i18nHtml); }); document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { el.placeholder = t(el.dataset.i18nPlaceholder); }); document.querySelectorAll('[data-i18n-content]').forEach(el => { el.content = t(el.dataset.i18nContent); }); } function applyNavLinks() { document.querySelectorAll('.nav a, .mobile-nav a').forEach(el => { const href = el.getAttribute('href') || ''; const normalized = href.replace(/\/+$/, ''); const base = normalized.split('/').pop().split('?')[0]; let key; if (base === 'index.html' || base === '' || normalized === '') { key = 'nav.home'; } else if (base.includes('qr-generator')) { key = 'nav.qr'; } else if (base.includes('barcode-generator')) { key = 'nav.barcode'; } else if (base === 'blog.html' || normalized.endsWith('/blog')) { key = 'nav.blog'; } else if (base.includes('faq')) { key = 'nav.faq'; } if (key) { el.textContent = t(key); } }); } function applyFooter() { const contactString = t('footer.contact'); const finalContact = contactString.includes(CONTACT_EMAIL) ? contactString : `${contactString} ${CONTACT_EMAIL}`; // Ensure a footer contact item exists even if legacy markup has no footer.contact node. let contactEl = document.querySelector('[data-i18n="footer.contact"]'); if (!contactEl) { const supportCol = document.querySelector('.footer-col h4[data-i18n="footer.support"]')?.closest('.footer-col'); if (supportCol) { let list = supportCol.querySelector('ul'); if (!list) { list = document.createElement('ul'); supportCol.appendChild(list); } const li = document.createElement('li'); const a = document.createElement('a'); a.setAttribute('data-i18n', 'footer.contact'); a.href = `mailto:${CONTACT_EMAIL}`; a.style.color = 'inherit'; a.style.textDecoration = 'none'; a.textContent = finalContact; li.appendChild(a); list.appendChild(li); contactEl = a; } } document.querySelectorAll('[data-i18n^="footer."]').forEach(el => { const key = el.dataset.i18n; if (key === 'footer.contact') { if (el.tagName === 'A') { el.href = `mailto:${CONTACT_EMAIL}`; el.textContent = finalContact; } else { el.innerHTML = `${finalContact}`; } } else { el.textContent = t(key); } }); const copyrightEl = document.querySelector('.footer-bottom > span'); if (copyrightEl) { copyrightEl.textContent = t('footer.copyright'); } } function applyPageHero() { const hero = document.querySelector('.page-hero'); if (!hero) return; const h1 = hero.querySelector('h1'); const p = hero.querySelector('p'); const path = location.pathname; let titleKey, subKey; if (path.includes('qr-generator')) { titleKey = 'qr.page_title'; subKey = 'qr.page_sub'; } else if (path.includes('barcode-generator')) { titleKey = 'bc.page_title'; subKey = 'bc.page_sub'; } else if (path.includes('faq')) { titleKey = 'faq.title'; subKey = 'faq.sub'; } else if (path.includes('blog')) { titleKey = 'blog.title'; subKey = 'blog.sub'; } if (h1 && titleKey) h1.innerHTML = t(titleKey); if (p && subKey) p.innerHTML = t(subKey); } function applyPageSpecific() { const path = location.pathname; if (path === '/' || path.endsWith('/index.html')) { // Index page specific translations } else if (path.includes('/faq')) { applyFaqTranslations(); } else if (path === '/blog' || path === '/blog/') { applyBlogTranslations(); } else if (path.startsWith('/blog/')) { applyBlogPostTranslations(); } else if (path.includes('/support')) { document.title = t('support.page_title'); } } function applyFaqTranslations() { document.querySelectorAll('.faq-item[data-faq-id]').forEach(item => { const id = item.dataset.faqId; const q = item.querySelector('.faq-q'); const a = item.querySelector('.faq-a-inner'); if (q) q.childNodes[0].textContent = t(`faq.q${id}`); if (a) a.innerHTML = t(`faq.a${id}`); }); } function applyBlogTranslations() { document.querySelectorAll('.blog-categories [data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); }); document.querySelectorAll('.blog-card[data-post-id]').forEach(card => { const id = card.dataset.postId; const cat = card.querySelector('.blog-cat'); const title = card.querySelector('h3'); const desc = card.querySelector('p'); const readMore = card.querySelector('.blog-read-more'); if (cat) cat.textContent = t(`blog.post${id}.cat`); if (title) title.textContent = t(`blog.post${id}.title`); if (desc) desc.textContent = t(`blog.post${id}.desc`); if (readMore) readMore.textContent = t('btn.read_more'); }); } function applyBlogPostTranslations() { const slug = location.pathname .replace(/\/+$/, '') .split('/') .pop(); const postConfigs = { 'qr-code-guide': { id: 1, pageTitle: 'blog.post1.page_title', pageDesc: 'blog.post1.page_desc', pageKeywords: 'blog.post1.page_keywords', date: 'blog.post1.date', crumb: 'blog.post1.crumb', bodyHtml: 'blog.post1.body_html', toc: ['blog.post1.toc1', 'blog.post1.toc2', 'blog.post1.toc3', 'blog.post1.toc4', 'blog.post1.toc5', 'blog.post1.toc6', 'blog.post1.toc7', 'blog.post1.toc8'], ctaTitle: 'blog.post1.cta_title', ctaSub: 'blog.post1.cta_sub', ctaBtn: 'faq.cta_qr', related: [ { icon: '☰', key: 'blog.post2.title' }, { icon: '❓', key: 'faq.title' }, { icon: '📊', key: 'blog.post3.title' } ] }, 'barcode-guide': { id: 2, pageTitle: 'blog.post2.page_title', pageDesc: 'blog.post2.page_desc', pageKeywords: 'blog.post2.page_keywords', date: 'blog.post2.date', crumb: 'blog.post2.crumb', bodyHtml: 'blog.post2.body_html', toc: ['blog.post2.toc1', 'blog.post2.toc2', 'blog.post2.toc3', 'blog.post2.toc4', 'blog.post2.toc5', 'blog.post2.toc6', 'blog.post2.toc7'], ctaTitle: 'blog.post2.cta_title', ctaSub: 'blog.post2.cta_sub', ctaBtn: 'faq.cta_bc', related: [ { icon: '⬛', key: 'blog.post1.title' }, { icon: '❓', key: 'faq.title' } ] }, 'qr-marketing-guide': { id: 3, pageTitle: 'blog.post3.page_title', pageDesc: 'blog.post3.page_desc', pageKeywords: 'blog.post3.page_keywords', date: 'blog.post3.date', crumb: 'blog.post3.crumb', bodyHtml: 'blog.post3.body_html', toc: ['blog.post3.toc1', 'blog.post3.toc2', 'blog.post3.toc3', 'blog.post3.toc4', 'blog.post3.toc5', 'blog.post3.toc6'], ctaTitle: 'blog.post3.cta_title', ctaSub: 'blog.post3.cta_sub', ctaBtn: 'faq.cta_qr', related: [ { icon: '⬛', key: 'blog.post1.title' }, { icon: '☰', key: 'blog.post2.title' }, { icon: '❓', key: 'faq.title' } ] }, 'wifi-qr-guide': { id: 4, pageTitle: 'blog.post4.page_title', pageDesc: 'blog.post4.page_desc', pageKeywords: 'blog.post4.page_keywords', date: 'blog.post4.date', crumb: 'blog.post4.crumb', bodyHtml: 'blog.post4.body_html', toc: ['blog.post4.toc1', 'blog.post4.toc2', 'blog.post4.toc3', 'blog.post4.toc4', 'blog.post4.toc5'], ctaTitle: 'blog.post4.cta_title', ctaSub: 'blog.post4.cta_sub', ctaBtn: 'faq.cta_qr', related: [ { icon: '⬛', key: 'blog.post1.title' }, { icon: '📊', key: 'blog.post3.title' }, { icon: '❓', key: 'faq.title' } ] }, 'smartstore-barcode-guide': { id: 5, pageTitle: 'blog.post5.page_title', pageDesc: 'blog.post5.page_desc', pageKeywords: 'blog.post5.page_keywords', date: 'blog.post5.date', crumb: 'blog.post5.crumb', bodyHtml: 'blog.post5.body_html', toc: ['blog.post5.toc1', 'blog.post5.toc2', 'blog.post5.toc3', 'blog.post5.toc4', 'blog.post5.toc5'], ctaTitle: 'blog.post5.cta_title', ctaSub: 'blog.post5.cta_sub', ctaBtn: 'faq.cta_bc', related: [ { icon: '☰', key: 'blog.post2.title' }, { icon: '⬛', key: 'blog.post1.title' }, { icon: '❓', key: 'faq.title' } ] }, 'custom-qr-design': { id: 6, pageTitle: 'blog.post6.page_title', pageDesc: 'blog.post6.page_desc', pageKeywords: 'blog.post6.page_keywords', date: 'blog.post6.date', crumb: 'blog.post6.crumb', bodyHtml: 'blog.post6.body_html', toc: ['blog.post6.toc1', 'blog.post6.toc2', 'blog.post6.toc3', 'blog.post6.toc4', 'blog.post6.toc5', 'blog.post6.toc6'], ctaTitle: 'blog.post6.cta_title', ctaSub: 'blog.post6.cta_sub', ctaBtn: 'faq.cta_qr', related: [ { icon: '⬛', key: 'blog.post1.title' }, { icon: '📊', key: 'blog.post3.title' }, { icon: '❓', key: 'faq.title' } ] } }; const cfg = postConfigs[slug]; if (!cfg) return; document.title = t(cfg.pageTitle); const metaDesc = document.querySelector('meta[name="description"]'); const metaKeywords = document.querySelector('meta[name="keywords"]'); if (metaDesc) metaDesc.content = t(cfg.pageDesc); if (metaKeywords) metaKeywords.content = t(cfg.pageKeywords); const breadcrumb = document.querySelector('.blog-post-wrap nav'); if (breadcrumb) { const links = breadcrumb.querySelectorAll('a'); if (links.length >= 2) { breadcrumb.innerHTML = `${links[0].outerHTML} > ${links[1].outerHTML} > ${t(cfg.crumb)}`; } } const cat = document.querySelector('.blog-post-cat'); const title = document.querySelector('.blog-post-header h1'); const date = document.querySelector('.blog-post-meta span:first-child'); const author = document.querySelector('.blog-post-meta span:last-child'); if (cat) cat.textContent = t(`blog.post${cfg.id}.cat`); if (title) title.textContent = t(`blog.post${cfg.id}.title`); if (date && cfg.date) date.textContent = t(cfg.date); if (author) author.textContent = t('blog.author'); if (cfg.bodyHtml) { const content = document.querySelector('.blog-post-content'); if (content) content.innerHTML = t(cfg.bodyHtml); } const tocTitle = document.querySelector('.blog-toc h4'); if (tocTitle) tocTitle.textContent = t('blog.toc'); const tocLinks = document.querySelectorAll('.blog-toc ol li a'); tocLinks.forEach((el, i) => { const key = cfg.toc[i]; if (key) el.textContent = t(key); }); const cta = document.querySelector('.blog-post-content > div[style*="text-align:center"][style*="linear-gradient"]'); if (cta) { const ctaTitle = cta.querySelector('h3'); const ctaSub = cta.querySelector('p'); const ctaBtn = cta.querySelector('a'); if (ctaTitle) ctaTitle.textContent = t(cfg.ctaTitle); if (ctaSub) ctaSub.textContent = t(cfg.ctaSub); if (ctaBtn) ctaBtn.textContent = t(cfg.ctaBtn); } const relatedTitle = document.querySelector('.related-posts h3'); if (relatedTitle) relatedTitle.textContent = t('blog.related'); const relatedLinks = document.querySelectorAll('.related-list a'); relatedLinks.forEach((el, i) => { const item = cfg.related[i]; if (item) { el.textContent = `${item.icon} ${t(item.key)}`; } }); } /* ══════════════════════════════════════════ UI AND INITIALIZATION ══════════════════════════════════════════ */ // 정적 다국어 라우트가 존재하는 언어와 페이지 (lib/i18n-build.js 와 일치) const ROUTE_LOCALES = ['ko', 'en', 'ja']; const LOCALIZABLE_ROUTES = ['/', '/qr-generator/', '/barcode-generator/', '/faq/', '/blog/']; /** * 현재 경로에서 언어 접두사를 제거한 기본 경로와, 정적 라우트로 전환 가능한지 반환. */ function getRouteContext() { let path = location.pathname; const prefixMatch = path.match(/^\/(en|ja)(\/.*)?$/); if (prefixMatch) { path = prefixMatch[2] || '/'; } return { base: path, canRoute: LOCALIZABLE_ROUTES.includes(path) }; } function injectLangSwitcher() { if (document.querySelector('.lang-switcher')) return; const select = document.createElement('select'); select.className = 'lang-switcher'; SUPPORTED.forEach(lang => { const option = document.createElement('option'); option.value = lang; option.textContent = LANG_NAMES[lang]; select.appendChild(option); }); select.addEventListener('change', async (e) => { const target = e.target.value; // 정적 번역 라우트가 있는 페이지/언어면 해당 URL로 이동 (하드 내비게이션) const { base, canRoute } = getRouteContext(); if (canRoute && ROUTE_LOCALES.includes(target)) { const dest = target === 'ko' ? base : `/${target}${base}`; if (dest !== location.pathname) { location.assign(dest); return; } } // 그 외(정적 라우트 없는 언어/페이지)는 기존 제자리 번역 await setLang(target); }); const headerInner = document.querySelector('.header-inner'); const hamburger = document.querySelector('.hamburger'); if (headerInner) { headerInner.insertBefore(select, hamburger); } } function updateLangSelector() { const selector = document.querySelector('.lang-switcher'); if (selector) { selector.value = currentLang; } } // Initial load document.addEventListener('DOMContentLoaded', async () => { injectLangSwitcher(); const lang = getLang(); await setLang(lang); });