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:
+27
-9
@@ -1,4 +1,5 @@
|
||||
// Shared layout, icons and small HTML helpers for every page.
|
||||
import { LOCALES, LOCALE_NAMES, LOCALE_SHORT, OG_LOCALE, dirOf, localize } from './i18n.mjs';
|
||||
|
||||
export const SITE = {
|
||||
url: 'https://dylanmazzei.com',
|
||||
@@ -61,7 +62,9 @@ const SMALL = new Set(['arrow', 'arrow-ur', 'check', 'plus', 'chev', 'brace']);
|
||||
export function icon(name, cls = '') {
|
||||
const vb = SMALL.has(name) ? '0 0 16 16' : name === 'menu' || name === 'close' ? '0 0 18 18' : '0 0 24 24';
|
||||
const sw = SMALL.has(name) || name === 'menu' || name === 'close' ? '' : ' stroke-width="1.75"';
|
||||
return `<svg class="${cls}" viewBox="${vb}" fill="none" stroke="currentColor"${sw} stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[name]}</svg>`;
|
||||
// Arrows point the other way on right-to-left pages (rtl.css).
|
||||
const c = name.startsWith('arrow') ? (cls ? cls + ' flip' : 'flip') : cls;
|
||||
return `<svg class="${c}" viewBox="${vb}" fill="none" stroke="currentColor"${sw} stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[name]}</svg>`;
|
||||
}
|
||||
|
||||
// Dylan Mazzei monogram: italic "D" (inherits text color) and the sky-blue blade.
|
||||
@@ -111,7 +114,16 @@ const NAV = [
|
||||
{ label: 'Press', href: '/press/' },
|
||||
];
|
||||
|
||||
function nav(path) {
|
||||
// EN · ES · AR. Plain links: each language is its own static page.
|
||||
function langSwitch(path, locale) {
|
||||
return `<nav class="lang-switch" aria-label="Language">${LOCALES.map((l) =>
|
||||
l === locale
|
||||
? `<span aria-current="true" lang="${l}" title="${LOCALE_NAMES[l]}">${LOCALE_SHORT[l]}</span>`
|
||||
: `<a href="${localize(path, l)}" hreflang="${l}" lang="${l}" title="${LOCALE_NAMES[l]}">${LOCALE_SHORT[l]}</a>`
|
||||
).join('')}</nav>`;
|
||||
}
|
||||
|
||||
function nav(path, locale) {
|
||||
const isActive = (href) => (href === '/' ? path === '/' : path.startsWith(href));
|
||||
const desktop = NAV.map((item) => {
|
||||
if (!item.children) {
|
||||
@@ -136,6 +148,7 @@ function nav(path) {
|
||||
<a class="brand" href="/" aria-label="Dylan Mazzei, home">${logo()}</a>
|
||||
<nav aria-label="Primary"><ul class="nav-links">${desktop}</ul></nav>
|
||||
<div class="nav-cta">
|
||||
${langSwitch(path, locale)}
|
||||
<button class="theme-btn" id="theme-btn" type="button" aria-label="Switch to dark mode">${icon('moon', 'moon')}${icon('sun', 'sun')}</button>
|
||||
<a class="btn btn-primary btn-sm" href="/contact/">START A CONVERSATION<span class="go">${icon('arrow')}</span></a>
|
||||
<button class="menu-btn" id="menu-btn" type="button" aria-expanded="false" aria-controls="mobile-menu" aria-label="Open menu">${icon('menu')}</button>
|
||||
@@ -143,6 +156,7 @@ function nav(path) {
|
||||
</div>
|
||||
<div class="wrap mobile-menu" id="mobile-menu" hidden>
|
||||
<ul>${mobile}</ul>
|
||||
${langSwitch(path, locale)}
|
||||
<a class="btn btn-primary" href="/contact/">START A CONVERSATION<span class="go">${icon('arrow')}</span></a>
|
||||
</div>
|
||||
</header>`;
|
||||
@@ -193,15 +207,17 @@ function footer() {
|
||||
<span class="eyebrow">WHERE I WORK</span>
|
||||
<p class="places">Texas<i>•</i>Florida<i>•</i>New York<i>•</i>Dubai</p>
|
||||
</div>
|
||||
<small>© ${new Date().getFullYear()} Dylan Mazzei</small>
|
||||
<small>© <span>${new Date().getFullYear()}</span> Dylan Mazzei</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot-word" aria-hidden="true">DYLAN MAZZEI</div>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
export function page({ path, title, description, body, schema = [], head = '', scripts = '', noindex = false }) {
|
||||
const canonical = SITE.url + path;
|
||||
export function page({ path, title, description, body, schema = [], head = '', scripts = '', noindex = false }, locale = 'en') {
|
||||
const canonical = SITE.url + localize(path, locale);
|
||||
const alternates = noindex ? '' : [...LOCALES.map((l) => [l, l]), ['x-default', 'en']]
|
||||
.map(([hl, l]) => `\n<link rel="alternate" hreflang="${hl}" href="${SITE.url + localize(path, l)}">`).join('');
|
||||
const person = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
@@ -215,17 +231,18 @@ export function page({ path, title, description, body, schema = [], head = '', s
|
||||
};
|
||||
const ld = [person, ...schema].map((s) => `<script type="application/ld+json">${JSON.stringify(s)}</script>`).join('\n');
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="${locale}" dir="${dirOf(locale)}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>${title}</title>
|
||||
<meta name="description" content="${description}">
|
||||
<link rel="canonical" href="${canonical}">${noindex ? '\n<meta name="robots" content="noindex">' : ''}
|
||||
<link rel="canonical" href="${canonical}">${alternates}${noindex ? '\n<meta name="robots" content="noindex">' : ''}
|
||||
<meta name="theme-color" content="#04060c">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Dylan Mazzei">
|
||||
<meta property="og:locale" content="${OG_LOCALE[locale]}">
|
||||
<meta property="og:title" content="${title}">
|
||||
<meta property="og:description" content="${description}">
|
||||
<meta property="og:url" content="${canonical}">
|
||||
@@ -240,15 +257,16 @@ export function page({ path, title, description, body, schema = [], head = '', s
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&family=Unbounded:wght@700&display=swap">
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="/assets/fonts/inter-tight-latin-500-normal.woff2">
|
||||
<link rel="stylesheet" href="/assets/fonts.css">
|
||||
<link rel="stylesheet" href="/assets/styles.css">
|
||||
<link rel="stylesheet" href="/assets/styles.css">${locale === 'ar' ? '\n<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Kufi+Arabic:wght@600;700&family=Noto+Sans+Arabic:wght@400;500;600;700&display=swap">\n<link rel="stylesheet" href="/assets/rtl.css">' : ''}
|
||||
${head}
|
||||
<script>(function(){var r=document.documentElement;try{var t=localStorage.getItem('theme');if(t==='dark'||t==='light')r.setAttribute('data-theme',t)}catch(e){}if(!r.hasAttribute('data-theme'))r.setAttribute('data-theme','dark')})();document.documentElement.classList.add('js');setTimeout(function(){if(!window.__site)document.documentElement.classList.remove('js')},2500)</script>
|
||||
<script>window.__i18n={}</script>
|
||||
${ld}
|
||||
</head>
|
||||
<body>
|
||||
<div class="bgfx" aria-hidden="true"></div>
|
||||
<div class="progress" id="progress" aria-hidden="true"></div>
|
||||
${nav(path)}
|
||||
${nav(path, locale)}
|
||||
<main id="main">
|
||||
${body}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user