mingle-website/build_i18n.js
68893236+KINDNICK@users.noreply.github.com 30cd06e9d6 feat: setup i18n build script and SEO optimizations
2026-03-01 14:56:52 +09:00

204 lines
7.9 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
const langs = ['ko', 'en', 'ja', 'zh'];
const defaultLang = 'ko';
const siteUrl = 'https://minglestudio.co.kr';
const rootDir = __dirname;
const i18nDir = path.join(rootDir, 'i18n');
const componentsDir = path.join(rootDir, 'components');
// Read all html files
const htmlFiles = fs.readdirSync(rootDir).filter(f => f.endsWith('.html') && f !== 'naver-site-verification.html');
// Load translations
const translations = {};
langs.forEach(lang => {
try {
translations[lang] = JSON.parse(fs.readFileSync(path.join(i18nDir, `${lang}.json`), 'utf-8'));
} catch (e) {
console.error(`Failed to load ${lang}.json`, e);
}
});
// Load components
const headerHtml = fs.readFileSync(path.join(componentsDir, 'header.html'), 'utf-8');
const footerHtml = fs.readFileSync(path.join(componentsDir, 'footer.html'), 'utf-8');
function t(lang, key, fallback = '') {
if (lang === defaultLang) return fallback || key;
const keys = key.split('.');
let val = translations[lang];
if (!val) return fallback || key;
for (const k of keys) {
if (val && typeof val === 'object' && k in val) {
val = val[k];
} else {
return fallback || key;
}
}
return typeof val === 'string' ? val : (fallback || key);
}
console.log('Starting i18n SSG Build...');
langs.forEach(lang => {
const isDefault = lang === defaultLang;
const outDir = isDefault ? rootDir : path.join(rootDir, lang);
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
htmlFiles.forEach(file => {
const pageName = file.replace('.html', '') || 'index';
const content = fs.readFileSync(path.join(rootDir, file), 'utf-8');
const $ = cheerio.load(content, { decodeEntities: false });
// Change lang attribute
$('html').attr('lang', lang);
// Remove existing hreflang tags if any to prevent duplicates
$('link[rel="alternate"][hreflang]').remove();
// Add hreflang tags
const head = $('head');
langs.forEach(l => {
const href = l === defaultLang
? `${siteUrl}${file === 'index.html' ? '/' : '/' + file}`
: `${siteUrl}/${l}${file === 'index.html' ? '/' : '/' + file}`;
head.append(`\n <link rel="alternate" hreflang="${l}" href="${href}" />`);
});
head.append(`\n <link rel="alternate" hreflang="x-default" href="${siteUrl}${file === 'index.html' ? '/' : '/' + file}" />\n`);
// Change canonical
const canonicalHref = isDefault
? `${siteUrl}${file === 'index.html' ? '/' : '/' + file}`
: `${siteUrl}/${lang}${file === 'index.html' ? '/' : '/' + file}`;
$('link[rel="canonical"]').attr('href', canonicalHref);
// Inject components
if ($('#header-placeholder').html()?.trim() === '') {
$('#header-placeholder').html('\n' + headerHtml + '\n');
}
if ($('#footer-placeholder').html()?.trim() === '') {
$('#footer-placeholder').html('\n' + footerHtml + '\n');
}
// Translate nodes (only if not default language)
if (!isDefault) {
$('[data-i18n]').each((i, el) => {
const key = $(el).attr('data-i18n');
const trans = t(lang, key, $(el).text());
if (trans.includes('<')) $(el).html(trans);
else $(el).text(trans);
});
$('[data-i18n-html]').each((i, el) => {
const key = $(el).attr('data-i18n-html');
const trans = t(lang, key, $(el).html());
$(el).html(trans);
});
$('[data-i18n-placeholder]').each((i, el) => {
const key = $(el).attr('data-i18n-placeholder');
$(el).attr('placeholder', t(lang, key, $(el).attr('placeholder')));
});
$('[data-i18n-aria]').each((i, el) => {
const key = $(el).attr('data-i18n-aria');
$(el).attr('aria-label', t(lang, key, $(el).attr('aria-label') || ''));
});
$('[data-i18n-title]').each((i, el) => {
const key = $(el).attr('data-i18n-title');
$(el).attr('title', t(lang, key, $(el).attr('title') || ''));
});
// Meta tags
const titleKey = `${pageName}.meta.title`;
const tTitle = t(lang, titleKey, null);
if (tTitle) $('title').text(tTitle);
const descKey = `${pageName}.meta.description`;
const tDesc = t(lang, descKey, null);
if (tDesc) $('meta[name="description"]').attr('content', tDesc);
const ogTitleKey = `${pageName}.meta.ogTitle`;
const tOgTitle = t(lang, ogTitleKey, null);
if (tOgTitle) $('meta[property="og:title"]').attr('content', tOgTitle);
const ogDescKey = `${pageName}.meta.ogDescription`;
const tOgDesc = t(lang, ogDescKey, null);
if (tOgDesc) $('meta[property="og:description"]').attr('content', tOgDesc);
}
if (!isDefault) {
// Rewrite asset paths
$('link[href^="css/"]').attr('href', (i, val) => '/' + val);
$('link[href^="components/"]').attr('href', (i, val) => '/' + val);
$('script[src^="js/"]').attr('src', (i, val) => '/' + val);
$('img[src^="images/"]').attr('src', (i, val) => '/' + val);
// Re-map internal links to proper language folder
$('a[href^="/"], a.nav-link').each((i, el) => {
const href = $(el).attr('href');
if (!href) return;
// If it's a root-relative link (e.g., /about, /contact)
if (href.startsWith('/') && href.length > 1 && !href.startsWith('/images') && !href.startsWith('/css') && !href.startsWith('/js') && !href.startsWith('/i18n')) {
const newHref = `/${lang}${href}`;
$(el).attr('href', newHref);
} else if (href === '/') {
$(el).attr('href', `/${lang}/`);
} else if (!href.startsWith('http') && !href.startsWith('#') && !href.startsWith('/') && !href.startsWith('tel:') && !href.startsWith('mailto:')) {
// For links like href="about.html" or href="contact.html", we need to change it
if (href.endsWith('.html')) {
const newHref = `/${lang}/${href.replace('.html', '')}`;
$(el).attr('href', newHref);
}
}
});
}
fs.writeFileSync(path.join(outDir, file), $.html());
console.log(`[${lang}] Processed ${file}`);
});
});
console.log('Generating sitemap.xml...');
let sitemapUrls = '';
langs.forEach(lang => {
htmlFiles.forEach(file => {
const isDefault = lang === defaultLang;
const pageRoute = file === 'index.html' ? '' : file.replace('.html', '');
const loc = isDefault
? `${siteUrl}/${pageRoute}`
: `${siteUrl}/${lang}/${pageRoute}`;
// Remove trailing slash if it's just root, but wait siteUrl + '/' for root
const cleanLoc = loc.endsWith('/') ? loc : (pageRoute === '' && loc === siteUrl ? `${siteUrl}/` : loc);
sitemapUrls += `
<url>
<loc>${cleanLoc}</loc>
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
<changefreq>weekly</changefreq>
<priority>${file === 'index.html' ? '1.0' : '0.8'}</priority>
</url>`;
});
});
const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
${sitemapUrls}
</urlset>
`;
fs.writeFileSync(path.join(rootDir, 'sitemap.xml'), sitemapContent);
console.log('Sitemap generated!');
console.log('Build completed! Language HTML files have been generated.');