feat: add LocaleProvider and hooks for managing locale and timezone settings
Some checks failed
Frontend CI / lint (push) Failing after 19s
Frontend CI / build (push) Has been skipped

This commit is contained in:
JSC
2025-08-15 19:19:05 +02:00
parent 32140d7b5a
commit cd654b8777
6 changed files with 182 additions and 9 deletions

View File

@@ -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 (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<AuthProvider>
<SocketProvider>
<AppRoutes />
<Toaster richColors />
</SocketProvider>
</AuthProvider>
</ThemeProvider>
<LocaleProvider defaultLocale="fr-FR" defaultTimezone="Europe/Paris">
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<AuthProvider>
<SocketProvider>
<AppRoutes />
<Toaster richColors />
</SocketProvider>
</AuthProvider>
</ThemeProvider>
</LocaleProvider>
)
}

View File

@@ -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<Locale>(() => {
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<string>(() => {
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 (
<LocaleProviderContext.Provider {...props} value={value}>
{children}
</LocaleProviderContext.Provider>
)
}

View File

@@ -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<LocaleProviderState>(initialState)
export type { Locale, LocaleProviderState }

11
src/hooks/use-locale.ts Normal file
View File

@@ -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
}

14
src/lib/utils/locale.ts Normal file
View File

@@ -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']
}

View File

@@ -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() {
</p>
</div>
<div className="pt-4">
<div className="space-y-2">
<Label>Language Preference</Label>
<Select
value={locale}
onValueChange={(value: 'en-US' | 'fr-FR') =>
setLocale(value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="en-US">English (US)</SelectItem>
<SelectItem value="fr-FR">Français (FR)</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Choose your preferred language for the interface
</p>
</div>
<div className="space-y-2">
<Label>Timezone</Label>
<Select
value={timezone}
onValueChange={setTimezone}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{getSupportedTimezones().map((tz) => (
<SelectItem key={tz} value={tz}>
{tz.replace('_', ' ')}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Choose your timezone for date and time display
</p>
</div>
<div className="pt-4 space-y-1">
<div className="text-sm text-muted-foreground">
Current theme:{' '}
<span className="font-medium capitalize">{theme}</span>
</div>
<div className="text-sm text-muted-foreground">
Current language:{' '}
<span className="font-medium">{locale === 'en-US' ? 'English (US)' : 'Français (FR)'}</span>
</div>
<div className="text-sm text-muted-foreground">
Current timezone:{' '}
<span className="font-medium">{timezone.replace('_', ' ')}</span>
</div>
</div>
</CardContent>
</Card>