feat: bilingual website (DE/EN) with language switcher
CI / TypeScript — type-check (push) Successful in 8s
CI / Publish — build & push image (push) Successful in 8s
CI / Deploy — pull & restart on host (push) Successful in 2s

Add LangContext with localStorage persistence (default: DE).
DE/EN toggle in Nav (top-right, before CTA button).

All pages translated:
- Home: all 8 sections incl. NIS2 block
- Features: all 13 sections with full German bullet points
- Drivers: headings and descriptions
- Getting Started (coming soon)
- Roadmap: bilingual data arrays inline (Planned + Under consideration)
- NIS2: full Art. 21 mapping, evidence blocks, coming-soon list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-06-28 17:00:22 +02:00
co-authored by Claude Sonnet 4.6
parent f194c379e4
commit d48c7143aa
11 changed files with 1011 additions and 618 deletions
+17 -14
View File
@@ -1,6 +1,9 @@
import { Link } from 'react-router-dom'
import { useLang } from '../context/LangContext'
export default function Footer() {
const { t } = useLang()
return (
<footer className="bg-slate-900 border-t border-slate-800 mt-auto">
<div className="max-w-7xl mx-auto px-6 py-12">
@@ -12,33 +15,33 @@ export default function Footer() {
net<span className="text-sky-400">Ork</span>
</span>
</Link>
<p className="text-sm text-slate-500">Self-hosted network orchestration</p>
<p className="text-sm text-slate-500">{t.footer.tagline}</p>
</div>
<div className="grid grid-cols-3 gap-10 flex-1 text-sm">
<div>
<p className="font-medium text-slate-400 mb-3">Links</p>
<p className="font-medium text-slate-400 mb-3">{t.footer.links}</p>
<ul className="space-y-2">
<li><Link to="/features" className="text-slate-500 hover:text-slate-100 transition-colors">Features</Link></li>
<li><Link to="/drivers" className="text-slate-500 hover:text-slate-100 transition-colors">Drivers</Link></li>
<li><Link to="/plugins" className="text-slate-500 hover:text-slate-100 transition-colors">Plugins</Link></li>
<li><Link to="/roadmap" className="text-slate-500 hover:text-slate-100 transition-colors">Roadmap</Link></li>
<li><Link to="/features" className="text-slate-500 hover:text-slate-100 transition-colors">{t.nav.features}</Link></li>
<li><Link to="/drivers" className="text-slate-500 hover:text-slate-100 transition-colors">{t.nav.drivers}</Link></li>
<li><Link to="/plugins" className="text-slate-500 hover:text-slate-100 transition-colors">{t.nav.plugins}</Link></li>
<li><Link to="/roadmap" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.roadmap}</Link></li>
</ul>
</div>
<div>
<p className="font-medium text-slate-400 mb-3">Resources</p>
<p className="font-medium text-slate-400 mb-3">{t.footer.resources}</p>
<ul className="space-y-2">
<li><Link to="/docs/getting-started" className="text-slate-500 hover:text-slate-100 transition-colors">Getting Started</Link></li>
<li><Link to="/docs/architecture" className="text-slate-500 hover:text-slate-100 transition-colors">Architecture</Link></li>
<li><span className="text-slate-600">Changelog</span></li>
<li><Link to="/nis2" className="text-slate-500 hover:text-slate-100 transition-colors">NIS2</Link></li>
<li><Link to="/docs/getting-started" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.gettingStarted}</Link></li>
<li><Link to="/docs/architecture" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.architecture}</Link></li>
<li><span className="text-slate-600">{t.footer.changelog}</span></li>
<li><Link to="/nis2" className="text-slate-500 hover:text-slate-100 transition-colors">{t.footer.nis2}</Link></li>
</ul>
</div>
<div>
<p className="font-medium text-slate-400 mb-3">Legal</p>
<p className="font-medium text-slate-400 mb-3">{t.footer.legal}</p>
<ul className="space-y-2">
<li><span className="text-slate-600">MIT License</span></li>
<li><span className="text-slate-600">Privacy (none collected)</span></li>
<li><span className="text-slate-600">{t.footer.mit}</span></li>
<li><span className="text-slate-600">{t.footer.privacy}</span></li>
</ul>
</div>
</div>
+35 -11
View File
@@ -1,11 +1,14 @@
import { useState, useRef, useEffect } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { ChevronDownIcon } from '@heroicons/react/24/outline'
import { useLang } from '../context/LangContext'
import type { Lang } from '../i18n/translations'
export default function Nav() {
const [docsOpen, setDocsOpen] = useState(false)
const location = useLocation()
const dropdownRef = useRef<HTMLDivElement>(null)
const { lang, setLang, t } = useLang()
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
@@ -41,11 +44,11 @@ export default function Nav() {
</Link>
<Link to="/features" className={linkCls('/features')}>
Features
{t.nav.features}
</Link>
<Link to="/drivers" className={linkCls('/drivers')}>
Drivers
{t.nav.drivers}
</Link>
<div className="relative" ref={dropdownRef}>
@@ -55,53 +58,74 @@ export default function Nav() {
isDocsActive ? 'text-slate-100' : 'text-slate-400 hover:text-slate-100'
}`}
>
Docs
{t.nav.docs}
<ChevronDownIcon
className={`h-4 w-4 transition-transform ${docsOpen ? 'rotate-180' : ''}`}
/>
</button>
{docsOpen && (
<div className="absolute top-full mt-2 left-0 w-48 rounded-lg border border-slate-800 bg-slate-900 py-1 shadow-xl">
<div className="absolute top-full mt-2 left-0 w-52 rounded-lg border border-slate-800 bg-slate-900 py-1 shadow-xl">
<Link
to="/docs/getting-started"
className="block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors"
>
Getting Started
{t.nav.docsItems.gettingStarted}
</Link>
<Link
to="/docs/architecture"
className="block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors"
>
Architecture
{t.nav.docsItems.architecture}
</Link>
<div className="my-1 border-t border-slate-800" />
<Link
to="/nis2"
className="block px-4 py-2 text-sm text-slate-400 hover:text-slate-100 hover:bg-slate-800 transition-colors"
>
NIS2
{t.nav.docsItems.nis2}
</Link>
</div>
)}
</div>
<Link to="/plugins" className={linkCls('/plugins')}>
Plugins
{t.nav.plugins}
</Link>
<Link to="/roadmap" className={linkCls('/roadmap')}>
Roadmap
{t.nav.roadmap}
</Link>
<div className="ml-auto">
<div className="ml-auto flex items-center gap-4">
<LangSwitch lang={lang} setLang={setLang} />
<Link
to="/docs/getting-started"
className="inline-flex items-center gap-2 px-4 py-1.5 text-sm rounded-lg bg-sky-600 hover:bg-sky-500 text-white font-medium transition-colors"
>
Get started
{t.nav.getStarted}
</Link>
</div>
</div>
</nav>
)
}
function LangSwitch({ lang, setLang }: { lang: Lang; setLang: (l: Lang) => void }) {
return (
<div className="flex items-center gap-1 text-xs font-medium">
<button
onClick={() => setLang('de')}
className={`transition-colors ${lang === 'de' ? 'text-slate-100' : 'text-slate-500 hover:text-slate-300'}`}
>
DE
</button>
<span className="text-slate-700">/</span>
<button
onClick={() => setLang('en')}
className={`transition-colors ${lang === 'en' ? 'text-slate-100' : 'text-slate-500 hover:text-slate-300'}`}
>
EN
</button>
</div>
)
}