feat: add LocaleProvider and hooks for managing locale and timezone settings
This commit is contained in:
19
src/App.tsx
19
src/App.tsx
@@ -1,4 +1,5 @@
|
|||||||
import { Navigate, Route, Routes } from 'react-router'
|
import { Navigate, Route, Routes } from 'react-router'
|
||||||
|
import { LocaleProvider } from './components/LocaleProvider'
|
||||||
import { ThemeProvider } from './components/ThemeProvider'
|
import { ThemeProvider } from './components/ThemeProvider'
|
||||||
import { Toaster } from './components/ui/sonner'
|
import { Toaster } from './components/ui/sonner'
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||||
@@ -139,14 +140,16 @@ function AppRoutes() {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<LocaleProvider defaultLocale="fr-FR" defaultTimezone="Europe/Paris">
|
||||||
<AuthProvider>
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<SocketProvider>
|
<AuthProvider>
|
||||||
<AppRoutes />
|
<SocketProvider>
|
||||||
<Toaster richColors />
|
<AppRoutes />
|
||||||
</SocketProvider>
|
<Toaster richColors />
|
||||||
</AuthProvider>
|
</SocketProvider>
|
||||||
</ThemeProvider>
|
</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 { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import { useTheme } from '@/hooks/use-theme'
|
||||||
|
import { useLocale } from '@/hooks/use-locale'
|
||||||
|
import { getSupportedTimezones } from '@/lib/utils/locale'
|
||||||
import {
|
import {
|
||||||
type ApiTokenStatusResponse,
|
type ApiTokenStatusResponse,
|
||||||
type UserProvider,
|
type UserProvider,
|
||||||
@@ -44,6 +46,7 @@ import { toast } from 'sonner'
|
|||||||
export function AccountPage() {
|
export function AccountPage() {
|
||||||
const { user, setUser } = useAuth()
|
const { user, setUser } = useAuth()
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
const { locale, timezone, setLocale, setTimezone } = useLocale()
|
||||||
|
|
||||||
// Profile state
|
// Profile state
|
||||||
const [profileName, setProfileName] = useState('')
|
const [profileName, setProfileName] = useState('')
|
||||||
@@ -363,11 +366,62 @@ export function AccountPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div className="text-sm text-muted-foreground">
|
||||||
Current theme:{' '}
|
Current theme:{' '}
|
||||||
<span className="font-medium capitalize">{theme}</span>
|
<span className="font-medium capitalize">{theme}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user