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:
@@ -8,6 +8,7 @@
|
||||
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const T = (s) => (window.__i18n && window.__i18n[s]) || s;
|
||||
|
||||
/* ---------- hero icons: one per venture ---------- */
|
||||
const svg = (p) => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + p + '</svg>';
|
||||
@@ -19,7 +20,7 @@
|
||||
{ n: 'Cybersecurity', side: 'r', x: '15%', y: '50%', r: '7deg', d: '-3s', p: '<path d="M12 3 4.5 6v5.5c0 4.5 3.2 7.8 7.5 9.5 4.3-1.7 7.5-5 7.5-9.5V6z"/><rect x="9" y="10.8" width="6" height="5" rx="1"/><path d="M10.2 10.8V9.4a1.8 1.8 0 0 1 3.6 0v1.4"/>' },
|
||||
{ n: 'E-Commerce', side: 'r', x: '6%', y: '73%', r: '-8deg', d: '-5s', p: '<path d="M5 8h14l-1 12H6z"/><path d="M9 8V7a3 3 0 0 1 6 0v1"/>' },
|
||||
];
|
||||
const tileHTML = (i) => '<div class="tile">' + svg(i.p) + '</div><span class="tl">' + i.n.replace('&', '&') + '</span>';
|
||||
const tileHTML = (i) => '<div class="tile">' + svg(i.p) + '</div><span class="tl">' + T(i.n).replace('&', '&') + '</span>';
|
||||
const fl = $('#floaters'), ir = $('#iconRow');
|
||||
if (fl && ir) {
|
||||
ICONS.forEach((i) => {
|
||||
@@ -225,7 +226,7 @@
|
||||
let vi = 0, vSwitch = () => {};
|
||||
V.forEach((v, i) => {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button'; b.setAttribute('role', 'tab'); b.textContent = v.n;
|
||||
b.type = 'button'; b.setAttribute('role', 'tab'); b.textContent = T(v.n);
|
||||
b.addEventListener('click', () => setV(i));
|
||||
vtabs.appendChild(b);
|
||||
});
|
||||
@@ -233,7 +234,7 @@
|
||||
function setV(i, first) {
|
||||
vi = (i + V.length) % V.length;
|
||||
vbtns.forEach((b, n) => b.setAttribute('aria-selected', String(n === vi)));
|
||||
const apply = () => { vname.textContent = V[vi].n; vdesc.textContent = V[vi].d; vnum.textContent = '0' + (vi + 1) + ' / 06'; vpanel.style.setProperty('--ac', V[vi].c); vt.classList.remove('out'); };
|
||||
const apply = () => { vname.textContent = T(V[vi].n); vdesc.textContent = T(V[vi].d); vnum.textContent = '0' + (vi + 1) + ' / 06'; vpanel.style.setProperty('--ac', V[vi].c); vt.classList.remove('out'); };
|
||||
if (first || reduce) apply(); else { vt.classList.add('out'); setTimeout(apply, 200); }
|
||||
vSwitch(vi);
|
||||
const sel = vbtns[vi];
|
||||
@@ -242,10 +243,12 @@
|
||||
$('#vprev').addEventListener('click', () => setV(vi - 1));
|
||||
$('#vnext').addEventListener('click', () => setV(vi + 1));
|
||||
vpanel.setAttribute('tabindex', '0');
|
||||
vpanel.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight') setV(vi + 1); if (e.key === 'ArrowLeft') setV(vi - 1); });
|
||||
// In Arabic (right to left) "next" is to the left.
|
||||
const fwd = document.documentElement.dir === 'rtl' ? -1 : 1;
|
||||
vpanel.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight') setV(vi + fwd); if (e.key === 'ArrowLeft') setV(vi - fwd); });
|
||||
let sx0 = null;
|
||||
vpanel.addEventListener('touchstart', (e) => { sx0 = e.touches[0].clientX; }, { passive: true });
|
||||
vpanel.addEventListener('touchend', (e) => { if (sx0 === null) return; const dx = e.changedTouches[0].clientX - sx0; if (Math.abs(dx) > 50) setV(vi + (dx < 0 ? 1 : -1)); sx0 = null; });
|
||||
vpanel.addEventListener('touchend', (e) => { if (sx0 === null) return; const dx = e.changedTouches[0].clientX - sx0; if (Math.abs(dx) > 50) setV(vi + (dx < 0 ? fwd : -fwd)); sx0 = null; });
|
||||
setV(0, true);
|
||||
|
||||
if (typeof THREE === 'undefined') return;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/* Arabic: right-to-left layout and type. Loaded only on /ar/ pages, after styles.css. */
|
||||
:root {
|
||||
--font-sans: "Noto Sans Arabic", "Inter Tight", "Inter", ui-sans-serif, system-ui, "Geeza Pro", Tahoma, sans-serif;
|
||||
}
|
||||
/* Arabic letters join; tracking (positive or negative) breaks them apart. */
|
||||
body *:not(.brand-word):not(.brand-word *):not(.foot-word) { letter-spacing: 0 !important; }
|
||||
.h-display-lg, .h-display, .h-lg, .h-md, .rule h3, .book h3, .shout q {
|
||||
font-family: "Noto Kufi Arabic", var(--font-sans); line-height: 1.35;
|
||||
}
|
||||
.statement, .poem, .quote-line { line-height: 1.6; }
|
||||
.eyebrow, .btn, .text-link, .tag-upper { font-size: 13.5px; }
|
||||
|
||||
/* The logo, code samples, URLs and the giant footer word stay left to right. */
|
||||
.brand, .foot-word, .file, .qlist p, .panel-url, .lang-switch, [dir="ltr"] { direction: ltr; }
|
||||
|
||||
/* Arrows */
|
||||
.flip { transform: scaleX(-1); }
|
||||
.btn:hover svg.flip, .text-link:hover svg.flip { transform: translateX(-3px) scaleX(-1); }
|
||||
.btn:hover .go { transform: translateX(-3px); }
|
||||
.tab-btn .arr { transform: translateX(4px) scaleX(-1); }
|
||||
.tab-btn[aria-selected="true"] .arr { transform: scaleX(-1); }
|
||||
#vprev svg, #vnext svg { transform: scaleX(-1); }
|
||||
|
||||
/* Physical offsets mirrored */
|
||||
.skip { left: auto; right: 16px; }
|
||||
.mobile-menu .sub a { padding-left: 2px; padding-right: 18px; }
|
||||
.panel-url { margin-left: 0; margin-right: auto; }
|
||||
.cursor { margin-left: 0; margin-right: 2px; }
|
||||
.cluster { text-align: right; }
|
||||
.dash { left: auto; right: 228px; }
|
||||
.profile { left: auto; right: 0; }
|
||||
.code { right: auto; left: 0; }
|
||||
.feature::before { left: auto; right: 0; }
|
||||
.stat { padding: 28px 0 0 24px; }
|
||||
.stat b sup { margin-left: 0; margin-right: 2px; }
|
||||
.quote-line { padding-left: 0; border-left: 0; padding-right: 20px; border-right: 2px solid var(--accent); }
|
||||
.flow { left: auto; right: -32px; }
|
||||
.process-step:not(:last-child)::after { right: auto; left: -16px; }
|
||||
.tab-btn { text-align: right; }
|
||||
.principle { padding: 32px 0 8px 32px; }
|
||||
.industries { border-left: 0; border-right: 1px solid var(--mist); }
|
||||
.industry { border-right: 0; border-left: 1px solid var(--mist); }
|
||||
.progress { transform-origin: 100% 50%; }
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.stat + .stat { padding-left: 0; border-left: 0; padding-right: 24px; border-right: 1px solid var(--mist); }
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.stat-note { border-right: 0 !important; }
|
||||
}
|
||||
@media (min-width: 901px) {
|
||||
.principle + .principle { padding-left: 0; border-left: 0; padding-right: 32px; border-right: 1px solid var(--mist); }
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.flow { right: 16px; }
|
||||
.process-step:not(:last-child)::after { left: auto; right: 58px; }
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.cluster .panel { right: auto; }
|
||||
}
|
||||
|
||||
/* Homepage */
|
||||
.cluster-fit .dash { left: auto; right: 176px; }
|
||||
.cluster-fit .profile { left: auto; right: 0; }
|
||||
.cluster-fit .code { right: auto; left: 0; }
|
||||
@media (max-width: 1100px) {
|
||||
.cluster-fit .panel { right: auto; left: auto; }
|
||||
}
|
||||
.w-side { right: auto; left: max(var(--gutter), calc((100vw - var(--page-max)) / 2 + var(--gutter))); }
|
||||
.w-top { left: auto; right: max(var(--gutter), calc((100vw - var(--page-max)) / 2 + var(--gutter))); }
|
||||
@media (max-width: 700px) { .w-side { left: var(--gutter); right: var(--gutter); } }
|
||||
.varrows { right: auto; left: 1.2rem; }
|
||||
.jfinal { left: auto; right: 1.2rem; }
|
||||
.jreplay { right: auto; left: 1rem; }
|
||||
.cc::before { right: auto; left: -40px; }
|
||||
/* Scroll-driven word tracks move left to right in every language. */
|
||||
.wmask, .pbword { direction: ltr; }
|
||||
@@ -1,6 +1,8 @@
|
||||
(function () {
|
||||
window.__site = true;
|
||||
var root = document.documentElement;
|
||||
// Translations for the current page's language, written into the page by the build.
|
||||
var T = function (s) { return (window.__i18n && window.__i18n[s]) || s; };
|
||||
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
// Theme toggle: remembers the visitor's choice; otherwise follows the system.
|
||||
@@ -9,7 +11,7 @@
|
||||
var paint = function () {
|
||||
if (!themeBtn) return;
|
||||
var dark = current() === 'dark';
|
||||
themeBtn.setAttribute('aria-label', dark ? 'Switch to light mode' : 'Switch to dark mode');
|
||||
themeBtn.setAttribute('aria-label', T(dark ? 'Switch to light mode' : 'Switch to dark mode'));
|
||||
themeBtn.setAttribute('aria-pressed', String(dark));
|
||||
};
|
||||
var apply = function (t, save) {
|
||||
@@ -41,7 +43,7 @@
|
||||
var open = menu.hidden;
|
||||
menu.hidden = !open;
|
||||
menuBtn.setAttribute('aria-expanded', String(open));
|
||||
menuBtn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
|
||||
menuBtn.setAttribute('aria-label', T(open ? 'Close menu' : 'Open menu'));
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && !menu.hidden) { menu.hidden = true; menuBtn.setAttribute('aria-expanded', 'false'); menuBtn.focus(); }
|
||||
@@ -116,7 +118,7 @@
|
||||
b.addEventListener('click', function () {
|
||||
var text = b.getAttribute('data-copy');
|
||||
var label = b.textContent;
|
||||
var done = function () { b.textContent = 'Copied'; setTimeout(function () { b.textContent = label; }, 1600); };
|
||||
var done = function () { b.textContent = T('Copied'); setTimeout(function () { b.textContent = label; }, 1600); };
|
||||
try { navigator.clipboard.writeText(text).then(done, function () {}); } catch (e) {}
|
||||
});
|
||||
});
|
||||
@@ -139,7 +141,7 @@
|
||||
var subject = 'Inquiry from ' + d.get('name') + (d.get('topic') ? ' — ' + d.get('topic') : '');
|
||||
window.location.href = 'mailto:' + form.getAttribute('data-to') + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body);
|
||||
var status = document.getElementById('form-status');
|
||||
if (status) status.textContent = 'Your email app should open with the message ready to send.';
|
||||
if (status) status.textContent = T('Your email app should open with the message ready to send.');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -889,6 +889,12 @@ h1, h2, h3, h4 { font-weight: 600; }
|
||||
.dropdown { background: var(--nav-bg); -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); border-color: var(--bd); }
|
||||
:root[data-theme="dark"] .dropdown { background: rgba(10, 14, 26, .96); }
|
||||
.theme-btn, .menu-btn { border-radius: 10px; border-color: var(--bd); background: var(--glass); }
|
||||
.lang-switch { display: inline-flex; align-items: center; gap: 2px; padding: 3px; border: 1px solid var(--bd); border-radius: 10px; background: var(--glass); flex: none; }
|
||||
.lang-switch a, .lang-switch span { display: inline-grid; place-items: center; min-width: 32px; height: 32px; padding: 0 6px; border-radius: 7px; font: 600 11.5px/1 var(--font-sans); letter-spacing: 0.08em; color: var(--slate); transition: color .15s ease, background-color .15s ease; }
|
||||
.lang-switch a:hover { color: var(--ink); }
|
||||
.lang-switch [aria-current] { color: var(--ink); background: rgba(26, 127, 255, .22); }
|
||||
.mobile-menu .lang-switch { display: flex; width: max-content; margin-top: 20px; }
|
||||
.mobile-menu .lang-switch a { padding: 0 6px; font-size: 12.5px; border-bottom: 0; }
|
||||
|
||||
/* Real logo on dark, vector recreation on light */
|
||||
.logo-img { display: none; height: 30px; width: auto; }
|
||||
@@ -950,6 +956,7 @@ h1, h2, h3, h4 { font-weight: 600; }
|
||||
.brand-word { font-size: 12px; }
|
||||
.logo-mark { width: 38px; }
|
||||
.nav-cta { gap: 6px; }
|
||||
.nav-cta .lang-switch { display: none; }
|
||||
.hero { padding-top: 56px; }
|
||||
.page-hero { padding-block: 48px 56px; }
|
||||
.cluster { grid-template-columns: 1fr; margin-top: 48px; }
|
||||
|
||||
Reference in New Issue
Block a user