360 lines
14 KiB
JavaScript
360 lines
14 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { en } from "../src/i18n/en.ts";
|
|
import { fr } from "../src/i18n/fr.ts";
|
|
import { ja } from "../src/i18n/ja.ts";
|
|
import { zh } from "../src/i18n/zh.ts";
|
|
|
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const outDir = path.join(root, "html");
|
|
|
|
const copies = { zh, en, ja, fr };
|
|
const LANGS = ["zh", "en", "ja", "fr"];
|
|
const htmlLang = { zh: "zh-CN", en: "en", ja: "ja", fr: "fr" };
|
|
const langLabels = { zh: "中文", en: "English", ja: "日本語", fr: "Français" };
|
|
|
|
function esc(value) {
|
|
return String(value)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
}
|
|
|
|
function assetPrefix(lang) {
|
|
return lang === "zh" ? "" : "../";
|
|
}
|
|
|
|
function pageHref(fromLang, toLang, page) {
|
|
const file = page === "home" ? "index.html" : "about.html";
|
|
if (fromLang === toLang) return file;
|
|
if (fromLang === "zh") return `${toLang}/${file}`;
|
|
if (toLang === "zh") return `../${file}`;
|
|
return `../${toLang}/${file}`;
|
|
}
|
|
|
|
function productHref(key, lang) {
|
|
if (key === "massiveedit" && (lang === "en" || lang === "fr")) {
|
|
return "/massiveedit/en/";
|
|
}
|
|
return `/${key}/`;
|
|
}
|
|
|
|
const ICONS = {
|
|
chevron: `<svg width="10" height="10" viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m6 9l6 6l6-6"/></svg>`,
|
|
moon: `<svg class="icon-light" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>`,
|
|
sun: `<svg class="icon-dark" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></g></svg>`,
|
|
menu: `<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5h16M4 12h16M4 19h16"/></svg>`,
|
|
};
|
|
|
|
function header(lang, page) {
|
|
const L = copies[lang];
|
|
const prefix = assetPrefix(lang);
|
|
const home = pageHref(lang, lang, "home");
|
|
const about = pageHref(lang, lang, "about");
|
|
const products = `${home}#products`;
|
|
|
|
const nav = [
|
|
{ label: L.nav.home, href: home, current: page === "home" },
|
|
{ label: L.nav.products, href: products, current: false },
|
|
{ label: L.nav.about, href: about, current: page === "about" },
|
|
];
|
|
|
|
return `<header class="site-header">
|
|
<div class="container site-nav">
|
|
<a class="brand" href="${home}" aria-label="${esc(L.brand.ariaHome)}">
|
|
<img class="brand-mark" src="${prefix}images/logo.svg" alt="" />
|
|
<div class="brand-copy">
|
|
<strong>${esc(L.brand.strong)}</strong><span>|</span><span>${esc(L.brand.suffix)}</span>
|
|
</div>
|
|
</a>
|
|
<nav class="nav-links" aria-label="${esc(L.lang.menuLabel)}">
|
|
${nav
|
|
.map(
|
|
(item) =>
|
|
`<a class="nav-link${item.current ? " current" : ""}" href="${item.href}">${esc(item.label)}</a>`,
|
|
)
|
|
.join("\n ")}
|
|
</nav>
|
|
<div class="header-tools">
|
|
<details class="lang-switch">
|
|
<summary class="locale-pill" aria-label="${esc(L.lang.menuLabel)}">
|
|
<span>${langLabels[lang]}</span>
|
|
${ICONS.chevron}
|
|
</summary>
|
|
<div class="lang-switch-panel">
|
|
${LANGS.map((code) => {
|
|
const href = pageHref(lang, code, page);
|
|
const hreflang = htmlLang[code];
|
|
return `<a${code === lang ? ' class="current"' : ""} href="${href}" hreflang="${hreflang}" lang="${hreflang}">${langLabels[code]}</a>`;
|
|
}).join("\n ")}
|
|
</div>
|
|
</details>
|
|
<button class="theme-toggle-btn" type="button" data-theme-toggle aria-label="${esc(L.theme.toggle)}" title="${esc(L.theme.toggle)}">
|
|
${ICONS.moon}
|
|
${ICONS.sun}
|
|
</button>
|
|
</div>
|
|
<button class="menu-toggle" type="button" data-menu-toggle aria-expanded="false" aria-label="Menu">
|
|
${ICONS.menu}
|
|
</button>
|
|
</div>
|
|
<div class="container mobile-menu" data-mobile-menu>
|
|
${nav
|
|
.map(
|
|
(item) =>
|
|
`<a${item.current ? ' class="current"' : ""} href="${item.href}">${esc(item.label)}</a>`,
|
|
)
|
|
.join("\n ")}
|
|
<div class="mobile-lang">
|
|
<p>${esc(L.lang.menuLabel)}</p>
|
|
${LANGS.map((code) => {
|
|
const href = pageHref(lang, code, page);
|
|
return `<a${code === lang ? ' class="current"' : ""} href="${href}">${langLabels[code]}</a>`;
|
|
}).join("\n ")}
|
|
</div>
|
|
<button class="theme-toggle-btn" type="button" data-theme-toggle aria-label="${esc(L.theme.toggle)}">
|
|
${ICONS.moon}
|
|
${ICONS.sun}
|
|
</button>
|
|
</div>
|
|
</header>`;
|
|
}
|
|
|
|
function footer(lang) {
|
|
const L = copies[lang];
|
|
return `<footer class="site-footer">
|
|
<div class="container footer-main">
|
|
<div>
|
|
<h3>${esc(L.footer.company)}</h3>
|
|
<p>${esc(L.footer.addressLine)}</p>
|
|
<p>${esc(L.footer.emailLine)}</p>
|
|
</div>
|
|
<div>
|
|
<h3>${esc(L.footer.colAbout)}</h3>
|
|
<div class="footer-links">
|
|
<a href="${pageHref(lang, lang, "about")}">${esc(L.footer.linkAbout)}</a>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3>${esc(L.footer.colProducts)}</h3>
|
|
<div class="footer-links">
|
|
<a href="${productHref("smartbuddy", lang)}">${esc(L.footer.linkBuddy)}</a>
|
|
<a href="${productHref("smartinput", lang)}">${esc(L.footer.linkInput)}</a>
|
|
<a href="${productHref("smarthanzi", lang)}">${esc(L.footer.linkHanzi)}</a>
|
|
<a href="${productHref("massiveedit", lang)}">${esc(L.footer.linkEdit)}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="container footer-bottom">
|
|
<p class="footer-legal-line">
|
|
${esc(L.footer.copyrightMain)}
|
|
<a class="footer-icp-link" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">${esc(L.footer.icpNumber)}</a>
|
|
</p>
|
|
<p class="footer-trademark">${esc(L.footer.trademarkNotice)}</p>
|
|
</div>
|
|
</footer>`;
|
|
}
|
|
|
|
function homeMain(lang) {
|
|
const L = copies[lang];
|
|
const products = [
|
|
{
|
|
key: "smartbuddy",
|
|
name: L.home.productBuddyName,
|
|
en: L.home.productBuddyEn,
|
|
body: L.home.productBuddyBody,
|
|
cta: L.home.productBuddyCta,
|
|
},
|
|
{
|
|
key: "smartinput",
|
|
name: L.home.productInputName,
|
|
en: L.home.productInputEn,
|
|
body: L.home.productInputBody,
|
|
cta: L.home.productInputCta,
|
|
},
|
|
{
|
|
key: "smarthanzi",
|
|
name: L.home.productHanziName,
|
|
en: L.home.productHanziEn,
|
|
body: L.home.productHanziBody,
|
|
cta: L.home.productHanziCta,
|
|
},
|
|
{
|
|
key: "massiveedit",
|
|
name: L.home.productEditName,
|
|
en: L.home.productEditEn,
|
|
body: L.home.productEditBody,
|
|
cta: L.home.productEditCta,
|
|
},
|
|
];
|
|
|
|
return `<section class="hero home-hero">
|
|
<div class="container">
|
|
<p class="home-kicker">${esc(L.brand.strong)} · ${esc(L.brand.suffix)}</p>
|
|
<h1 class="hero-title">${esc(L.home.heroTitle)}</h1>
|
|
<p class="home-lead">${esc(L.home.heroSubtitle)}</p>
|
|
<div class="hero-actions">
|
|
<a class="btn btn-primary" href="#products">${esc(L.home.ctaProducts)}</a>
|
|
<a class="btn btn-outline" href="${pageHref(lang, lang, "about")}">${esc(L.home.ctaAbout)}</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="section compact" id="products">
|
|
<div class="container">
|
|
<div class="section-head">
|
|
<h2 class="section-title">${esc(L.home.productsTitle)}</h2>
|
|
<p class="section-copy">${esc(L.home.productsLead)}</p>
|
|
</div>
|
|
<div class="product-grid">
|
|
${products
|
|
.map(
|
|
(product) => `<article class="feature-card product-card">
|
|
<p class="product-kicker">${esc(product.en)}</p>
|
|
<h3>${esc(product.name)}</h3>
|
|
<p>${esc(product.body)}</p>
|
|
<a class="mini-link" href="${productHref(product.key, lang)}">${esc(product.cta)}</a>
|
|
</article>`,
|
|
)
|
|
.join("\n ")}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="section compact" id="company">
|
|
<div class="container grid-two company-contact">
|
|
<article class="feature-card">
|
|
<h3>${esc(L.home.companyTitle)}</h3>
|
|
<p>${esc(L.home.companyBody)}</p>
|
|
<a class="mini-link" href="${pageHref(lang, lang, "about")}">${esc(L.home.ctaCompany)}</a>
|
|
</article>
|
|
<article class="feature-card">
|
|
<h3>${esc(L.home.contactTitle)}</h3>
|
|
<p>${esc(L.home.contactBody)}</p>
|
|
<a class="mini-link" href="mailto:liutingchao@fivequestions.cn">${esc(L.home.ctaContact)}</a>
|
|
</article>
|
|
</div>
|
|
</section>`;
|
|
}
|
|
|
|
function aboutMain(lang) {
|
|
const L = copies[lang];
|
|
return `<section class="hero home-hero about-hero">
|
|
<div class="container">
|
|
<p class="home-kicker">${esc(L.brand.strong)} · ${esc(L.brand.suffix)}</p>
|
|
<h1 class="hero-title">${esc(L.about.heroTitle)}</h1>
|
|
<p class="home-lead">${esc(L.about.heroLead)}</p>
|
|
</div>
|
|
</section>
|
|
<section class="section compact">
|
|
<div class="container about-stack">
|
|
<article class="feature-card">
|
|
<h2>${esc(L.about.visionTitle)}</h2>
|
|
<p>${esc(L.about.visionBody)}</p>
|
|
</article>
|
|
<article class="feature-card">
|
|
<h2>${esc(L.about.missionTitle)}</h2>
|
|
<p>${esc(L.about.missionBody)}</p>
|
|
</article>
|
|
<article class="feature-card">
|
|
<h2>${esc(L.about.storyTitle)}</h2>
|
|
<p>${esc(L.about.storyBody)}</p>
|
|
</article>
|
|
<article class="feature-card">
|
|
<h2>${esc(L.about.teamTitle)}</h2>
|
|
<p>${esc(L.about.teamBody)}</p>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
<section class="section compact">
|
|
<div class="container">
|
|
<div class="about-products">
|
|
<a href="${productHref("smartbuddy", lang)}">${esc(L.footer.linkBuddy)}</a>
|
|
<a href="${productHref("smartinput", lang)}">${esc(L.footer.linkInput)}</a>
|
|
<a href="${productHref("smarthanzi", lang)}">${esc(L.footer.linkHanzi)}</a>
|
|
<a href="${productHref("massiveedit", lang)}">${esc(L.footer.linkEdit)}</a>
|
|
</div>
|
|
</div>
|
|
</section>`;
|
|
}
|
|
|
|
function documentFor(lang, page) {
|
|
const L = copies[lang];
|
|
const prefix = assetPrefix(lang);
|
|
const meta = page === "home" ? L.home : L.about;
|
|
const alternates = LANGS.map((code) => {
|
|
const href =
|
|
code === "zh"
|
|
? lang === "zh"
|
|
? page === "home"
|
|
? "index.html"
|
|
: "about.html"
|
|
: `../${page === "home" ? "index.html" : "about.html"}`
|
|
: lang === "zh"
|
|
? `${code}/${page === "home" ? "index.html" : "about.html"}`
|
|
: code === lang
|
|
? page === "home"
|
|
? "index.html"
|
|
: "about.html"
|
|
: `../${code}/${page === "home" ? "index.html" : "about.html"}`;
|
|
return `<link rel="alternate" hreflang="${htmlLang[code]}" href="${href}" />`;
|
|
}).join("\n ");
|
|
|
|
const defaultHref =
|
|
lang === "zh"
|
|
? page === "home"
|
|
? "index.html"
|
|
: "about.html"
|
|
: `../${page === "home" ? "index.html" : "about.html"}`;
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="${htmlLang[lang]}" data-theme="light" class="site-lang-${lang}">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>${esc(meta.metaTitle)}</title>
|
|
<meta name="description" content="${esc(meta.metaDescription)}" />
|
|
<link rel="icon" type="image/svg+xml" href="${prefix}images/logo.svg" />
|
|
${alternates}
|
|
<link rel="alternate" hreflang="x-default" href="${defaultHref}" />
|
|
<link rel="stylesheet" href="${prefix}css/site.css" />
|
|
<script>
|
|
(function () {
|
|
try {
|
|
var t = localStorage.getItem("theme");
|
|
var dark =
|
|
t === "dark" ||
|
|
(!t && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
|
|
} catch (_) {}
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="page-shell bg-wash">
|
|
${header(lang, page)}
|
|
<main>
|
|
${page === "home" ? homeMain(lang) : aboutMain(lang)}
|
|
</main>
|
|
${footer(lang)}
|
|
</div>
|
|
<script src="${prefix}js/site.js"></script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
function writePage(lang, page) {
|
|
const dir = lang === "zh" ? outDir : path.join(outDir, lang);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
const file = page === "home" ? "index.html" : "about.html";
|
|
fs.writeFileSync(path.join(dir, file), documentFor(lang, page));
|
|
}
|
|
|
|
for (const lang of LANGS) {
|
|
writePage(lang, "home");
|
|
writePage(lang, "about");
|
|
}
|
|
|
|
console.log("Wrote static portal pages to html/");
|