The site felt old, unfocused and bloated: 11 pages, a features page of 171 bullets, a homepage of 830 words in card grids around seven mockup-style screenshots, and no single thing it wanted a visitor to understand. - Message: control instead of drift. Audience: IT departments in small and mid-sized companies. Goal: buy a licence — netOrk itself is free, the licence adds vulnerability data and image updates. - Home is under 400 words: the drift comparison of a real access point, three steps, the hardware it runs on, vulnerabilities with a licence, what else is in the box, one closing band. Features, Drivers and Roadmap are gone; their URLs redirect (router and nginx 301). - New Pricing page: the free core, Starter / Pro / Enterprise on request with the plan differences from the licence server, four questions; buttons go to the licence portal. The unit-less KB request limit is left out. - Persona pages are one template; NIS2 and Plugins are cut to half or less. Impressum and Datenschutz exist as marked placeholders; the unsupported "MIT licence" claim is gone from the footer. - Look: light paper and ink, the dark product on a stage, Inter self-hosted, split sections and ruled lists instead of cards. Four real, cropped screenshots replace eight full-window ones. - Language follows the browser until someone chooses; <html lang> is set. Scroll-to-top on navigation, a catch-all route, no dead /docs/architecture. - CLAUDE.md, DESIGN.md, PAGES.md and PRODUCT.md describe the new rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
1.6 KiB
TypeScript
54 lines
1.6 KiB
TypeScript
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
|
|
import { translations, type Lang } from '../i18n/translations'
|
|
|
|
interface LangContextValue {
|
|
lang: Lang
|
|
setLang: (l: Lang) => void
|
|
t: typeof translations.en
|
|
}
|
|
|
|
const LangContext = createContext<LangContextValue | null>(null)
|
|
|
|
// A stored choice wins; otherwise the browser decides: German for de-*, English for everything else.
|
|
function initialLang(): Lang {
|
|
try {
|
|
const stored = localStorage.getItem('lang')
|
|
if (stored === 'de' || stored === 'en') return stored
|
|
} catch {
|
|
// storage blocked: fall through to the browser language
|
|
}
|
|
const preferred = navigator.languages?.[0] ?? navigator.language ?? 'en'
|
|
return preferred.toLowerCase().startsWith('de') ? 'de' : 'en'
|
|
}
|
|
|
|
export function LangProvider({ children }: { children: ReactNode }) {
|
|
const [lang, setLangState] = useState<Lang>(initialLang)
|
|
|
|
// <html lang> drives hyphenation and screen readers.
|
|
useEffect(() => {
|
|
document.documentElement.lang = lang
|
|
}, [lang])
|
|
|
|
// Only an explicit switch is remembered, so the browser keeps deciding until someone chooses.
|
|
function setLang(l: Lang) {
|
|
setLangState(l)
|
|
try {
|
|
localStorage.setItem('lang', l)
|
|
} catch {
|
|
// storage blocked: the choice lasts for this visit
|
|
}
|
|
}
|
|
|
|
return (
|
|
<LangContext.Provider value={{ lang, setLang, t: translations[lang] }}>
|
|
{children}
|
|
</LangContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useLang() {
|
|
const ctx = useContext(LangContext)
|
|
if (!ctx) throw new Error('useLang must be used inside LangProvider')
|
|
return ctx
|
|
}
|