feat: add LocaleProvider and hooks for managing locale and timezone settings
This commit is contained in:
@@ -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,6 +140,7 @@ function AppRoutes() {
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<LocaleProvider defaultLocale="fr-FR" defaultTimezone="Europe/Paris">
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<AuthProvider>
|
||||
<SocketProvider>
|
||||
@@ -147,6 +149,7 @@ function App() {
|
||||
</SocketProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</LocaleProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
70
src/components/LocaleProvider.tsx
Normal file
70
src/components/LocaleProvider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
21
src/contexts/LocaleContext.tsx
Normal file
21
src/contexts/LocaleContext.tsx
Normal 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
11
src/hooks/use-locale.ts
Normal 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
14
src/lib/utils/locale.ts
Normal 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']
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user