From cd654b877749384949dc133efbb28fbd933fc5e2 Mon Sep 17 00:00:00 2001
From: JSC
Date: Fri, 15 Aug 2025 19:19:05 +0200
Subject: [PATCH] feat: add LocaleProvider and hooks for managing locale and
timezone settings
---
src/App.tsx | 19 +++++----
src/components/LocaleProvider.tsx | 70 +++++++++++++++++++++++++++++++
src/contexts/LocaleContext.tsx | 21 ++++++++++
src/hooks/use-locale.ts | 11 +++++
src/lib/utils/locale.ts | 14 +++++++
src/pages/AccountPage.tsx | 56 ++++++++++++++++++++++++-
6 files changed, 182 insertions(+), 9 deletions(-)
create mode 100644 src/components/LocaleProvider.tsx
create mode 100644 src/contexts/LocaleContext.tsx
create mode 100644 src/hooks/use-locale.ts
create mode 100644 src/lib/utils/locale.ts
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('_', ' ')}
+