Files
dylanmazzei-website/src/i18n.mjs
T
Alejandro Veras d99d86a269 Remove Spirits from the home page
Drops the Spirits venture from the hero icons, the ventures tabs and its 3D bottle,
and from the home copy and meta description, with matching ES/AR translations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:30:05 -04:00

98 lines
4.7 KiB
JavaScript

// Languages. English is served from the root (/about/); Spanish and Arabic carry a prefix (/es/about/).
// Pages are written in English; each translated page is the English HTML with its text swapped
// through the dictionaries in src/i18n/<locale>.json (English text → translation).
export const LOCALES = ['en', 'es', 'ar'];
export const DEFAULT_LOCALE = 'en';
export const LOCALE_NAMES = { en: 'English', es: 'Español', ar: 'العربية' };
export const LOCALE_SHORT = { en: 'EN', es: 'ES', ar: 'AR' };
export const OG_LOCALE = { en: 'en_US', es: 'es_ES', ar: 'ar_AE' };
export const dirOf = (locale) => (locale === 'ar' ? 'rtl' : 'ltr');
/** Prefix an internal path with the locale. */
export const localize = (path, locale) => (locale === DEFAULT_LOCALE ? path : `/${locale}${path}`);
// Strings set by the browser scripts. Each page gets the ones for its language in window.__i18n.
export const JS_STRINGS = [
'Switch to light mode', 'Switch to dark mode', 'Open menu', 'Close menu', 'Copied',
'Your email app should open with the message ready to send.',
'AI Software', 'Marketing & AI Search', 'Education Technology', 'Cybersecurity', 'E-Commerce',
'MARKETING & AI SEARCH', 'AI SOFTWARE', 'EDUCATION TECHNOLOGY', 'CYBERSECURITY', 'E-COMMERCE',
'Leading teams through a constantly changing search landscape.',
'Building around new opportunities created by AI.',
'Creating technology designed for real classrooms.',
'Solving security challenges for growing businesses.',
'Turning product ideas into businesses built around real customers.',
];
// Tags that stay inside a translatable segment; any other tag ends it.
const INLINE = new Set(['b', 'strong', 'i', 'em', 'u', 'br', 'sup', 'sub', 'mark', 'abbr', 'wbr']);
const ATTRS = ['alt', 'aria-label', 'title', 'placeholder'];
const META = new Set(['description', 'og:title', 'og:description', 'twitter:title', 'twitter:description']);
const TOKEN = /<!--[\s\S]*?-->|<(script|style|svg)\b[\s\S]*?<\/\1>|<[^>]+>|[^<]+/gi;
export const norm = (s) => s.replace(/\s+/g, ' ').trim();
const hasWords = (s) => /\p{L}/u.test(s.replace(/<[^>]*>|&[a-z#0-9]+;/gi, ''));
const tagName = (t) => (t.match(/^<\/?([a-z0-9-]+)/i) || [])[1]?.toLowerCase();
/**
* Walk the HTML and call `fn(text)` for every translatable string: runs of text with inline
* markup, translatable attributes and meta descriptions. `fn` returns the replacement.
*/
export function mapStrings(html, fn) {
const out = [];
let seg = [];
const flush = () => {
if (!seg.length) return;
const raw = seg.join('');
seg = [];
if (!hasWords(raw)) return out.push(raw);
const lead = raw.match(/^\s*/)[0], trail = raw.slice(lead.length).match(/\s*$/)[0];
out.push(lead + fn(norm(raw)) + trail);
};
for (const [tok, block] of html.matchAll(TOKEN)) {
if (block || tok.startsWith('<!')) { flush(); out.push(tok); continue; }
if (tok[0] !== '<') { seg.push(tok); continue; }
const name = tagName(tok);
if (INLINE.has(name) && !/\b(class|id|data-)/.test(tok)) { seg.push(tok); continue; }
flush();
out.push(mapAttrs(tok, name, fn));
}
flush();
return out.join('');
}
function mapAttrs(tag, name, fn) {
if (/\slang="/.test(tag)) return tag; // the language switch names each language in its own script
let t = tag.replace(new RegExp(`\\s(${ATTRS.join('|')})="([^"]*)"`, 'g'), (m, a, v) =>
hasWords(v) ? ` ${a}="${fn(norm(v))}"` : m);
if (name === 'meta') {
const key = (t.match(/\s(?:name|property)="([^"]+)"/) || [])[1];
if (META.has(key)) t = t.replace(/\scontent="([^"]*)"/, (m, v) => ` content="${fn(norm(v))}"`);
}
return t;
}
/** Point internal page links at the locale's copy. Links marked with hreflang are left alone. */
export function localizeLinks(html, locale) {
if (locale === DEFAULT_LOCALE) return html;
return html.replace(/<a\b[^>]*>/g, (tag) =>
/\shreflang=/.test(tag) ? tag : tag.replace(/\shref="(\/(?![^"#?]*\.[a-z0-9]+(?:[#?"]))[^"]*)"/i, (m, p) => ` href="${localize(p, locale)}"`));
}
/** Translate a rendered English page. Returns the HTML and the strings with no translation. */
export function translatePage(html, locale, dict) {
const missing = new Set();
const tr = (s) => {
if (Object.hasOwn(dict, s)) return dict[s];
missing.add(s);
return s;
};
const jsDict = Object.fromEntries(JS_STRINGS.filter((s) => Object.hasOwn(dict, s)).map((s) => [s, dict[s]]));
// Leave <head>'s inline scripts and JSON-LD alone; translate the rest.
let out = mapStrings(html, tr);
out = localizeLinks(out, locale);
out = out.replace('<script>window.__i18n={}</script>', `<script>window.__i18n=${JSON.stringify(jsDict).replace(/</g, '\\u003c')}</script>`);
return { html: out, missing: [...missing] };
}