Add Spanish and Arabic versions with a language switch
- English stays at /, Spanish at /es/ and Arabic at /ar/ (right to left) - EN · ES · AR switch in the header and mobile menu - Pages are written in English; the build translates them through src/i18n/es.json and src/i18n/ar.json, including browser-script strings - rtl.css mirrors the layout and loads Arabic fonts on /ar/ pages - hreflang alternates on every page and a multilingual sitemap - scripts/i18n-extract.mjs lists strings that still need translating Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,8 @@ import { mkdir, rm, writeFile, cp, readFile } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { page, eyebrow, btn } from './src/layout.mjs';
|
||||
import { page, eyebrow, btn, SITE } from './src/layout.mjs';
|
||||
import { LOCALES, DEFAULT_LOCALE, localize, translatePage } from './src/i18n.mjs';
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
const out = join(root, 'dist');
|
||||
@@ -15,18 +16,35 @@ await cp(join(root, 'static'), out, { recursive: true });
|
||||
|
||||
// Fingerprint CSS/JS so a new deploy never reuses a stale cached copy.
|
||||
const hashOf = async (f) => createHash('sha256').update(await readFile(join(root, 'static', f))).digest('hex').slice(0, 10);
|
||||
const assetVersions = Object.fromEntries(await Promise.all(['styles.css', 'site.js', 'home.css', 'home.js', 'fonts.css', 'book.js'].map(async (f) => ['/assets/' + f, await hashOf('assets/' + f)])));
|
||||
const assetVersions = Object.fromEntries(await Promise.all(['styles.css', 'site.js', 'home.css', 'home.js', 'fonts.css', 'book.js', 'rtl.css'].map(async (f) => ['/assets/' + f, await hashOf('assets/' + f)])));
|
||||
const fingerprint = (html) => Object.entries(assetVersions).reduce((h, [u, v]) => h.replaceAll(`"${u}"`, `"${u}?v=${v}"`), html);
|
||||
|
||||
// Spanish and Arabic pages are the English pages with their text swapped through src/i18n/<locale>.json.
|
||||
const dicts = Object.fromEntries(await Promise.all(LOCALES.filter((l) => l !== DEFAULT_LOCALE).map(async (l) => [l, JSON.parse(await readFile(join(root, 'src/i18n', l + '.json'), 'utf8'))])));
|
||||
const missing = {};
|
||||
|
||||
const urls = [];
|
||||
for (const name of pages) {
|
||||
const def = (await import(`./src/pages/${name}.mjs`)).default;
|
||||
const file = join(out, def.path, 'index.html');
|
||||
await mkdir(dirname(file), { recursive: true });
|
||||
await writeFile(file, fingerprint(page(def)));
|
||||
for (const locale of LOCALES) {
|
||||
let html = page(def, locale);
|
||||
if (locale !== DEFAULT_LOCALE) {
|
||||
const t = translatePage(html, locale, dicts[locale]);
|
||||
html = t.html;
|
||||
for (const s of t.missing) (missing[locale] ??= new Set()).add(s);
|
||||
}
|
||||
const file = join(out, localize(def.path, locale), 'index.html');
|
||||
await mkdir(dirname(file), { recursive: true });
|
||||
await writeFile(file, fingerprint(html));
|
||||
}
|
||||
urls.push(def.path);
|
||||
console.log('built', def.path);
|
||||
}
|
||||
for (const [l, set] of Object.entries(missing)) {
|
||||
console.warn(`\n${l}: ${set.size} strings without translation (shown in English):`);
|
||||
for (const s of set) console.warn(' ' + JSON.stringify(s));
|
||||
}
|
||||
if (process.argv.includes('--strict') && Object.keys(missing).length) process.exit(1);
|
||||
|
||||
await writeFile(join(out, '404.html'), fingerprint(page({
|
||||
path: '/404/',
|
||||
@@ -37,8 +55,8 @@ await writeFile(join(out, '404.html'), fingerprint(page({
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
await writeFile(join(out, 'sitemap.xml'), `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${urls.map((u) => ` <url><loc>https://dylanmazzei.com${u}</loc><lastmod>${today}</lastmod></url>`).join('\n')}
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
${urls.flatMap((u) => LOCALES.map((l) => ` <url><loc>${SITE.url}${localize(u, l)}</loc><lastmod>${today}</lastmod>${LOCALES.map((a) => `<xhtml:link rel="alternate" hreflang="${a}" href="${SITE.url}${localize(u, a)}"/>`).join('')}</url>`)).join('\n')}
|
||||
</urlset>
|
||||
`);
|
||||
await writeFile(join(out, 'robots.txt'), 'User-agent: *\nAllow: /\nSitemap: https://dylanmazzei.com/sitemap.xml\n');
|
||||
|
||||
Reference in New Issue
Block a user