change html
|
After Width: | Height: | Size: 3.7 MiB |
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import icon from 'astro-icon';
|
||||
import { productSites } from './scripts/product-sites.mjs';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [icon(), productSites()],
|
||||
compressHTML: true,
|
||||
build: {
|
||||
inlineStylesheets: 'auto',
|
||||
},
|
||||
redirects: {
|
||||
'/teaching-philosophy': '/',
|
||||
'/curriculum': '/',
|
||||
'/faculty': '/',
|
||||
'/en/teaching-philosophy': '/en',
|
||||
'/en/curriculum': '/en',
|
||||
'/en/faculty': '/en',
|
||||
'/ja/teaching-philosophy': '/ja',
|
||||
'/ja/curriculum': '/ja',
|
||||
'/ja/faculty': '/ja',
|
||||
'/fr/teaching-philosophy': '/fr',
|
||||
'/fr/curriculum': '/fr',
|
||||
'/fr/faculty': '/fr',
|
||||
},
|
||||
vite: {
|
||||
build: {
|
||||
cssMinify: 'esbuild',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "fiveq-website",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev --host 0.0.0.0",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/manrope": "^5.2.8",
|
||||
"@iconify-json/lucide": "^1.2.32",
|
||||
"astro": "^5.7.10",
|
||||
"astro-icon": "^1.1.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Cache static assets for 1 year (content hashed filenames ensure cache busting)
|
||||
/_astro/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
# Fonts — cache for 1 year
|
||||
/fonts/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
# Images — cache for 1 year
|
||||
/images/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
# HTML pages — no cache (always fetch latest)
|
||||
/index.html
|
||||
Cache-Control: no-cache
|
||||
|
||||
/about/*
|
||||
Cache-Control: no-cache
|
||||
|
||||
/en/*
|
||||
Cache-Control: no-cache
|
||||
|
||||
/ja/*
|
||||
Cache-Control: no-cache
|
||||
|
||||
/fr/*
|
||||
Cache-Control: no-cache
|
||||
|
||||
/smartbuddy/*
|
||||
Cache-Control: no-cache
|
||||
|
||||
/smartinput/*
|
||||
Cache-Control: no-cache
|
||||
|
||||
/smarthanzi/*
|
||||
Cache-Control: no-cache
|
||||
|
||||
/massiveedit/*
|
||||
Cache-Control: no-cache
|
||||
@@ -0,0 +1,73 @@
|
||||
/* Self-hosted Manrope font — eliminates slow Google Fonts CDN dependency */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
src: url('/fonts/manrope-latin-400-normal.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
src: url('/fonts/manrope-latin-ext-400-normal.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
src: url('/fonts/manrope-latin-600-normal.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
src: url('/fonts/manrope-latin-ext-600-normal.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
src: url('/fonts/manrope-latin-700-normal.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
src: url('/fonts/manrope-latin-ext-700-normal.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
src: url('/fonts/manrope-latin-800-normal.woff2') format('woff2');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
src: url('/fonts/manrope-latin-ext-800-normal.woff2') format('woff2');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="none">
|
||||
<rect x="2" y="2" width="36" height="36" rx="18" fill="#7AA0FF"/>
|
||||
<path d="M12 15.5h5v9h-5zm11 0h5v9h-5zM17.5 12h5v5h-5zm0 12h5v5h-5z" fill="#fff"/>
|
||||
<path d="M9.5 26.5c1.5-2.9 4.2-4.2 7-4.2h7c2.7 0 5.4 1.3 7 4.2" stroke="#fff" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
@@ -0,0 +1,9 @@
|
||||
<svg viewBox="0 0 43.4326 42.6396" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="43.432617" height="42.639648" fill="none" customFrame="#000000">
|
||||
<g id="组合 1">
|
||||
<path id="Fill 29" d="M26.142 22.754C28.928 20.721 30.699 16.653 30.699 12.185C30.699 8.453 29.569 5.07 27.599 2.903L27.597 2.901C25.962 1.058 23.614 0 21.154 0C16.555 0 12.823 3.542 12.561 8.105C12.629 8.112 12.697 8.12 12.765 8.13C16.793 8.688 20.408 10.479 22.942 13.173C25.49 15.881 26.643 19.178 26.189 22.455C26.175 22.553 26.16 22.651 26.142 22.754Z" fill="rgb(49,123,255)" fill-rule="evenodd" />
|
||||
<path id="Fill 30" d="M0.429332 16.2493C-0.991668 20.6233 1.22233 25.2673 5.48133 26.9263C5.51033 26.8633 5.53833 26.8023 5.56833 26.7393C7.34433 23.0813 10.1643 20.1973 13.5093 18.6193C16.8723 17.0333 20.3643 16.9553 23.3413 18.3993C23.4293 18.4423 23.5183 18.4883 23.6103 18.5363C22.5373 15.2593 19.2173 12.3173 14.9663 10.9373C13.2343 10.3733 11.4963 10.0923 9.86133 10.0923C8.14833 10.0923 6.54833 10.4003 5.18133 11.0173L5.17933 11.0183C2.92033 12.0033 1.18933 13.9103 0.429332 16.2493Z" fill="rgb(49,123,255)" fill-rule="evenodd" />
|
||||
<path id="Fill 31" d="M21.1932 39.4748C21.1432 39.4288 21.0922 39.3828 21.0432 39.3348C14.9782 33.5008 13.8832 24.7628 18.6032 19.8558C18.6722 19.7848 18.7422 19.7128 18.8172 19.6418C15.3682 19.6488 11.5442 21.8988 8.91825 25.5138C6.72525 28.5328 5.65125 31.9328 5.97025 34.8438L5.97125 34.8478C6.21025 37.2998 7.48825 39.5358 9.47825 40.9818C13.1992 43.6848 18.2992 43.0128 21.1932 39.4748Z" fill="rgb(49,123,255)" fill-rule="evenodd" />
|
||||
<path id="Fill 32" d="M35.7959 40.0178C39.5169 37.3138 40.4539 32.2558 37.9839 28.4108C37.9239 28.4438 37.8649 28.4778 37.8039 28.5098C34.2169 30.4258 30.2409 31.1008 26.6079 30.4118C22.9539 29.7178 20.0839 27.7288 18.5239 24.8108C18.4779 24.7238 18.4319 24.6348 18.3869 24.5408C17.3279 27.8238 18.2859 32.1558 20.9129 35.7708C23.1059 38.7888 26.0079 40.8618 28.8759 41.4568L28.8789 41.4568C31.2849 41.9878 33.8059 41.4628 35.7959 40.0178Z" fill="rgb(49,123,255)" fill-rule="evenodd" />
|
||||
<path id="Fill 33" d="M42.2426 21.7159L42.2436 21.7139C43.4926 19.5889 43.7726 17.0289 43.0126 14.6899C41.5906 10.3159 37.0696 7.86194 32.6496 9.02294C32.6626 9.08894 32.6756 9.15594 32.6876 9.22394C34.1656 17.5079 29.9146 25.2219 23.2126 26.4169C23.1156 26.4349 23.0166 26.4499 22.9136 26.4649C25.7076 28.4859 30.1236 28.9139 34.3736 27.5319C37.9226 26.3799 40.7896 24.2599 42.2426 21.7159Z" fill="rgb(49,123,255)" fill-rule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 498 B |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 954 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,3 @@
|
||||
npm install
|
||||
npm run dev
|
||||
npm run build
|
||||
@@ -0,0 +1,359 @@
|
||||
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/");
|
||||
@@ -0,0 +1,115 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export const PRODUCT_DIRS = ['smartbuddy', 'smartinput', 'smarthanzi', 'massiveedit', 'html'];
|
||||
|
||||
const SKIP_NAMES = new Set([
|
||||
'.DS_Store',
|
||||
'.texpadtmp',
|
||||
'smartbuddy_readme',
|
||||
]);
|
||||
|
||||
const SKIP_EXT = new Set([
|
||||
'.tex',
|
||||
'.aux',
|
||||
'.log',
|
||||
'.toc',
|
||||
'.out',
|
||||
'.synctex.gz',
|
||||
]);
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.mjs': 'text/javascript; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.webp': 'image/webp',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff2': 'font/woff2',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.md': 'text/plain; charset=utf-8',
|
||||
'.apk': 'application/vnd.android.package-archive',
|
||||
'.deb': 'application/vnd.debian.binary-package',
|
||||
'.pkg': 'application/octet-stream',
|
||||
'.exe': 'application/octet-stream',
|
||||
'.zip': 'application/zip',
|
||||
};
|
||||
|
||||
export function shouldCopy(src) {
|
||||
const base = path.basename(src);
|
||||
if (SKIP_NAMES.has(base)) return false;
|
||||
if (base.startsWith('.')) return false;
|
||||
if (SKIP_EXT.has(path.extname(base))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function copyProductSites(destRoot, sourceRoot = process.cwd()) {
|
||||
for (const dir of PRODUCT_DIRS) {
|
||||
const from = path.join(sourceRoot, dir);
|
||||
if (!fs.existsSync(from)) continue;
|
||||
fs.cpSync(from, path.join(destRoot, dir), {
|
||||
recursive: true,
|
||||
filter: shouldCopy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resolveProductFile(root, urlPath) {
|
||||
const clean = urlPath.split('?')[0];
|
||||
const product = PRODUCT_DIRS.find(
|
||||
(name) => clean === `/${name}` || clean === `/${name}/` || clean.startsWith(`/${name}/`),
|
||||
);
|
||||
if (!product) return null;
|
||||
|
||||
let rel = clean.slice(product.length + 2);
|
||||
if (!rel || rel.endsWith('/')) rel = `${rel}index.html`;
|
||||
|
||||
const file = path.join(root, product, rel);
|
||||
if (fs.existsSync(file) && fs.statSync(file).isFile()) return file;
|
||||
|
||||
const asIndex = path.join(root, product, rel, 'index.html');
|
||||
if (fs.existsSync(asIndex) && fs.statSync(asIndex).isFile()) return asIndex;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function serveProduct(root, req, res, next) {
|
||||
const file = resolveProductFile(root, req.url ?? '');
|
||||
if (!file) return next();
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream');
|
||||
fs.createReadStream(file).pipe(res);
|
||||
}
|
||||
|
||||
export function productSites() {
|
||||
const root = process.cwd();
|
||||
return {
|
||||
name: 'product-sites',
|
||||
hooks: {
|
||||
'astro:config:setup'({ updateConfig }) {
|
||||
updateConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
{
|
||||
name: 'serve-product-sites',
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => serveProduct(root, req, res, next));
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
'astro:build:done'({ dir }) {
|
||||
copyProductSites(fileURLToPath(dir));
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
import { hrefFor, hrefForProduct, type Lang, type SiteCopy } from '../i18n';
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
copy: SiteCopy;
|
||||
}
|
||||
|
||||
const { lang, copy } = Astro.props;
|
||||
---
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-main">
|
||||
<div>
|
||||
<h3>{copy.footer.company}</h3>
|
||||
<p>{copy.footer.addressLine}</p>
|
||||
<p>{copy.footer.emailLine}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{copy.footer.colAbout}</h3>
|
||||
<div class="footer-links">
|
||||
<a href={hrefFor(lang, 'about')}>{copy.footer.linkAbout}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{copy.footer.colProducts}</h3>
|
||||
<div class="footer-links">
|
||||
<a href={hrefForProduct('smartbuddy', lang)}>{copy.footer.linkBuddy}</a>
|
||||
<a href={hrefForProduct('smartinput', lang)}>{copy.footer.linkInput}</a>
|
||||
<a href={hrefForProduct('smarthanzi', lang)}>{copy.footer.linkHanzi}</a>
|
||||
<a href={hrefForProduct('massiveedit', lang)}>{copy.footer.linkEdit}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container footer-bottom">
|
||||
<p class="footer-legal-line">
|
||||
{copy.footer.copyrightMain}
|
||||
{' '}
|
||||
<a
|
||||
class="footer-icp-link"
|
||||
href="https://beian.miit.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{copy.footer.icpNumber}
|
||||
</a>
|
||||
</p>
|
||||
<p class="footer-trademark">{copy.footer.trademarkNotice}</p>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import {
|
||||
LANGS,
|
||||
hrefFor,
|
||||
hrefForHomeHash,
|
||||
langLabels,
|
||||
type Lang,
|
||||
type RouteKey,
|
||||
type SiteCopy,
|
||||
} from '../i18n';
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
copy: SiteCopy;
|
||||
currentSegment: RouteKey;
|
||||
}
|
||||
|
||||
const { lang, copy, currentSegment } = Astro.props;
|
||||
|
||||
const navItems: { label: string; href: string; current: boolean }[] = [
|
||||
{ label: copy.nav.home, href: hrefFor(lang, ''), current: currentSegment === '' },
|
||||
{ label: copy.nav.products, href: hrefForHomeHash(lang, 'products'), current: false },
|
||||
{ label: copy.nav.about, href: hrefFor(lang, 'about'), current: currentSegment === 'about' },
|
||||
];
|
||||
---
|
||||
|
||||
<header class="site-header">
|
||||
<div class="container site-nav">
|
||||
<a class="brand" href={hrefFor(lang, '')} aria-label={copy.brand.ariaHome}>
|
||||
<img class="brand-mark" src="/images/logo.svg" alt="" />
|
||||
<div class="brand-copy">
|
||||
<strong>{copy.brand.strong}</strong><span>|</span><span>{copy.brand.suffix}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<nav class="nav-links" aria-label={copy.lang.menuLabel}>
|
||||
{navItems.map((item) => (
|
||||
<a class:list={['nav-link', { current: item.current }]} href={item.href}>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div class="header-tools">
|
||||
<details class="lang-switch">
|
||||
<summary class="locale-pill" aria-label={copy.lang.menuLabel}>
|
||||
<span>{langLabels[lang]}</span>
|
||||
<Icon name="lucide:chevron-down" size={10} />
|
||||
</summary>
|
||||
<div class="lang-switch-panel">
|
||||
{LANGS.map((code) => (
|
||||
<a
|
||||
class:list={[{ current: code === lang }]}
|
||||
href={hrefFor(code, currentSegment)}
|
||||
hreflang={code === 'zh' ? 'zh-CN' : code}
|
||||
lang={code === 'zh' ? 'zh-CN' : code}
|
||||
>
|
||||
{langLabels[code]}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<button
|
||||
class="theme-toggle-btn"
|
||||
type="button"
|
||||
data-theme-toggle
|
||||
aria-label={copy.theme.toggle}
|
||||
title={copy.theme.toggle}
|
||||
>
|
||||
<Icon class="icon-light" name="lucide:moon" size={20} />
|
||||
<Icon class="icon-dark" name="lucide:sun" size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="menu-toggle"
|
||||
type="button"
|
||||
data-menu-toggle
|
||||
aria-expanded="false"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<Icon name="lucide:menu" size={22} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="container mobile-menu" data-mobile-menu>
|
||||
{navItems.map((item) => (
|
||||
<a class:list={[{ current: item.current }]} href={item.href}>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
<div class="mobile-lang">
|
||||
<p>{copy.lang.menuLabel}</p>
|
||||
{LANGS.map((code) => (
|
||||
<a class:list={[{ current: code === lang }]} href={hrefFor(code, currentSegment)}>
|
||||
{langLabels[code]}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
class="theme-toggle-btn"
|
||||
type="button"
|
||||
data-theme-toggle
|
||||
aria-label={copy.theme.toggle}
|
||||
>
|
||||
<Icon class="icon-light" name="lucide:moon" size={20} />
|
||||
<Icon class="icon-dark" name="lucide:sun" size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { SiteCopy } from './types';
|
||||
|
||||
export const en: SiteCopy = {
|
||||
brand: {
|
||||
strong: 'Zhihua',
|
||||
suffix: 'Five Questions',
|
||||
ariaHome: 'Zhihua home',
|
||||
},
|
||||
nav: {
|
||||
home: 'Home',
|
||||
products: 'Products',
|
||||
about: 'About',
|
||||
},
|
||||
theme: {
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
toggle: 'Switch light or dark theme',
|
||||
},
|
||||
lang: {
|
||||
menuLabel: 'Language',
|
||||
switchTo: 'Switch to',
|
||||
},
|
||||
footer: {
|
||||
company: 'Five Questions (Beijing) Technology Co., Ltd.',
|
||||
addressLine: 'Address: Room A-012, 1F, North Auxiliary Building, No. 6 Tianfu North Street, Wenjing Subdistrict, Tongzhou District, Beijing 101100, China',
|
||||
emailLine: 'Email: liutingchao@fivequestions.cn',
|
||||
colAbout: 'About',
|
||||
linkAbout: 'Company',
|
||||
colProducts: 'Products',
|
||||
linkBuddy: 'SmartBuddy',
|
||||
linkInput: 'SmartInput',
|
||||
linkHanzi: 'SmartHanzi',
|
||||
linkEdit: 'MassiveEdit',
|
||||
copyrightMain: '© 2026 Five Questions (Beijing) Technology Co., Ltd. All rights reserved.',
|
||||
icpNumber: '京ICP备2025143052号',
|
||||
trademarkNotice:
|
||||
'五道题® and 智花® are registered trademarks of Five Questions (Beijing) Technology Co., Ltd. English wording on this site is for display only and does not replace the Chinese marks. Unauthorized use that may cause confusion is prohibited.',
|
||||
},
|
||||
home: {
|
||||
metaTitle: 'Zhihua | Five Questions',
|
||||
metaDescription:
|
||||
'Software from Five Questions: SmartBuddy, SmartInput, SmartHanzi, and MassiveEdit—learning, input, and productivity tools.',
|
||||
heroTitle: 'Learning, input, and productivity tools',
|
||||
heroSubtitle:
|
||||
'From Five Questions. Zhihua covers Chinese learning and input; MassiveEdit is built for very large text files.',
|
||||
ctaProducts: 'See products',
|
||||
ctaAbout: 'About us',
|
||||
productsTitle: 'Products',
|
||||
productsLead:
|
||||
'Zhihua is for Chinese learning and typing. MassiveEdit is for people who need to work with huge text files.',
|
||||
productBuddyName: '智花学伴',
|
||||
productBuddyEn: 'SmartBuddy',
|
||||
productBuddyBody:
|
||||
'A student app for international Chinese: homework from teachers, speaking practice, coaching, and real-life scenes.',
|
||||
productBuddyCta: 'Open SmartBuddy',
|
||||
productInputName: '智花输入法',
|
||||
productInputEn: 'SmartInput',
|
||||
productInputBody:
|
||||
'Full QWERTY pinyin, 9-key pinyin, stroke, and handwriting—on iOS, Android, and desktop.',
|
||||
productInputCta: 'Open SmartInput',
|
||||
productHanziName: '智花识字',
|
||||
productHanziEn: 'SmartHanzi',
|
||||
productHanziBody:
|
||||
'Learn characters with the school textbook: reading, stroke tracing, and spaced review.',
|
||||
productHanziCta: 'Open SmartHanzi',
|
||||
productEditName: 'MassiveEdit',
|
||||
productEditEn: 'Large File Editor',
|
||||
productEditBody:
|
||||
'A text editor for very large files, built for fast open, smooth scrolling, and steady editing.',
|
||||
productEditCta: 'Open MassiveEdit',
|
||||
companyTitle: 'Five Questions',
|
||||
companyBody:
|
||||
'Five Questions (Beijing) Technology Co., Ltd. builds practical learning and productivity software. Zhihua is the brand for Chinese learning and input; MassiveEdit is our large-file editor.',
|
||||
ctaCompany: 'About the company',
|
||||
contactTitle: 'Contact',
|
||||
contactBody: 'For trials, partnerships, or product questions, write to us.',
|
||||
ctaContact: 'Email us',
|
||||
},
|
||||
about: {
|
||||
metaTitle: 'About | Zhihua · Five Questions',
|
||||
metaDescription:
|
||||
'Five Questions (Beijing) Technology Co., Ltd.: Zhihua learning and input products, and tools such as MassiveEdit.',
|
||||
heroTitle: 'About us',
|
||||
heroLead:
|
||||
'We build learning, input, and productivity software. Zhihua covers Chinese learning and typing; MassiveEdit is for people who work with very large text files.',
|
||||
visionFigureAlt: 'Product and workspace visuals',
|
||||
visionTitle: 'Vision',
|
||||
visionBody:
|
||||
'Turn the hard parts of learning, writing, and everyday work into software people can actually use—clear, stable, and worth refining over time.',
|
||||
missionTitle: 'Mission',
|
||||
missionBody:
|
||||
'Ship tools that hold up in real use: students finish homework and speak, typing feels natural, character study has a path, and huge files still open and edit smoothly.',
|
||||
missionImgAlt: 'Engineering and collaboration',
|
||||
storyTitle: 'Brand story',
|
||||
storyBody:
|
||||
'“Zhihua” suggests intelligence and culture coming into bloom. Five Questions is the company; Zhihua is the product brand for Chinese learning and input. Linguistics, engineering, and product work sit on one line.',
|
||||
storyImgAlt: 'Brand story artwork',
|
||||
teamTitle: 'Team',
|
||||
teamBody:
|
||||
'The team builds and iterates products: ship features, fix issues, listen to feedback. We do not recruit students or run a faculty franchise—effort goes into the software.',
|
||||
teamImgAlt: 'Team collaboration',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { SiteCopy } from './types';
|
||||
|
||||
export const fr: SiteCopy = {
|
||||
brand: {
|
||||
strong: 'Zhihua',
|
||||
suffix: 'Five Questions',
|
||||
ariaHome: 'Accueil Zhihua',
|
||||
},
|
||||
nav: {
|
||||
home: 'Accueil',
|
||||
products: 'Produits',
|
||||
about: 'À propos',
|
||||
},
|
||||
theme: {
|
||||
light: 'Clair',
|
||||
dark: 'Sombre',
|
||||
toggle: 'Basculer entre thème clair et sombre',
|
||||
},
|
||||
lang: {
|
||||
menuLabel: 'Langue',
|
||||
switchTo: 'Afficher en',
|
||||
},
|
||||
footer: {
|
||||
company: 'Five Questions (Beijing) Technology Co., Ltd.',
|
||||
addressLine: 'Adresse : Room A-012, 1F, North Auxiliary Building, No. 6 Tianfu North Street, Wenjing Subdistrict, Tongzhou District, Beijing 101100, Chine',
|
||||
emailLine: 'Courriel : liutingchao@fivequestions.cn',
|
||||
colAbout: 'À propos',
|
||||
linkAbout: 'L’entreprise',
|
||||
colProducts: 'Produits',
|
||||
linkBuddy: 'SmartBuddy',
|
||||
linkInput: 'SmartInput',
|
||||
linkHanzi: 'SmartHanzi',
|
||||
linkEdit: 'MassiveEdit',
|
||||
copyrightMain:
|
||||
'© 2026 Five Questions (Beijing) Technology Co., Ltd. Tous droits réservés.',
|
||||
icpNumber: '京ICP备2025143052号',
|
||||
trademarkNotice:
|
||||
'五道题® et 智花® sont des marques enregistrées de Five Questions (Beijing) Technology Co., Ltd. Les libellés en anglais sur ce site sont purement indicatifs et ne remplacent pas les marques en chinois. Toute utilisation non autorisée susceptible de créer une confusion est interdite.',
|
||||
},
|
||||
home: {
|
||||
metaTitle: 'Zhihua | Five Questions',
|
||||
metaDescription:
|
||||
'Logiciels de Five Questions : SmartBuddy, SmartInput, SmartHanzi et MassiveEdit—apprentissage, saisie et outils d’efficacité.',
|
||||
heroTitle: 'Apprentissage, saisie et outils d’efficacité',
|
||||
heroSubtitle:
|
||||
'Édités par Five Questions. Zhihua couvre l’apprentissage et la saisie du chinois ; MassiveEdit s’adresse aux très gros fichiers texte.',
|
||||
ctaProducts: 'Voir les produits',
|
||||
ctaAbout: 'À propos',
|
||||
productsTitle: 'Produits',
|
||||
productsLead:
|
||||
'Zhihua sert l’apprentissage et la saisie du chinois. MassiveEdit s’adresse à ceux qui travaillent sur d’énormes fichiers texte.',
|
||||
productBuddyName: '智花学伴',
|
||||
productBuddyEn: 'SmartBuddy',
|
||||
productBuddyBody:
|
||||
'Application élève pour le chinois international : devoirs, oral, renforcement et scènes de la vie quotidienne.',
|
||||
productBuddyCta: 'Ouvrir SmartBuddy',
|
||||
productInputName: '智花输入法',
|
||||
productInputEn: 'SmartInput',
|
||||
productInputBody:
|
||||
'Pinyin clavier complet, pinyin 9 touches, traits et écriture manuscrite—sur iOS, Android et bureau.',
|
||||
productInputCta: 'Ouvrir SmartInput',
|
||||
productHanziName: '智花识字',
|
||||
productHanziEn: 'SmartHanzi',
|
||||
productHanziBody:
|
||||
'Apprendre les caractères avec le manuel scolaire : lecture, tracé des traits et révision espacée.',
|
||||
productHanziCta: 'Ouvrir SmartHanzi',
|
||||
productEditName: 'MassiveEdit',
|
||||
productEditEn: 'Large File Editor',
|
||||
productEditBody:
|
||||
'Éditeur pour de très gros fichiers texte : ouverture rapide, défilement fluide, édition stable.',
|
||||
productEditCta: 'Ouvrir MassiveEdit',
|
||||
companyTitle: 'Five Questions',
|
||||
companyBody:
|
||||
'Five Questions (Beijing) Technology Co., Ltd. conçoit des logiciels d’apprentissage et d’efficacité. Zhihua est la marque pour le chinois ; MassiveEdit est notre éditeur de très gros fichiers.',
|
||||
ctaCompany: 'L’entreprise',
|
||||
contactTitle: 'Contact',
|
||||
contactBody: 'Pour un essai, un partenariat ou une question produit, écrivez-nous.',
|
||||
ctaContact: 'Envoyer un courriel',
|
||||
},
|
||||
about: {
|
||||
metaTitle: 'À propos | Zhihua · Five Questions',
|
||||
metaDescription:
|
||||
'Five Questions (Beijing) Technology Co., Ltd. : produits Zhihua d’apprentissage et de saisie, et des outils comme MassiveEdit.',
|
||||
heroTitle: 'À propos',
|
||||
heroLead:
|
||||
'Nous développons des logiciels d’apprentissage, de saisie et d’efficacité. Zhihua couvre le chinois ; MassiveEdit s’adresse aux très gros fichiers texte.',
|
||||
visionFigureAlt: 'Visuels de produits et de travail',
|
||||
visionTitle: 'Vision',
|
||||
visionBody:
|
||||
'Transformer ce qui est vraiment pénible dans l’apprentissage, l’expression et le travail quotidien en logiciels utilisables—clairs, stables, et faits pour durer.',
|
||||
missionTitle: 'Mission',
|
||||
missionBody:
|
||||
'Livrer des outils qui tiennent sur le terrain : finir les devoirs et parler, saisir plus naturellement, suivre un chemin pour les caractères, ouvrir et éditer d’énormes fichiers.',
|
||||
missionImgAlt: 'Ingénierie et collaboration',
|
||||
storyTitle: 'Histoire de marque',
|
||||
storyBody:
|
||||
'« Zhihua » évoque l’intelligence et la culture qui s’épanouissent ensemble. Five Questions est l’entreprise ; Zhihua, la marque pour l’apprentissage et la saisie du chinois. Linguistique, ingénierie et produit avancent sur la même ligne.',
|
||||
storyImgAlt: 'Illustration de marque',
|
||||
teamTitle: 'Équipe',
|
||||
teamBody:
|
||||
'L’équipe développe et itère : livrer, corriger, écouter. Nous ne recrutons pas d’élèves et n’exploitons pas de réseau enseignant—l’effort va au logiciel.',
|
||||
teamImgAlt: 'Collaboration d’équipe',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { DEFAULT_LANG, isLang, LANGS } from './locales';
|
||||
import type { SiteCopy } from './types';
|
||||
import { zh } from './zh';
|
||||
import { en } from './en';
|
||||
import { ja } from './ja';
|
||||
import { fr } from './fr';
|
||||
|
||||
const copies: Record<Lang, SiteCopy> = { zh, en, ja, fr };
|
||||
|
||||
export function getCopy(lang: string | undefined): SiteCopy {
|
||||
if (isLang(lang)) return copies[lang];
|
||||
return copies[DEFAULT_LANG];
|
||||
}
|
||||
|
||||
/** Astro `getStaticPaths`:默认语言生成无前缀路径,其他语言带前缀 */
|
||||
export function langStaticPaths() {
|
||||
return LANGS.map((lang) => ({
|
||||
params: { lang: lang === DEFAULT_LANG ? undefined : lang },
|
||||
}));
|
||||
}
|
||||
|
||||
export type { Lang } from './locales';
|
||||
export type { SiteCopy } from './types';
|
||||
export {
|
||||
LANGS,
|
||||
DEFAULT_LANG,
|
||||
htmlLang,
|
||||
langLabels,
|
||||
hrefFor,
|
||||
hrefForHomeHash,
|
||||
hrefForProduct,
|
||||
isLang,
|
||||
resolveLang,
|
||||
} from './locales';
|
||||
export type { RouteKey, ProductKey } from './locales';
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { SiteCopy } from './types';
|
||||
|
||||
export const ja: SiteCopy = {
|
||||
brand: {
|
||||
strong: '智花',
|
||||
suffix: '五道题',
|
||||
ariaHome: '智花ホーム',
|
||||
},
|
||||
nav: {
|
||||
home: 'ホーム',
|
||||
products: '製品',
|
||||
about: '私たちについて',
|
||||
},
|
||||
theme: {
|
||||
light: 'ライト',
|
||||
dark: 'ダーク',
|
||||
toggle: 'テーマを切り替え',
|
||||
},
|
||||
lang: {
|
||||
menuLabel: '言語',
|
||||
switchTo: '切り替え',
|
||||
},
|
||||
footer: {
|
||||
company: '五道题(北京)科技有限公司',
|
||||
addressLine: '住所:北京市通州区文景街道田府北街6号北配楼1階A-012',
|
||||
emailLine: 'メール:liutingchao@fivequestions.cn',
|
||||
colAbout: '会社情報',
|
||||
linkAbout: '会社紹介',
|
||||
colProducts: '製品',
|
||||
linkBuddy: '智花学伴',
|
||||
linkInput: '智花入力',
|
||||
linkHanzi: '智花識字',
|
||||
linkEdit: 'MassiveEdit',
|
||||
copyrightMain: '© 2026 五道题(北京)科技有限公司。無断転載を禁じます。',
|
||||
icpNumber: '京ICP备2025143052号',
|
||||
trademarkNotice:
|
||||
'「五道题®」「智花®」は五道题(北京)科技有限公司の登録商標です。関連公衆の誤認を招くおそれのある無断使用は禁止されます。',
|
||||
},
|
||||
home: {
|
||||
metaTitle: '智花|五道题',
|
||||
metaDescription:
|
||||
'五道题(北京)科技有限公司のソフトウェア:智花学伴、智花入力、智花識字、MassiveEdit。学習・入力・効率ツール。',
|
||||
heroTitle: '学習、入力、効率のためのツール',
|
||||
heroSubtitle:
|
||||
'五道题の製品です。智花は中国語の学習と入力を、MassiveEdit は巨大テキストの編集を担います。',
|
||||
ctaProducts: '製品を見る',
|
||||
ctaAbout: '私たちについて',
|
||||
productsTitle: '製品',
|
||||
productsLead: '智花は中国語の学習と入力向け。MassiveEdit は超大容量テキストを扱う人向けです。',
|
||||
productBuddyName: '智花学伴',
|
||||
productBuddyEn: 'SmartBuddy',
|
||||
productBuddyBody:
|
||||
'国際中国語学習向けの学習者アプリ。教員が出した課題、音声評価、強化練習、生活場面の会話に対応します。',
|
||||
productBuddyCta: '智花学伴へ',
|
||||
productInputName: '智花入力',
|
||||
productInputEn: 'SmartInput',
|
||||
productInputBody:
|
||||
'フルキー拼音、九鍵拼音、筆画、手書きに対応。iOS、Android、デスクトップで利用できます。',
|
||||
productInputCta: '智花入力へ',
|
||||
productHanziName: '智花識字',
|
||||
productHanziEn: 'SmartHanzi',
|
||||
productHanziBody: '小学校の教材に沿って漢字を読む・書く。筆順のなぞりと復習を一本の道筋にします。',
|
||||
productHanziCta: '智花識字へ',
|
||||
productEditName: 'MassiveEdit',
|
||||
productEditEn: 'Large File Editor',
|
||||
productEditBody: '超大容量テキスト向けのエディタ。起動、スクロール、安定した編集を重視します。',
|
||||
productEditCta: 'MassiveEdit へ',
|
||||
companyTitle: '五道题',
|
||||
companyBody:
|
||||
'五道题(北京)科技有限公司は、使える学習・効率ソフトウェアを作ります。智花は中国語の学習と入力のブランド、MassiveEdit は同一主体の大容量テキストエディタです。',
|
||||
ctaCompany: '会社を見る',
|
||||
contactTitle: 'お問い合わせ',
|
||||
contactBody: '試用、提携、製品に関するご質問はメールでご連絡ください。',
|
||||
ctaContact: 'メールを送る',
|
||||
},
|
||||
about: {
|
||||
metaTitle: '私たちについて|智花 · 五道题',
|
||||
metaDescription:
|
||||
'五道题(北京)科技有限公司:智花の学習・入力製品と、MassiveEdit などの効率ツール。',
|
||||
heroTitle: '私たちについて',
|
||||
heroLead:
|
||||
'学習、入力、効率のためのソフトウェアを開発しています。智花は中国語の学習と入力、MassiveEdit は巨大テキストを扱う人のための製品です。',
|
||||
visionFigureAlt: '製品と制作のビジュアル',
|
||||
visionTitle: 'ビジョン',
|
||||
visionBody:
|
||||
'学び、伝え、日々の仕事で本当に手間のかかる部分を、使えるソフトウェアにします。分かりやすく、安定して、長く磨いていけるものを目指します。',
|
||||
missionTitle: 'ミッション',
|
||||
missionBody:
|
||||
'現場で使える製品を出し続けること。課題を終え、口を開け、入力が自然になり、識字に道筋があり、巨大ファイルも開いて編集できること。',
|
||||
missionImgAlt: '開発と協働',
|
||||
storyTitle: 'ブランドストーリー',
|
||||
storyBody:
|
||||
'「智花」は知恵と文化がともに花開くイメージです。五道题は会社主体、智花は中国語の学習と入力の製品ブランド。言語学、エンジニアリング、製品改善を一本の線で進めています。',
|
||||
storyImgAlt: 'ブランドアートワーク',
|
||||
teamTitle: 'チーム',
|
||||
teamBody:
|
||||
'チームの仕事は製品開発と継続的な改善です。機能を出し、不具合を直し、フィードバックを聞く。生徒募集や教員フランチャイズは行いません。',
|
||||
teamImgAlt: 'チームコラボレーション',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
export type Lang = 'zh' | 'en' | 'ja' | 'fr';
|
||||
|
||||
export const LANGS: Lang[] = ['zh', 'en', 'ja', 'fr'];
|
||||
|
||||
export const DEFAULT_LANG: Lang = 'zh';
|
||||
|
||||
export const htmlLang: Record<Lang, string> = {
|
||||
zh: 'zh-CN',
|
||||
en: 'en',
|
||||
ja: 'ja',
|
||||
fr: 'fr',
|
||||
};
|
||||
|
||||
export const langLabels: Record<Lang, string> = {
|
||||
zh: '中文',
|
||||
en: 'English',
|
||||
ja: '日本語',
|
||||
fr: 'Français',
|
||||
};
|
||||
|
||||
export function isLang(value: string | undefined): value is Lang {
|
||||
return value !== undefined && (LANGS as string[]).includes(value);
|
||||
}
|
||||
|
||||
/** 站内路径(不含语言前缀),首页为空字符串 */
|
||||
export type RouteKey = '' | 'about';
|
||||
|
||||
export type ProductKey = 'smartbuddy' | 'smartinput' | 'smarthanzi' | 'massiveedit';
|
||||
|
||||
export function hrefFor(lang: Lang, segment: RouteKey): string {
|
||||
const prefix = lang === DEFAULT_LANG ? '' : `/${lang}`;
|
||||
if (!segment) return prefix || '/';
|
||||
return `${prefix}/${segment}`;
|
||||
}
|
||||
|
||||
export function hrefForHomeHash(lang: Lang, hash: string): string {
|
||||
const home = hrefFor(lang, '');
|
||||
if (home === '/') return `/#${hash}`;
|
||||
return `${home}#${hash}`;
|
||||
}
|
||||
|
||||
export function hrefForProduct(key: ProductKey, lang: Lang): string {
|
||||
if (key === 'massiveedit' && (lang === 'en' || lang === 'fr')) {
|
||||
return '/massiveedit/en/';
|
||||
}
|
||||
return `/${key}/`;
|
||||
}
|
||||
|
||||
/** 从 [...lang] rest 参数解析语言,undefined 时返回默认语言 */
|
||||
export function resolveLang(param: string | undefined): Lang {
|
||||
return isLang(param) ? param : DEFAULT_LANG;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
export interface SiteCopy {
|
||||
brand: {
|
||||
strong: string;
|
||||
suffix: string;
|
||||
ariaHome: string;
|
||||
};
|
||||
nav: {
|
||||
home: string;
|
||||
products: string;
|
||||
about: string;
|
||||
};
|
||||
theme: {
|
||||
light: string;
|
||||
dark: string;
|
||||
toggle: string;
|
||||
};
|
||||
lang: {
|
||||
menuLabel: string;
|
||||
switchTo: string;
|
||||
};
|
||||
footer: {
|
||||
company: string;
|
||||
addressLine: string;
|
||||
emailLine: string;
|
||||
colAbout: string;
|
||||
linkAbout: string;
|
||||
colProducts: string;
|
||||
linkBuddy: string;
|
||||
linkInput: string;
|
||||
linkHanzi: string;
|
||||
linkEdit: string;
|
||||
copyrightMain: string;
|
||||
icpNumber: string;
|
||||
trademarkNotice: string;
|
||||
};
|
||||
home: {
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
heroTitle: string;
|
||||
heroSubtitle: string;
|
||||
ctaProducts: string;
|
||||
ctaAbout: string;
|
||||
productsTitle: string;
|
||||
productsLead: string;
|
||||
productBuddyName: string;
|
||||
productBuddyEn: string;
|
||||
productBuddyBody: string;
|
||||
productBuddyCta: string;
|
||||
productInputName: string;
|
||||
productInputEn: string;
|
||||
productInputBody: string;
|
||||
productInputCta: string;
|
||||
productHanziName: string;
|
||||
productHanziEn: string;
|
||||
productHanziBody: string;
|
||||
productHanziCta: string;
|
||||
productEditName: string;
|
||||
productEditEn: string;
|
||||
productEditBody: string;
|
||||
productEditCta: string;
|
||||
companyTitle: string;
|
||||
companyBody: string;
|
||||
ctaCompany: string;
|
||||
contactTitle: string;
|
||||
contactBody: string;
|
||||
ctaContact: string;
|
||||
};
|
||||
about: {
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
heroTitle: string;
|
||||
heroLead: string;
|
||||
visionFigureAlt: string;
|
||||
visionTitle: string;
|
||||
visionBody: string;
|
||||
missionTitle: string;
|
||||
missionBody: string;
|
||||
missionImgAlt: string;
|
||||
storyTitle: string;
|
||||
storyBody: string;
|
||||
storyImgAlt: string;
|
||||
teamTitle: string;
|
||||
teamBody: string;
|
||||
teamImgAlt: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { SiteCopy } from './types';
|
||||
|
||||
export const zh: SiteCopy = {
|
||||
brand: {
|
||||
strong: '智花',
|
||||
suffix: '五道题',
|
||||
ariaHome: '智花首页',
|
||||
},
|
||||
nav: {
|
||||
home: '首页',
|
||||
products: '产品',
|
||||
about: '关于我们',
|
||||
},
|
||||
theme: {
|
||||
light: '浅色',
|
||||
dark: '深色',
|
||||
toggle: '切换浅色或深色主题',
|
||||
},
|
||||
lang: {
|
||||
menuLabel: '语言',
|
||||
switchTo: '切换到',
|
||||
},
|
||||
footer: {
|
||||
company: '五道题(北京)科技有限公司',
|
||||
addressLine: '地址:北京市通州区文景街道田府北街6号北配楼1层A-012',
|
||||
emailLine: '邮箱:liutingchao@fivequestions.cn',
|
||||
colAbout: '关于我们',
|
||||
linkAbout: '公司介绍',
|
||||
colProducts: '产品',
|
||||
linkBuddy: '智花学伴',
|
||||
linkInput: '智花输入法',
|
||||
linkHanzi: '智花识字',
|
||||
linkEdit: 'MassiveEdit',
|
||||
copyrightMain: '© 2026 五道题(北京)科技有限公司 保留所有权利',
|
||||
icpNumber: '京ICP备2025143052号',
|
||||
trademarkNotice:
|
||||
'「五道题®」「智花®」为五道题(北京)科技有限公司注册商标;未经许可,他人不得以可能造成相关公众混淆的方式使用。',
|
||||
},
|
||||
home: {
|
||||
metaTitle: '智花|五道题',
|
||||
metaDescription:
|
||||
'五道题(北京)科技有限公司出品:智花学伴、智花输入法、智花识字与 MassiveEdit,覆盖学习、输入与效率工具。',
|
||||
heroTitle: '学习、输入与效率工具',
|
||||
heroSubtitle: '五道题出品。智花覆盖中文学习与输入,同时提供面向大文件的效率工具。',
|
||||
ctaProducts: '查看产品',
|
||||
ctaAbout: '关于我们',
|
||||
productsTitle: '我们的产品',
|
||||
productsLead: '智花面向中文学习与输入;MassiveEdit 服务需要稳定处理超大文本的人。',
|
||||
productBuddyName: '智花学伴',
|
||||
productBuddyEn: 'SmartBuddy',
|
||||
productBuddyBody: '面向国际中文学习的学生端:完成教师布置的作业,用语音评测、强化练习和生活场景把中文说出来。',
|
||||
productBuddyCta: '进入智花学伴',
|
||||
productInputName: '智花输入法',
|
||||
productInputEn: 'SmartInput',
|
||||
productInputBody: '支持全键盘拼音、九键拼音、笔画与手写,覆盖 iOS、Android 与桌面平台。',
|
||||
productInputCta: '进入智花输入法',
|
||||
productHanziName: '智花识字',
|
||||
productHanziEn: 'SmartHanzi',
|
||||
productHanziBody: '跟着小学教材认字写字,认读、描红笔顺与智能复习连成一条路径。',
|
||||
productHanziCta: '进入智花识字',
|
||||
productEditName: 'MassiveEdit',
|
||||
productEditEn: 'Large File Editor',
|
||||
productEditBody: '面向超大文本文件的编辑器,强调打开速度、滚动流畅与稳定编辑。',
|
||||
productEditCta: '进入 MassiveEdit',
|
||||
companyTitle: '五道题',
|
||||
companyBody:
|
||||
'五道题(北京)科技有限公司做可落地的学习与效率软件。智花是面向中文学习与输入的产品品牌;MassiveEdit 是同一主体下的大文件文本编辑器。',
|
||||
ctaCompany: '了解公司',
|
||||
contactTitle: '联系我们',
|
||||
contactBody: '试用、合作或产品问题,欢迎来信。',
|
||||
ctaContact: '发送邮件',
|
||||
},
|
||||
about: {
|
||||
metaTitle: '关于我们|智花 · 五道题',
|
||||
metaDescription:
|
||||
'了解五道题(北京)科技有限公司:智花学习与输入产品,以及 MassiveEdit 等效率工具。',
|
||||
heroTitle: '关于我们',
|
||||
heroLead:
|
||||
'五道题(北京)科技有限公司开发学习、输入与效率类软件。智花覆盖中文学习与输入;MassiveEdit 面向需要处理超大文本的用户。',
|
||||
visionFigureAlt: '产品与工作场景',
|
||||
visionTitle: '企业愿景',
|
||||
visionBody:
|
||||
'把学习、表达与日常工作里真正费力的环节做成可用的软件。我们希望工具足够清楚、稳定,也愿意长期打磨细节。',
|
||||
missionTitle: '我们的使命',
|
||||
missionBody:
|
||||
'持续交付可落地的产品:学生能完成作业与开口练习,输入更自然,识字写字有路径可循,大文件也能流畅打开与编辑。',
|
||||
missionImgAlt: '研发与协作',
|
||||
storyTitle: '品牌故事',
|
||||
storyBody:
|
||||
'「智花」寓意智慧与文化的共同绽放。五道题是公司主体,「智花」是面向中文学习与输入的产品品牌。我们把语言学、工程与产品迭代放在同一条工作线上。',
|
||||
storyImgAlt: '品牌故事插画',
|
||||
teamTitle: '团队',
|
||||
teamBody:
|
||||
'团队服务产品研发与持续迭代:做功能、修问题、听反馈。我们不招生、不运营师资加盟,把精力放在软件本身。',
|
||||
teamImgAlt: '团队合作',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import '../styles/global.css';
|
||||
import { DEFAULT_LANG, getCopy, htmlLang, hrefFor, LANGS, type Lang, type RouteKey } from '../i18n';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
currentSegment: RouteKey;
|
||||
lang: Lang;
|
||||
}
|
||||
|
||||
const { title, description, currentSegment, lang } = Astro.props;
|
||||
const copy = getCopy(lang);
|
||||
const htmlLangAttr = htmlLang[lang];
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang={htmlLangAttr} data-theme="light" class={`site-lang-${lang}`}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/svg+xml" href="/images/logo.svg" />
|
||||
{LANGS.map((l) => (
|
||||
<link rel="alternate" hreflang={htmlLang[l]} href={hrefFor(l, currentSegment)} />
|
||||
))}
|
||||
<link rel="alternate" hreflang="x-default" href={hrefFor(DEFAULT_LANG, currentSegment)} />
|
||||
<link rel="stylesheet" href="/fonts/manrope.css" />
|
||||
<script is:inline>
|
||||
(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={lang} copy={copy} currentSegment={currentSegment} />
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<Footer lang={lang} copy={copy} />
|
||||
</div>
|
||||
<script>
|
||||
const toggle = document.querySelector('[data-menu-toggle]');
|
||||
const menu = document.querySelector('[data-mobile-menu]');
|
||||
if (toggle && menu) {
|
||||
toggle.addEventListener('click', () => {
|
||||
const isOpen = menu.classList.toggle('is-open');
|
||||
toggle.setAttribute('aria-expanded', String(isOpen));
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const cur =
|
||||
document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
||||
const next = cur === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
try {
|
||||
localStorage.setItem('theme', next);
|
||||
} catch (_) {}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getCopy, hrefForProduct, langStaticPaths, resolveLang } from '../../i18n';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return langStaticPaths();
|
||||
}
|
||||
|
||||
const locale = resolveLang(Astro.params.lang);
|
||||
const L = getCopy(locale);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={L.about.metaTitle}
|
||||
description={L.about.metaDescription}
|
||||
currentSegment="about"
|
||||
lang={locale}
|
||||
>
|
||||
<section class="hero home-hero about-hero">
|
||||
<div class="container">
|
||||
<p class="home-kicker">{L.brand.strong} · {L.brand.suffix}</p>
|
||||
<h1 class="hero-title">{L.about.heroTitle}</h1>
|
||||
<p class="home-lead">{L.about.heroLead}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section compact">
|
||||
<div class="container about-stack">
|
||||
<article class="feature-card">
|
||||
<h2>{L.about.visionTitle}</h2>
|
||||
<p>{L.about.visionBody}</p>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<h2>{L.about.missionTitle}</h2>
|
||||
<p>{L.about.missionBody}</p>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<h2>{L.about.storyTitle}</h2>
|
||||
<p>{L.about.storyBody}</p>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<h2>{L.about.teamTitle}</h2>
|
||||
<p>{L.about.teamBody}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section compact">
|
||||
<div class="container">
|
||||
<div class="about-products">
|
||||
<a href={hrefForProduct('smartbuddy', locale)}>{L.footer.linkBuddy}</a>
|
||||
<a href={hrefForProduct('smartinput', locale)}>{L.footer.linkInput}</a>
|
||||
<a href={hrefForProduct('smarthanzi', locale)}>{L.footer.linkHanzi}</a>
|
||||
<a href={hrefForProduct('massiveedit', locale)}>{L.footer.linkEdit}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getCopy, hrefFor, hrefForHomeHash, hrefForProduct, langStaticPaths, resolveLang } from '../../i18n';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return langStaticPaths();
|
||||
}
|
||||
|
||||
const locale = resolveLang(Astro.params.lang);
|
||||
const L = getCopy(locale);
|
||||
|
||||
const products = [
|
||||
{
|
||||
key: 'smartbuddy' as const,
|
||||
name: L.home.productBuddyName,
|
||||
en: L.home.productBuddyEn,
|
||||
body: L.home.productBuddyBody,
|
||||
cta: L.home.productBuddyCta,
|
||||
},
|
||||
{
|
||||
key: 'smartinput' as const,
|
||||
name: L.home.productInputName,
|
||||
en: L.home.productInputEn,
|
||||
body: L.home.productInputBody,
|
||||
cta: L.home.productInputCta,
|
||||
},
|
||||
{
|
||||
key: 'smarthanzi' as const,
|
||||
name: L.home.productHanziName,
|
||||
en: L.home.productHanziEn,
|
||||
body: L.home.productHanziBody,
|
||||
cta: L.home.productHanziCta,
|
||||
},
|
||||
{
|
||||
key: 'massiveedit' as const,
|
||||
name: L.home.productEditName,
|
||||
en: L.home.productEditEn,
|
||||
body: L.home.productEditBody,
|
||||
cta: L.home.productEditCta,
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={L.home.metaTitle}
|
||||
description={L.home.metaDescription}
|
||||
currentSegment=""
|
||||
lang={locale}
|
||||
>
|
||||
<section class="hero home-hero">
|
||||
<div class="container">
|
||||
<p class="home-kicker">{L.brand.strong} · {L.brand.suffix}</p>
|
||||
<h1 class="hero-title">{L.home.heroTitle}</h1>
|
||||
<p class="home-lead">{L.home.heroSubtitle}</p>
|
||||
<div class="hero-actions">
|
||||
<a class="btn btn-primary" href={hrefForHomeHash(locale, 'products')}>{L.home.ctaProducts}</a>
|
||||
<a class="btn btn-outline" href={hrefFor(locale, 'about')}>{L.home.ctaAbout}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section compact" id="products">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">{L.home.productsTitle}</h2>
|
||||
<p class="section-copy">{L.home.productsLead}</p>
|
||||
</div>
|
||||
<div class="product-grid">
|
||||
{products.map((product) => (
|
||||
<article class="feature-card product-card">
|
||||
<p class="product-kicker">{product.en}</p>
|
||||
<h3>{product.name}</h3>
|
||||
<p>{product.body}</p>
|
||||
<a class="mini-link" href={hrefForProduct(product.key, locale)}>{product.cta}</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section compact" id="company">
|
||||
<div class="container grid-two company-contact">
|
||||
<article class="feature-card">
|
||||
<h3>{L.home.companyTitle}</h3>
|
||||
<p>{L.home.companyBody}</p>
|
||||
<a class="mini-link" href={hrefFor(locale, 'about')}>{L.home.ctaCompany}</a>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<h3>{L.home.contactTitle}</h3>
|
||||
<p>{L.home.contactBody}</p>
|
||||
<a class="mini-link" href="mailto:liutingchao@fivequestions.cn">{L.home.ctaContact}</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
After Width: | Height: | Size: 3.1 MiB |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||