diff --git a/web/src/shell/app-shell.test.tsx b/web/src/shell/app-shell.test.tsx
new file mode 100644
index 0000000..1e58ec9
--- /dev/null
+++ b/web/src/shell/app-shell.test.tsx
@@ -0,0 +1,40 @@
+import { expect, test, beforeEach, afterEach } from "vitest";
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { Routes, Route } from "react-router-dom";
+import i18n from "../i18n";
+import { renderApp } from "../test/render";
+import { AppShell } from "./app-shell";
+
+beforeEach(async () => {
+ await i18n.changeLanguage("en");
+});
+
+afterEach(async () => {
+ await i18n.changeLanguage("en");
+});
+
+function tree() {
+ return (
+
+ }>
+ objects outlet} />
+
+ login page} />
+
+ );
+}
+
+test("shows active and disabled nav and renders the outlet", async () => {
+ renderApp(tree(), { route: "/objects" });
+ expect(await screen.findByText("objects outlet")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: /objects/i })).toBeInTheDocument();
+ // later milestones are present but disabled
+ expect(screen.getByRole("button", { name: /search/i })).toBeDisabled();
+});
+
+test("language switch toggles to Swedish", async () => {
+ renderApp(tree(), { route: "/objects" });
+ await userEvent.click(await screen.findByRole("button", { name: "SV" }));
+ await waitFor(() => expect(screen.getByText("Föremål")).toBeInTheDocument());
+});
diff --git a/web/src/shell/app-shell.tsx b/web/src/shell/app-shell.tsx
new file mode 100644
index 0000000..2ff6c69
--- /dev/null
+++ b/web/src/shell/app-shell.tsx
@@ -0,0 +1,59 @@
+import { NavLink, Outlet, useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+
+import { useLogout } from "../api/queries";
+import { Button } from "@/components/ui/button";
+import { LangSwitch } from "./lang-switch";
+
+const FUTURE = ["vocabularies", "authorities", "fields", "search"] as const;
+
+export function AppShell() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const logout = useLogout();
+
+ const onSignOut = () =>
+ logout.mutate(undefined, {
+ onSuccess: () => navigate("/login", { replace: true }),
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/shell/lang-switch.tsx b/web/src/shell/lang-switch.tsx
new file mode 100644
index 0000000..67b9090
--- /dev/null
+++ b/web/src/shell/lang-switch.tsx
@@ -0,0 +1,21 @@
+import { useLocale } from "../i18n/use-locale";
+
+export function LangSwitch() {
+ const { locale, setLocale } = useLocale();
+ const base = locale.startsWith("sv") ? "sv" : "en";
+
+ return (
+
+ {(["sv", "en"] as const).map((lng) => (
+
+ ))}
+
+ );
+}