67246c69e5
- Settings sidebar was a fixed-width vertical list with no responsive
breakpoint, squeezing the form into a sliver on mobile. Turn it into
a horizontal scrollable tab bar under md, vertical sidebar above.
- setLocale() fired the PATCH and swallowed the result with
.catch(() => {}) — never checked res.ok, no success/failure feedback.
A failed save (expired session, network blip) looked identical to a
successful one. Now returns a boolean, reverts local state on
failure, and the settings form toasts success/failure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
import { NextIntlClientProvider } from "next-intl";
|
|
|
|
export const SUPPORTED_LOCALES = [
|
|
{ code: "en", label: "English" },
|
|
{ code: "fr", label: "Français" },
|
|
] as const;
|
|
|
|
export type Locale = (typeof SUPPORTED_LOCALES)[number]["code"];
|
|
|
|
const STORAGE_KEY = "epicure-locale";
|
|
|
|
const LocaleContext = createContext<{
|
|
locale: Locale;
|
|
setLocale: (locale: Locale) => Promise<boolean>;
|
|
}>({ locale: "en", setLocale: async () => false });
|
|
|
|
export function useLocale() {
|
|
return useContext(LocaleContext);
|
|
}
|
|
|
|
export function I18nProvider({
|
|
children,
|
|
messages,
|
|
initialLocale,
|
|
}: {
|
|
children: React.ReactNode;
|
|
messages: Record<string, Record<string, unknown>>;
|
|
initialLocale?: Locale;
|
|
}) {
|
|
const resolved = initialLocale ?? ("en" as Locale);
|
|
const [locale, setLocaleState] = useState<Locale>(resolved);
|
|
const [currentMessages, setCurrentMessages] = useState(messages[resolved] ?? messages["en"]!);
|
|
|
|
useEffect(() => {
|
|
// If no server-side locale, fall back to localStorage
|
|
if (!initialLocale) {
|
|
const stored = localStorage.getItem(STORAGE_KEY) as Locale | null;
|
|
if (stored && messages[stored]) {
|
|
setLocaleState(stored);
|
|
setCurrentMessages(messages[stored]!);
|
|
}
|
|
}
|
|
}, [messages, initialLocale]);
|
|
|
|
async function setLocale(next: Locale): Promise<boolean> {
|
|
const previous = locale;
|
|
const previousMessages = currentMessages;
|
|
setLocaleState(next);
|
|
setCurrentMessages(messages[next]!);
|
|
localStorage.setItem(STORAGE_KEY, next);
|
|
try {
|
|
const res = await fetch("/api/v1/users/me", {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ locale: next }),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to save locale");
|
|
return true;
|
|
} catch {
|
|
// Revert on failure so the UI doesn't claim a language that never saved.
|
|
setLocaleState(previous);
|
|
setCurrentMessages(previousMessages);
|
|
localStorage.setItem(STORAGE_KEY, previous);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<LocaleContext.Provider value={{ locale, setLocale }}>
|
|
<NextIntlClientProvider locale={locale} messages={currentMessages} timeZone="UTC">
|
|
{children}
|
|
</NextIntlClientProvider>
|
|
</LocaleContext.Provider>
|
|
);
|
|
}
|