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" }), ),