// 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/.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', 'Spirits', 'Education Technology', 'Cybersecurity', 'E-Commerce', 'MARKETING & AI SEARCH', 'AI SOFTWARE', 'SPIRITS', 'EDUCATION TECHNOLOGY', 'CYBERSECURITY', 'E-COMMERCE', 'Leading teams through a constantly changing search landscape.', 'Building around new opportunities created by AI.', 'Taking a consumer brand from idea to market.', '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 = /|<(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(' 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(/]*>/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 's inline scripts and JSON-LD alone; translate the rest. let out = mapStrings(html, tr); out = localizeLinks(out, locale); out = out.replace('', ``); return { html: out, missing: [...missing] }; }