feat: German as the main language, English under /en, with a language switch

The site now exists in German (at the root, with /impressum and /datenschutz)
and English (/en). A request without a language follows the visitor's choice
from the language switch, then the browser language, then German; the old
English legal addresses move to /en for good.

All visible texts are data in src/content/de.ts and en.ts, so components hold
none of their own. The header carries a DE/EN switch whose bit curtain covers
the page while it is swapped and reopens at the same reading position. Title,
description, canonical, hreflang, Open Graph, the share image, the sitemap and
the structured data are per language. The privacy notice now describes the
cookie that remembers the chosen language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 13:07:22 +02:00
co-authored by Claude Opus 5.5
parent e89a248ca5
commit 75993100fa
72 changed files with 4418 additions and 1405 deletions
@@ -3,13 +3,9 @@
import { BitAura } from "@/src/components/bits/BitAura";
import * as React from "react";
import { useNarrow } from "./useNarrow";
import {
KIND_LABEL,
WORKFLOWS,
type FlowNode,
type NodeKind,
type Workflow,
} from "@/src/content/automation";
import { useContent } from "@/src/components/ContentProvider";
import { workflowsFor, type FlowNode } from "@/src/content/automation";
import type { NodeKind } from "@/src/content/types";
/**
* AutomationFlow: a workflow drawn as connected systems, with the work visibly
@@ -95,6 +91,9 @@ const KIND_STYLE: Readonly<
human: { stroke: "#f8fafc", dash: "2 4", tag: "#f8fafc" },
};
/** The order of the legend below the drawing. */
const KINDS: readonly NodeKind[] = ["trigger", "rule", "ai", "action", "human"];
type Clock = {
/** Which run of the workflow is playing. */
readonly run: number;
@@ -107,7 +106,12 @@ type Clock = {
const MONO = "font-mono text-[11px] uppercase tracking-[0.2em]";
export const AutomationFlow: React.FC = () => {
const [flowId, setFlowId] = React.useState(WORKFLOWS[0]?.id ?? "");
const { automation } = useContent();
const workflows = React.useMemo(
() => workflowsFor(automation.workflows),
[automation.workflows],
);
const [flowId, setFlowId] = React.useState(workflows[0]?.id ?? "");
const [clock, setClock] = React.useState<Clock>({ run: 0, step: 0, t: 0 });
const ref = React.useRef<HTMLDivElement>(null);
const [inView, setInView] = React.useState(false);
@@ -116,7 +120,7 @@ export const AutomationFlow: React.FC = () => {
const tall = useNarrow();
const canvas = tall ? TALL : WIDE;
const flow = WORKFLOWS.find((item) => item.id === flowId) ?? WORKFLOWS[0];
const flow = workflows.find((item) => item.id === flowId) ?? workflows[0];
React.useEffect(() => {
const node = ref.current;
@@ -195,10 +199,15 @@ export const AutomationFlow: React.FC = () => {
}
});
const logLines: string[] = [];
const logLines: { text: string; kind: NodeKind }[] = [];
const log = (node: Placed) => {
if (logLines.every((line) => line.text !== node.log)) {
logLines.push({ text: node.log, kind: node.kind });
}
};
if (firstNode) {
const first = nodeOf(firstNode);
if (first) logLines.push(first.log);
if (first) log(first);
}
steps.forEach((edges, index) => {
const arrived =
@@ -206,7 +215,7 @@ export const AutomationFlow: React.FC = () => {
if (!arrived) return;
edges.forEach(([, to]) => {
const node = nodeOf(to);
if (node && !logLines.includes(node.log)) logLines.push(node.log);
if (node) log(node);
});
});
const finished = clock.step >= steps.length;
@@ -215,10 +224,10 @@ export const AutomationFlow: React.FC = () => {
<div ref={ref}>
<div
role="tablist"
aria-label="Example workflows"
aria-label={automation.examplesLabel}
className="flex flex-wrap gap-2"
>
{WORKFLOWS.map((item) => {
{workflows.map((item) => {
const selected = item.id === flow.id;
return (
<button
@@ -251,7 +260,7 @@ export const AutomationFlow: React.FC = () => {
<span
className={`h-1.5 w-1.5 rounded-full ${finished ? "bg-emerald-400" : "bg-amber-300 animate-pulse"}`}
/>
{finished ? "run complete" : "running"}
{finished ? automation.runComplete : automation.running}
</span>
</div>
@@ -383,53 +392,51 @@ export const AutomationFlow: React.FC = () => {
<div className="grid grid-cols-1 border-t border-slate-800 md:grid-cols-12">
<div className="border-b border-slate-800 px-5 py-5 md:col-span-7 md:border-b-0 md:border-r md:px-7">
<p className={`${MONO} m-0 text-slate-500`}>Log</p>
<p className={`${MONO} m-0 text-slate-500`}>{automation.log}</p>
<ol
className="m-0 mt-3 min-h-[7.5rem] list-none p-0 font-mono text-[12px] leading-relaxed text-slate-300 md:text-[13px]"
aria-live="off"
>
{logLines.map((line, index) => (
<li key={line} className="flex gap-3">
<li key={line.text} className="flex gap-3">
<span className="text-slate-600">
{String(index + 1).padStart(2, "0")}
</span>
<span
className={
line.startsWith("unclear")
? "text-amber-200"
: undefined
line.kind === "human" ? "text-amber-200" : undefined
}
>
{line}
{line.text}
</span>
</li>
))}
</ol>
</div>
<ul className="m-0 flex list-none flex-col justify-center gap-2 px-5 py-5 md:col-span-5 md:px-7">
{(Object.keys(KIND_LABEL) as NodeKind[])
.filter((kind) => flow.nodes.some((node) => node.kind === kind))
.map((kind) => (
<li
key={kind}
className="flex items-center gap-3 font-mono text-[11px] text-slate-400"
>
<svg width="22" height="12" aria-hidden="true">
<rect
x="1"
y="1"
width="20"
height="10"
rx="3"
fill="none"
stroke={KIND_STYLE[kind].stroke}
strokeWidth="1.5"
strokeDasharray={KIND_STYLE[kind].dash}
/>
</svg>
{KIND_LABEL[kind]}
</li>
))}
{KINDS.filter((kind) =>
flow.nodes.some((node) => node.kind === kind),
).map((kind) => (
<li
key={kind}
className="flex items-center gap-3 font-mono text-[11px] text-slate-400"
>
<svg width="22" height="12" aria-hidden="true">
<rect
x="1"
y="1"
width="20"
height="10"
rx="3"
fill="none"
stroke={KIND_STYLE[kind].stroke}
strokeWidth="1.5"
strokeDasharray={KIND_STYLE[kind].dash}
/>
</svg>
{automation.kinds[kind]}
</li>
))}
</ul>
</div>
</div>
@@ -437,13 +444,13 @@ export const AutomationFlow: React.FC = () => {
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="rounded-3xl border border-slate-200 p-6 md:p-8">
<p className={`${MONO} m-0 text-slate-400`}>By hand</p>
<p className={`${MONO} m-0 text-slate-400`}>{automation.byHand}</p>
<p className="m-0 mt-3 font-sans text-lg leading-relaxed text-slate-500 md:text-xl">
{flow.byHand}
</p>
</div>
<div className="rounded-3xl border border-slate-900 p-6 md:p-8">
<p className={`${MONO} m-0 text-slate-900`}>Automated</p>
<p className={`${MONO} m-0 text-slate-900`}>{automation.automated}</p>
<p className="m-0 mt-3 font-sans text-lg leading-relaxed text-slate-900 md:text-xl">
{flow.automated}
</p>