diff --git a/src/App.tsx b/src/App.tsx index 3f7686b..607b8e2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { Navigate, Route, Routes } from 'react-router' +import { LocaleProvider } from './components/LocaleProvider' import { ThemeProvider } from './components/ThemeProvider' import { Toaster } from './components/ui/sonner' import { AuthProvider, useAuth } from './contexts/AuthContext' @@ -139,14 +140,16 @@ function AppRoutes() { function App() { return ( - - - - - - - - + + + + + + + + + + ) } diff --git a/src/components/LocaleProvider.tsx b/src/components/LocaleProvider.tsx new file mode 100644 index 0000000..5476d3b --- /dev/null +++ b/src/components/LocaleProvider.tsx @@ -0,0 +1,70 @@ +import { type Locale, LocaleProviderContext } from '@/contexts/LocaleContext' +import { getSupportedTimezones } from '@/lib/utils/locale' +import { useEffect, useState } from 'react' + +type LocaleProviderProps = { + children: React.ReactNode + defaultLocale?: Locale + defaultTimezone?: string + localeStorageKey?: string + timezoneStorageKey?: string +} + +export function LocaleProvider({ + children, + defaultLocale = 'fr-FR', + defaultTimezone = 'Europe/Paris', + localeStorageKey = 'locale', + timezoneStorageKey = 'timezone', + ...props +}: LocaleProviderProps) { + const [locale, setLocaleState] = useState(() => { + const stored = localStorage.getItem(localeStorageKey) as Locale + const validLocale = stored && ['en-US', 'fr-FR'].includes(stored) ? stored : defaultLocale + + // Set default in localStorage if not present + if (!stored) { + localStorage.setItem(localeStorageKey, defaultLocale) + } + + return validLocale + }) + + const [timezone, setTimezoneState] = useState(() => { + const stored = localStorage.getItem(timezoneStorageKey) + const supportedTimezones = getSupportedTimezones() + const validTimezone = stored && supportedTimezones.includes(stored) ? stored : defaultTimezone + + // Set default in localStorage if not present + if (!stored) { + localStorage.setItem(timezoneStorageKey, defaultTimezone) + } + + return validTimezone + }) + + useEffect(() => { + // Set document language attribute for accessibility + document.documentElement.lang = locale.split('-')[0] + }, [locale]) + + const value = { + locale, + timezone, + setLocale: (newLocale: Locale) => { + localStorage.setItem(localeStorageKey, newLocale) + setLocaleState(newLocale) + }, + setTimezone: (newTimezone: string) => { + localStorage.setItem(timezoneStorageKey, newTimezone) + setTimezoneState(newTimezone) + }, + } + + return ( + + {children} + + ) +} + diff --git a/src/contexts/LocaleContext.tsx b/src/contexts/LocaleContext.tsx new file mode 100644 index 0000000..ddea7e4 --- /dev/null +++ b/src/contexts/LocaleContext.tsx @@ -0,0 +1,21 @@ +import { createContext } from 'react' + +type Locale = 'en-US' | 'fr-FR' + +type LocaleProviderState = { + locale: Locale + timezone: string + setLocale: (locale: Locale) => void + setTimezone: (timezone: string) => void +} + +const initialState: LocaleProviderState = { + locale: 'fr-FR', + timezone: 'Europe/Paris', + setLocale: () => null, + setTimezone: () => null, +} + +export const LocaleProviderContext = + createContext(initialState) +export type { Locale, LocaleProviderState } \ No newline at end of file diff --git a/src/hooks/use-locale.ts b/src/hooks/use-locale.ts new file mode 100644 index 0000000..396dda4 --- /dev/null +++ b/src/hooks/use-locale.ts @@ -0,0 +1,11 @@ +import { LocaleProviderContext } from '@/contexts/LocaleContext' +import { useContext } from 'react' + +export const useLocale = () => { + const context = useContext(LocaleProviderContext) + + if (context === undefined) + throw new Error('useLocale must be used within a LocaleProvider') + + return context +} \ No newline at end of file diff --git a/src/lib/utils/locale.ts b/src/lib/utils/locale.ts new file mode 100644 index 0000000..ae8b984 --- /dev/null +++ b/src/lib/utils/locale.ts @@ -0,0 +1,14 @@ +// Get supported timezones, fallback to basic list if Intl.supportedValuesOf is not available +export const getSupportedTimezones = (): string[] => { + try { + if (typeof Intl !== 'undefined' && 'supportedValuesOf' in Intl) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (Intl as any).supportedValuesOf('timeZone') + } + } catch { + console.warn('Intl.supportedValuesOf not available, using fallback timezones') + } + + // Fallback timezone list + return ['America/New_York', 'Europe/Paris'] +} \ No newline at end of file diff --git a/src/pages/AccountPage.tsx b/src/pages/AccountPage.tsx index 5142454..f399618 100644 --- a/src/pages/AccountPage.tsx +++ b/src/pages/AccountPage.tsx @@ -20,6 +20,8 @@ import { import { Skeleton } from '@/components/ui/skeleton' import { useAuth } from '@/contexts/AuthContext' import { useTheme } from '@/hooks/use-theme' +import { useLocale } from '@/hooks/use-locale' +import { getSupportedTimezones } from '@/lib/utils/locale' import { type ApiTokenStatusResponse, type UserProvider, @@ -44,6 +46,7 @@ import { toast } from 'sonner' export function AccountPage() { const { user, setUser } = useAuth() const { theme, setTheme } = useTheme() + const { locale, timezone, setLocale, setTimezone } = useLocale() // Profile state const [profileName, setProfileName] = useState('') @@ -363,11 +366,62 @@ export function AccountPage() {

-
+
+ + +

+ Choose your preferred language for the interface +

+
+ +
+ + +

+ Choose your timezone for date and time display +

+
+ +
Current theme:{' '} {theme}
+
+ Current language:{' '} + {locale === 'en-US' ? 'English (US)' : 'Français (FR)'} +
+
+ Current timezone:{' '} + {timezone.replace('_', ' ')} +