diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index be28e8a..e82d4d2 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -3,6 +3,7 @@ import type { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";
import Navbar from "../components/Navbar";
+import LocaleInitializer from "../components/LocaleInitializer";
import ServiceWorkerRegister from "../components/ServiceWorkerRegister";
import { AuthProvider } from "./context/AuthContext";
import ThemeProvider from "./context/ThemeProvider";
@@ -65,9 +66,10 @@ export default async function RootLayout({
return (
-
+
+
{children}
diff --git a/frontend/components/LanguageSwitcher.tsx b/frontend/components/LanguageSwitcher.tsx
index a18f84b..87cb77a 100644
--- a/frontend/components/LanguageSwitcher.tsx
+++ b/frontend/components/LanguageSwitcher.tsx
@@ -5,13 +5,15 @@
*
* Uses next-intl's locale-aware router so navigation preserves the current
* page while switching locale (e.g. `/wallet` → `/yo/wallet`). The choice is
- * persisted in the `NEXT_LOCALE` cookie by the i18n middleware.
+ * persisted to LocalStorage via the storage helper for client-side persistence
+ * across sessions.
*/
import { useState, useRef, useEffect, useTransition } from "react";
import { useLocale } from "next-intl";
import { useRouter, usePathname } from "../i18n/navigation";
import { routing } from "../i18n/routing";
+import { saveLocale } from "../lib/storage";
const LOCALE_LABELS: Record<(typeof routing.locales)[number], string> = {
en: "English",
@@ -45,6 +47,8 @@ export default function LanguageSwitcher() {
return;
}
setOpen(false);
+ // Persist the selected locale to LocalStorage
+ saveLocale(next);
startTransition(() => {
router.replace(pathname, { locale: next });
});
diff --git a/frontend/components/LocaleInitializer.tsx b/frontend/components/LocaleInitializer.tsx
new file mode 100644
index 0000000..e040da8
--- /dev/null
+++ b/frontend/components/LocaleInitializer.tsx
@@ -0,0 +1,37 @@
+/**
+ * LocaleInitializer — client component that restores the user's saved locale
+ * preference from LocalStorage on app load.
+ *
+ * Runs once on mount to check for a saved locale and redirect to it if:
+ * 1. A locale is saved in LocalStorage
+ * 2. It differs from the current locale
+ * 3. It's a valid, supported locale
+ *
+ * This ensures minimal FOUC (Flash of Unstyled Content) or locale mismatch
+ * after a page reload or new session.
+ */
+
+"use client";
+
+import { useEffect } from "react";
+import { useLocale } from "next-intl";
+import { useRouter, usePathname } from "../i18n/navigation";
+import { getSavedLocale } from "../hooks/useLocaleStorage";
+
+export default function LocaleInitializer() {
+ const locale = useLocale();
+ const router = useRouter();
+ const pathname = usePathname();
+
+ useEffect(() => {
+ const saved = getSavedLocale();
+
+ // Redirect to saved locale if it differs from current
+ if (saved !== locale) {
+ router.replace(pathname, { locale: saved });
+ }
+ }, [locale, router, pathname]);
+
+ // Invisible component — renders nothing
+ return null;
+}
diff --git a/frontend/hooks/useLocaleStorage.ts b/frontend/hooks/useLocaleStorage.ts
new file mode 100644
index 0000000..09ac2b3
--- /dev/null
+++ b/frontend/hooks/useLocaleStorage.ts
@@ -0,0 +1,54 @@
+/**
+ * Hook for managing locale persistence with LocalStorage.
+ *
+ * Provides utilities for saving, loading, and clearing the user's locale
+ * preference from client-side storage.
+ */
+
+"use client";
+
+import { loadLocale, saveLocale, clearLocale } from "../lib/storage";
+import { routing, type AppLocale } from "../i18n/routing";
+
+/**
+ * Validates that a locale is supported by the application.
+ */
+function isValidLocale(locale: unknown): locale is AppLocale {
+ return (
+ typeof locale === "string" &&
+ (routing.locales as readonly string[]).includes(locale)
+ );
+}
+
+/**
+ * Gets the saved locale from LocalStorage, if available and valid.
+ * Falls back to the default locale if not found or invalid.
+ *
+ * @returns The saved locale if valid, otherwise the default locale.
+ */
+export function getSavedLocale(): AppLocale {
+ const saved = loadLocale();
+ if (!saved || !isValidLocale(saved)) {
+ return routing.defaultLocale as AppLocale;
+ }
+ return saved;
+}
+
+/**
+ * Saves a locale to LocalStorage after validating it.
+ * Silently ignores invalid locales.
+ *
+ * @param locale - The locale to save
+ */
+export function persistLocale(locale: string): void {
+ if (isValidLocale(locale)) {
+ saveLocale(locale);
+ }
+}
+
+/**
+ * Clears the saved locale preference, reverting to default on next load.
+ */
+export function resetLocalePreference(): boolean {
+ return clearLocale();
+}
diff --git a/frontend/lib/storage.ts b/frontend/lib/storage.ts
new file mode 100644
index 0000000..067f7de
--- /dev/null
+++ b/frontend/lib/storage.ts
@@ -0,0 +1,67 @@
+/**
+ * Local storage helpers with SSR/hydration safety.
+ *
+ * LocalStorage is only available on the browser, so we guard against access
+ * during server rendering or before hydration.
+ */
+
+const LOCALE_KEY = "airflex_locale";
+
+/**
+ * Checks if LocalStorage is available (browser environment).
+ */
+function isLocalStorageAvailable(): boolean {
+ if (typeof window === "undefined") return false;
+ try {
+ const test = "__test__";
+ window.localStorage.setItem(test, test);
+ window.localStorage.removeItem(test);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Saves the selected locale to LocalStorage.
+ * Silently fails in SSR/non-browser environments.
+ */
+export function saveLocale(locale: string): void {
+ if (!isLocalStorageAvailable()) return;
+ try {
+ window.localStorage.setItem(LOCALE_KEY, locale);
+ } catch (error) {
+ // LocalStorage quota exceeded or private browsing mode — fail silently
+ console.warn("[storage] Failed to save locale to LocalStorage", error);
+ }
+}
+
+/**
+ * Reads the saved locale from LocalStorage.
+ * Returns null if not found or unavailable.
+ */
+export function loadLocale(): string | null {
+ if (!isLocalStorageAvailable()) return null;
+ try {
+ return window.localStorage.getItem(LOCALE_KEY);
+ } catch (error) {
+ // Private browsing mode or other storage errors — fail silently
+ console.warn("[storage] Failed to read locale from LocalStorage", error);
+ return null;
+ }
+}
+
+/**
+ * Clears the saved locale from LocalStorage.
+ * Returns true if successful, false otherwise.
+ */
+export function clearLocale(): boolean {
+ if (!isLocalStorageAvailable()) return false;
+ try {
+ window.localStorage.removeItem(LOCALE_KEY);
+ return true;
+ } catch (error) {
+ console.warn("[storage] Failed to clear locale from LocalStorage", error);
+ return false;
+ }
+}