/**
* 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);
});