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:
Mo Tarbin
2026-03-08 15:57:57 +00:00
parent 21e0b08716
commit 6d6185dc5e
26 changed files with 2234 additions and 20 deletions

View File

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

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