feat: Add comprehensive internationalization (i18n) support
- Add multi-language support with i18next and react-i18next - Implement user-selectable date format preferences (5 formats) - Add time format preferences (12-hour/24-hour) - Implement RTL (right-to-left) support for Arabic, Hebrew, Persian, Urdu - Add first day of week preference - Create LocalizationContext for managing i18n settings - Add LocalizationSettings component to Settings page - Include sample translations: English, Spanish, Arabic - Configure 10 languages: en, es, fr, de, ar, he, zh, ja, pt, ru - Add translation management documentation and Crowdin config - Create date formatting utilities that respect user preferences - Add RTL CSS styles for proper layout mirroring - Update Settings and ThemeToggle components to use translations - Add comprehensive documentation (implementation guide, quick reference, translation guide) All user preferences are persisted to localStorage and apply throughout the app.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { AlertsProvider } from '../service/AlertsProvider'
|
||||
import { NotificationProvider } from '../service/NotificationProvider'
|
||||
import { LocalizationProvider } from './LocalizationContext'
|
||||
import QueryContext from './QueryContext'
|
||||
import RouterContext from './RouterContext'
|
||||
import ThemeContext from './ThemeContext'
|
||||
@@ -8,6 +9,7 @@ const Contexts = ({ children }) => {
|
||||
const contexts = [
|
||||
AlertsProvider,
|
||||
ThemeContext,
|
||||
LocalizationProvider,
|
||||
QueryContext,
|
||||
NotificationProvider,
|
||||
RouterContext,
|
||||
|
||||
119
src/contexts/LocalizationContext.jsx
Normal file
119
src/contexts/LocalizationContext.jsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import useStickyState from '@/hooks/useStickyState'
|
||||
import moment from 'moment'
|
||||
import { createContext, useContext, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const LocalizationContext = createContext()
|
||||
|
||||
export const DATE_FORMATS = {
|
||||
MDY: 'MM/DD/YYYY',
|
||||
DMY: 'DD/MM/YYYY',
|
||||
YMD: 'YYYY-MM-DD',
|
||||
LONG: 'MMMM D, YYYY',
|
||||
SHORT: 'MMM D, YYYY',
|
||||
}
|
||||
|
||||
export const TIME_FORMATS = {
|
||||
HOUR_12: 'h:mm A',
|
||||
HOUR_24: 'HH:mm',
|
||||
}
|
||||
|
||||
export const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur']
|
||||
|
||||
export const AVAILABLE_LANGUAGES = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
|
||||
{ code: 'fr', name: 'French', nativeName: 'Français' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
|
||||
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' },
|
||||
{ code: 'he', name: 'Hebrew', nativeName: 'עברית' },
|
||||
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
|
||||
{ code: 'ja', name: 'Japanese', nativeName: '日本語' },
|
||||
{ code: 'pt', name: 'Portuguese', nativeName: 'Português' },
|
||||
{ code: 'ru', name: 'Russian', nativeName: 'Русский' },
|
||||
]
|
||||
|
||||
export const LocalizationProvider = ({ children }) => {
|
||||
const { i18n } = useTranslation()
|
||||
const [dateFormat, setDateFormat] = useStickyState(
|
||||
DATE_FORMATS.MDY,
|
||||
'dateFormat',
|
||||
)
|
||||
const [timeFormat, setTimeFormat] = useStickyState(
|
||||
TIME_FORMATS.HOUR_12,
|
||||
'timeFormat',
|
||||
)
|
||||
const [firstDayOfWeek, setFirstDayOfWeek] = useStickyState(0, 'firstDayOfWeek') // 0 = Sunday, 1 = Monday
|
||||
const [language, setLanguage] = useStickyState('en', 'language')
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language)
|
||||
moment.locale(language)
|
||||
}, [language, i18n])
|
||||
|
||||
useEffect(() => {
|
||||
const isRTL = RTL_LANGUAGES.includes(language)
|
||||
document.documentElement.dir = isRTL ? 'rtl' : 'ltr'
|
||||
document.documentElement.lang = language
|
||||
}, [language])
|
||||
|
||||
const formatDate = (date, format = dateFormat) => {
|
||||
if (!date) return ''
|
||||
return moment(date).format(format)
|
||||
}
|
||||
|
||||
const formatDateTime = (date, format) => {
|
||||
if (!date) return ''
|
||||
const dateTimeFormat = format || `${dateFormat} ${timeFormat}`
|
||||
return moment(date).format(dateTimeFormat)
|
||||
}
|
||||
|
||||
const formatTime = (date, format = timeFormat) => {
|
||||
if (!date) return ''
|
||||
return moment(date).format(format)
|
||||
}
|
||||
|
||||
const formatRelative = date => {
|
||||
if (!date) return ''
|
||||
return moment(date).fromNow()
|
||||
}
|
||||
|
||||
const formatCalendar = date => {
|
||||
if (!date) return ''
|
||||
return moment(date).calendar()
|
||||
}
|
||||
|
||||
const isRTL = RTL_LANGUAGES.includes(language)
|
||||
|
||||
const value = {
|
||||
dateFormat,
|
||||
setDateFormat,
|
||||
timeFormat,
|
||||
setTimeFormat,
|
||||
firstDayOfWeek,
|
||||
setFirstDayOfWeek,
|
||||
language,
|
||||
setLanguage,
|
||||
isRTL,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
formatTime,
|
||||
formatRelative,
|
||||
formatCalendar,
|
||||
availableLanguages: AVAILABLE_LANGUAGES,
|
||||
}
|
||||
|
||||
return (
|
||||
<LocalizationContext.Provider value={value}>
|
||||
{children}
|
||||
</LocalizationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useLocalization = () => {
|
||||
const context = useContext(LocalizationContext)
|
||||
if (!context) {
|
||||
throw new Error('useLocalization must be used within LocalizationProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
117
src/i18n/README.md
Normal file
117
src/i18n/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Internationalization (i18n) Setup
|
||||
|
||||
This directory contains the internationalization configuration for Donetick.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
src/i18n/
|
||||
├── config.js # i18next configuration
|
||||
└── README.md # This file
|
||||
|
||||
public/locales/
|
||||
├── en/ # English (default)
|
||||
│ ├── common.json
|
||||
│ ├── settings.json
|
||||
│ └── chores.json
|
||||
├── es/ # Spanish
|
||||
├── ar/ # Arabic (RTL)
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Usage in Components
|
||||
|
||||
### Using translations
|
||||
|
||||
```jsx
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function MyComponent() {
|
||||
const { t } = useTranslation('settings') // or 'common', 'chores'
|
||||
|
||||
return <h1>{t('title')}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
### Using date formatting
|
||||
|
||||
```jsx
|
||||
import { useLocalization } from '@/contexts/LocalizationContext'
|
||||
|
||||
function MyComponent() {
|
||||
const { formatDate, formatDateTime, formatRelative } = useLocalization()
|
||||
|
||||
const date = new Date()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Date: {formatDate(date)}</p>
|
||||
<p>DateTime: {formatDateTime(date)}</p>
|
||||
<p>Relative: {formatRelative(date)}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Using language/format settings
|
||||
|
||||
```jsx
|
||||
import { useLocalization } from '@/contexts/LocalizationContext'
|
||||
|
||||
function MyComponent() {
|
||||
const {
|
||||
language,
|
||||
setLanguage,
|
||||
dateFormat,
|
||||
setDateFormat,
|
||||
isRTL
|
||||
} = useLocalization()
|
||||
|
||||
return (
|
||||
<div dir={isRTL ? 'rtl' : 'ltr'}>
|
||||
Current language: {language}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Available Namespaces
|
||||
|
||||
- **common**: General UI elements (buttons, messages, etc.)
|
||||
- **settings**: Settings page translations
|
||||
- **chores**: Chores-related translations
|
||||
|
||||
## Adding New Translations
|
||||
|
||||
1. Add the text to the appropriate JSON file in `public/locales/en/`
|
||||
2. Use the translation in your component with `t('key')`
|
||||
3. Upload to translation platform for community translation
|
||||
|
||||
## RTL Support
|
||||
|
||||
Languages in the `RTL_LANGUAGES` array automatically get:
|
||||
- `dir="rtl"` on the document
|
||||
- RTL-specific CSS styles
|
||||
- Proper text alignment
|
||||
|
||||
Currently supported RTL languages: Arabic (ar), Hebrew (he), Persian (fa), Urdu (ur)
|
||||
|
||||
## Date Format Preferences
|
||||
|
||||
Users can choose from:
|
||||
- MM/DD/YYYY (US)
|
||||
- DD/MM/YYYY (Europe)
|
||||
- YYYY-MM-DD (ISO)
|
||||
- Long format (January 1, 2024)
|
||||
- Short format (Jan 1, 2024)
|
||||
|
||||
## Time Format Preferences
|
||||
|
||||
- 12-hour (with AM/PM)
|
||||
- 24-hour
|
||||
|
||||
## First Day of Week
|
||||
|
||||
Users can choose:
|
||||
- Sunday
|
||||
- Monday
|
||||
36
src/i18n/config.js
Normal file
36
src/i18n/config.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import i18n from 'i18next'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import HttpBackend from 'i18next-http-backend'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
i18n
|
||||
.use(HttpBackend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
debug: import.meta.env.DEV,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
|
||||
ns: ['common', 'settings', 'chores'],
|
||||
defaultNS: 'common',
|
||||
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage'],
|
||||
lookupLocalStorage: 'i18nextLng',
|
||||
},
|
||||
|
||||
react: {
|
||||
useSuspense: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -45,3 +45,44 @@ html {
|
||||
.animate-optimized.animation-complete {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
/* RTL Support */
|
||||
[dir='rtl'] {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir='rtl'] .rtl-mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* Handle margins and paddings for RTL */
|
||||
[dir='rtl'] .ml-auto {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
[dir='rtl'] .mr-auto {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Flip icons and arrows in RTL */
|
||||
[dir='rtl'] .rtl-flip {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* Ensure proper text alignment in RTL */
|
||||
[dir='rtl'] input,
|
||||
[dir='rtl'] textarea {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Handle border radius for RTL */
|
||||
[dir='rtl'] .rounded-l-none {
|
||||
border-radius: 0 0.375rem 0.375rem 0;
|
||||
}
|
||||
|
||||
[dir='rtl'] .rounded-r-none {
|
||||
border-radius: 0.375rem 0 0 0.375rem;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import Contexts from './contexts/Contexts.jsx'
|
||||
import './i18n/config'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
|
||||
83
src/utils/DateFormatter.js
Normal file
83
src/utils/DateFormatter.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import moment from 'moment'
|
||||
|
||||
export const createDateFormatter = (
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
firstDayOfWeek,
|
||||
) => {
|
||||
moment.updateLocale('en', {
|
||||
week: {
|
||||
dow: firstDayOfWeek,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
formatDate: (date, customFormat) => {
|
||||
if (!date) return ''
|
||||
return moment(date).format(customFormat || dateFormat)
|
||||
},
|
||||
|
||||
formatDateTime: (date, customFormat) => {
|
||||
if (!date) return ''
|
||||
const format = customFormat || `${dateFormat} ${timeFormat}`
|
||||
return moment(date).format(format)
|
||||
},
|
||||
|
||||
formatTime: (date, customFormat) => {
|
||||
if (!date) return ''
|
||||
return moment(date).format(customFormat || timeFormat)
|
||||
},
|
||||
|
||||
formatRelative: date => {
|
||||
if (!date) return ''
|
||||
return moment(date).fromNow()
|
||||
},
|
||||
|
||||
formatCalendar: (date, opts) => {
|
||||
if (!date) return ''
|
||||
return moment(date).calendar(null, opts)
|
||||
},
|
||||
|
||||
formatShortDate: date => {
|
||||
if (!date) return ''
|
||||
return moment(date).format('MMM D, YYYY')
|
||||
},
|
||||
|
||||
formatLongDate: date => {
|
||||
if (!date) return ''
|
||||
return moment(date).format('MMMM D, YYYY')
|
||||
},
|
||||
|
||||
isBefore: (date, compareDate) => {
|
||||
return moment(date).isBefore(compareDate)
|
||||
},
|
||||
|
||||
isAfter: (date, compareDate) => {
|
||||
return moment(date).isAfter(compareDate)
|
||||
},
|
||||
|
||||
diff: (date1, date2, unit) => {
|
||||
return moment(date1).diff(moment(date2), unit)
|
||||
},
|
||||
|
||||
add: (date, amount, unit) => {
|
||||
return moment(date).add(amount, unit).toDate()
|
||||
},
|
||||
|
||||
subtract: (date, amount, unit) => {
|
||||
return moment(date).subtract(amount, unit).toDate()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const useDateFormatter = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return createDateFormatter('MM/DD/YYYY', 'h:mm A', 0)
|
||||
}
|
||||
|
||||
const dateFormat = localStorage.getItem('dateFormat') || 'MM/DD/YYYY'
|
||||
const timeFormat = localStorage.getItem('timeFormat') || 'h:mm A'
|
||||
const firstDayOfWeek = parseInt(localStorage.getItem('firstDayOfWeek') || '0')
|
||||
|
||||
return createDateFormatter(dateFormat, timeFormat, firstDayOfWeek)
|
||||
}
|
||||
187
src/views/Settings/LocalizationSettings.jsx
Normal file
187
src/views/Settings/LocalizationSettings.jsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import {
|
||||
useLocalization,
|
||||
DATE_FORMATS,
|
||||
TIME_FORMATS,
|
||||
} from '@/contexts/LocalizationContext'
|
||||
import { LanguageOutlined } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Option,
|
||||
Select,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const LocalizationSettings = () => {
|
||||
const { t } = useTranslation('settings')
|
||||
const {
|
||||
language,
|
||||
setLanguage,
|
||||
dateFormat,
|
||||
setDateFormat,
|
||||
timeFormat,
|
||||
setTimeFormat,
|
||||
firstDayOfWeek,
|
||||
setFirstDayOfWeek,
|
||||
availableLanguages,
|
||||
isRTL,
|
||||
} = useLocalization()
|
||||
|
||||
const sampleDate = moment('2024-01-15 14:30:00')
|
||||
|
||||
const dateFormatOptions = [
|
||||
{ value: DATE_FORMATS.MDY, label: t('localization.formats.mdy') },
|
||||
{ value: DATE_FORMATS.DMY, label: t('localization.formats.dmy') },
|
||||
{ value: DATE_FORMATS.YMD, label: t('localization.formats.ymd') },
|
||||
{ value: DATE_FORMATS.LONG, label: t('localization.formats.long') },
|
||||
{ value: DATE_FORMATS.SHORT, label: t('localization.formats.short') },
|
||||
]
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Card variant='outlined' sx={{ p: 3, mb: 2 }}>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<LanguageOutlined />
|
||||
<Typography level='title-md'>
|
||||
{t('localization.language')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography level='body-sm' sx={{ mb: 2 }}>
|
||||
{t('localization.languageDescription')}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={language}
|
||||
onChange={(_, value) => setLanguage(value)}
|
||||
sx={{ minWidth: 250 }}
|
||||
>
|
||||
{availableLanguages.map(lang => (
|
||||
<Option key={lang.code} value={lang.code}>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<Typography>{lang.nativeName}</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
({lang.name})
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{isRTL && (
|
||||
<FormHelperText>
|
||||
This language uses right-to-left (RTL) text direction
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
{t('localization.dateFormat')}
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ mb: 2 }}>
|
||||
{t('localization.dateFormatDescription')}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={dateFormat}
|
||||
onChange={(_, value) => setDateFormat(value)}
|
||||
sx={{ minWidth: 250 }}
|
||||
>
|
||||
{dateFormatOptions.map(option => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%', gap: 2 }}>
|
||||
<Typography>{option.label}</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
{sampleDate.format(option.value)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Preview: {sampleDate.format(dateFormat)}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
{t('localization.timeFormat')}
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ mb: 2 }}>
|
||||
{t('localization.timeFormatDescription')}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={timeFormat}
|
||||
onChange={(_, value) => setTimeFormat(value)}
|
||||
sx={{ minWidth: 250 }}
|
||||
>
|
||||
<Option value={TIME_FORMATS.HOUR_12}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%', gap: 2 }}>
|
||||
<Typography>{t('localization.12hour')}</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
{sampleDate.format(TIME_FORMATS.HOUR_12)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
<Option value={TIME_FORMATS.HOUR_24}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%', gap: 2 }}>
|
||||
<Typography>{t('localization.24hour')}</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
{sampleDate.format(TIME_FORMATS.HOUR_24)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Preview: {sampleDate.format(timeFormat)}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Box>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
{t('localization.firstDayOfWeek')}
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ mb: 2 }}>
|
||||
{t('localization.firstDayOfWeekDescription')}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
variant={firstDayOfWeek === 0 ? 'solid' : 'outlined'}
|
||||
onClick={() => setFirstDayOfWeek(0)}
|
||||
>
|
||||
{t('localization.sunday')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={firstDayOfWeek === 1 ? 'solid' : 'outlined'}
|
||||
onClick={() => setFirstDayOfWeek(1)}
|
||||
>
|
||||
{t('localization.monday')}
|
||||
</Button>
|
||||
</Box>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default LocalizationSettings
|
||||
@@ -23,6 +23,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import RealTimeSettings from '../../components/RealTimeSettings'
|
||||
import SubscriptionModal from '../../components/SubscriptionModal'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import Logo from '../../Logo'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
@@ -46,6 +47,7 @@ import NativeCancelSubscriptionModal from '../Modals/Inputs/NativeCancelSubscrip
|
||||
import PassowrdChangeModal from '../Modals/Inputs/PasswordChangeModal'
|
||||
import UserDeletionModal from '../Modals/Inputs/UserDeletionModal'
|
||||
import APITokenSettings from './APITokenSettings'
|
||||
import LocalizationSettings from './LocalizationSettings'
|
||||
import MFASettings from './MFASettings'
|
||||
import NotificationSetting from './NotificationSetting'
|
||||
import ProfileSettings from './ProfileSettings'
|
||||
@@ -58,6 +60,7 @@ const Settings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const { showNotification } = useNotification()
|
||||
const navigate = useNavigate()
|
||||
const { formatDate } = useLocalization()
|
||||
|
||||
const [userCircles, setUserCircles] = useState([])
|
||||
const [circleMemberRequests, setCircleMemberRequests] = useState([])
|
||||
@@ -188,13 +191,13 @@ const Settings = () => {
|
||||
|
||||
const getSubscriptionDetails = () => {
|
||||
if (userProfile?.subscription === 'active') {
|
||||
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment(
|
||||
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${formatDate(
|
||||
userProfile?.expiration,
|
||||
).format('MMM DD, YYYY')}.`
|
||||
)}.`
|
||||
} else if (userProfile?.subscription === 'cancelled') {
|
||||
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment(
|
||||
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${formatDate(
|
||||
userProfile?.expiration,
|
||||
).format('MMM DD, YYYY')}.`
|
||||
)}.`
|
||||
} else {
|
||||
return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
|
||||
}
|
||||
@@ -204,9 +207,7 @@ const Settings = () => {
|
||||
return `Plus`
|
||||
} else if (userProfile?.subscription === 'cancelled') {
|
||||
if (moment().isBefore(userProfile?.expiration)) {
|
||||
return `Plus(until ${moment(userProfile?.expiration).format(
|
||||
'MMM DD, YYYY',
|
||||
)})`
|
||||
return `Plus(until ${formatDate(userProfile?.expiration)})`
|
||||
}
|
||||
return `Free`
|
||||
} else {
|
||||
@@ -916,6 +917,16 @@ const Settings = () => {
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 py-4' id='localization'>
|
||||
<Typography level='h3'>Localization</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
Customize language, date format, and regional preferences for your
|
||||
account. These settings will apply throughout the application.
|
||||
</Typography>
|
||||
<LocalizationSettings />
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
{confirmModalConfig?.isOpen && (
|
||||
<ConfirmationModal config={confirmModalConfig} />
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
ToggleButtonGroup,
|
||||
useColorScheme,
|
||||
} from '@mui/joy'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ELEMENTID = 'select-theme-mode'
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const { t } = useTranslation('settings')
|
||||
const { mode, setMode } = useColorScheme()
|
||||
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
|
||||
|
||||
@@ -30,7 +32,7 @@ const ThemeToggle = () => {
|
||||
id={`${ELEMENTID}-label`}
|
||||
htmlFor='select-theme-mode'
|
||||
>
|
||||
Theme mode
|
||||
{t('theme.themeMode')}
|
||||
</FormLabel>
|
||||
)
|
||||
|
||||
@@ -45,13 +47,13 @@ const ThemeToggle = () => {
|
||||
onChange={handleThemeModeChange}
|
||||
>
|
||||
<Button startDecorator={<LightModeOutlined />} value='light'>
|
||||
Light
|
||||
{t('theme.light')}
|
||||
</Button>
|
||||
<Button startDecorator={<DarkModeOutlined />} value='dark'>
|
||||
Dark
|
||||
{t('theme.dark')}
|
||||
</Button>
|
||||
<Button startDecorator={<LaptopOutlined />} value='system'>
|
||||
System
|
||||
{t('theme.system')}
|
||||
</Button>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user