diff --git a/web/src/config/config-context.test.tsx b/web/src/config/config-context.test.tsx
new file mode 100644
index 0000000..996b239
--- /dev/null
+++ b/web/src/config/config-context.test.tsx
@@ -0,0 +1,38 @@
+import { expect, test, beforeEach } from "vitest";
+import { screen, waitFor, render } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import i18n, { LOCALE_KEY } from "../i18n";
+import { ConfigProvider, useConfig } from "./config-context";
+
+function Probe() {
+ const config = useConfig();
+ return {config.default_language};
+}
+
+function renderProvider() {
+ const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ return render(
+
+
+ ,
+ );
+}
+
+beforeEach(() => {
+ localStorage.clear();
+ void i18n.changeLanguage("en");
+});
+
+test("exposes config and applies default language when no stored preference", async () => {
+ renderProvider();
+ expect(await screen.findByTestId("lang")).toHaveTextContent("sv");
+ await waitFor(() => expect(i18n.language).toBe("sv"));
+});
+
+test("a stored locale preference wins over the instance default", async () => {
+ localStorage.setItem(LOCALE_KEY, "en");
+ void i18n.changeLanguage("en");
+ renderProvider();
+ await screen.findByTestId("lang");
+ await waitFor(() => expect(i18n.language).toBe("en"));
+});
diff --git a/web/src/config/config-context.tsx b/web/src/config/config-context.tsx
new file mode 100644
index 0000000..978672e
--- /dev/null
+++ b/web/src/config/config-context.tsx
@@ -0,0 +1,44 @@
+import { createContext, useContext, useEffect, type ReactNode } from "react";
+import { useQuery } from "@tanstack/react-query";
+
+import type { components } from "../api/schema";
+import { api } from "../api/client";
+import i18n, { LOCALE_KEY } from "../i18n";
+
+type ConfigView = components["schemas"]["ConfigView"];
+
+const DEFAULTS: ConfigView = {
+ app_name: "Collection Management System",
+ default_language: "sv",
+ default_timezone: "Europe/Stockholm",
+};
+
+const ConfigContext = createContext(DEFAULTS);
+
+export function useConfig(): ConfigView {
+ return useContext(ConfigContext);
+}
+
+export function ConfigProvider({ children }: { children: ReactNode }) {
+ const { data } = useQuery({
+ queryKey: ["config"],
+ queryFn: async (): Promise => {
+ const { data, error } = await api.GET("/api/config");
+
+ if (error || !data) throw new Error("failed to load config");
+
+ return data;
+ },
+ staleTime: Infinity,
+ });
+
+ // Default the UI language to the instance default, unless the user has chosen one for
+ // this browser (LangSwitch persists to localStorage[LOCALE_KEY]).
+ useEffect(() => {
+ if (data && !localStorage.getItem(LOCALE_KEY)) {
+ void i18n.changeLanguage(data.default_language);
+ }
+ }, [data]);
+
+ return {children};
+}
diff --git a/web/src/main.tsx b/web/src/main.tsx
index 871fcd6..92b2d30 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { App } from "./app";
+import { ConfigProvider } from "./config/config-context";
import "./index.css";
import "./i18n";
@@ -13,7 +14,9 @@ const queryClient = new QueryClient({
createRoot(document.getElementById("root")!).render(
-
+
+
+
,
);
diff --git a/web/src/test/handlers.ts b/web/src/test/handlers.ts
index f6dcfba..5b5c01d 100644
--- a/web/src/test/handlers.ts
+++ b/web/src/test/handlers.ts
@@ -3,6 +3,14 @@ import { http, HttpResponse } from "msw";
import { amphora, fibula, fieldDefinitions, materialTerms, objectsPage, personAuthorities, searchHits, vocabularies } from "./fixtures";
export const handlers = [
+ http.get("/api/config", () =>
+ HttpResponse.json({
+ app_name: "Test Museum",
+ default_language: "sv",
+ default_timezone: "Europe/Stockholm",
+ }),
+ ),
+
http.get("/api/admin/me", () =>
HttpResponse.json({ id: "u1", email: "editor@example.com", role: "editor" }),
),