diff --git a/src/components/I18nProvider.jsx b/src/components/I18nProvider.jsx
index 916c4200..05cbba34 100644
--- a/src/components/I18nProvider.jsx
+++ b/src/components/I18nProvider.jsx
@@ -1,9 +1,40 @@
import React, { createContext, useContext, useCallback, useEffect, useState } from 'react';
import { I18nextProvider } from 'react-i18next';
import i18n, { SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY, RTL_LANGUAGES } from '../i18n/index.js';
+import {
+ detectBestLocale,
+ formatLocaleCurrency,
+ formatLocaleDateTime,
+ formatLocaleNumber,
+ getCulturalAdaptations,
+ getLocaleProfile,
+ getRegionalContent,
+ isLocaleRTL,
+ loadPersistedLocale,
+ persistLocalePreference,
+ validateLocaleFormatting,
+} from '../lib/localeFeatures';
const I18nContext = createContext(null);
+function getInitialLocaleProfile() {
+ return detectBestLocale({
+ storedLocale: loadPersistedLocale(),
+ navigatorLanguage: typeof navigator !== 'undefined' ? navigator.language : null,
+ htmlLang: typeof document !== 'undefined' ? document.documentElement.getAttribute('lang') : null,
+ });
+}
+
+function applyLocaleDocumentAttributes(profile) {
+ if (typeof document === 'undefined') return;
+ const root = document.documentElement;
+ root.setAttribute('lang', profile.code);
+ root.setAttribute('dir', profile.textDirection);
+ root.dataset.locale = profile.code;
+ root.dataset.region = profile.region;
+ root.dataset.currency = profile.currency;
+ root.dataset.layoutDensity = profile.layoutDensity;
+}
/**
* I18nProvider
@@ -18,47 +49,88 @@ const I18nContext = createContext(null);
*
*/
export function I18nProvider({ children }) {
+ const initialProfile = getInitialLocaleProfile();
const [currentLanguage, setCurrentLanguage] = useState(
- () => i18n.language?.slice(0, 2) || 'en'
+ () => i18n.language?.slice(0, 2) || initialProfile.languageCode
+ );
+ const [currentLocale, setCurrentLocale] = useState(
+ () => initialProfile.code
);
// Keep local state in sync when i18next changes language externally
useEffect(() => {
- const onLangChange = (lng) => setCurrentLanguage(lng.slice(0, 2));
+ const onLangChange = (lng) => {
+ const profile = getLocaleProfile(lng);
+ setCurrentLanguage(profile.languageCode);
+ setCurrentLocale(profile.code);
+ applyLocaleDocumentAttributes(profile);
+ };
i18n.on('languageChanged', onLangChange);
return () => i18n.off('languageChanged', onLangChange);
}, []);
+ useEffect(() => {
+ applyLocaleDocumentAttributes(getLocaleProfile(currentLocale));
+ }, [currentLocale]);
+
/**
* Switch the active language.
* @param {string} langCode - BCP-47 language code, e.g. 'en' | 'es'
*/
const changeLanguage = useCallback(async (langCode) => {
- const supported = SUPPORTED_LANGUAGES.find((l) => l.code === langCode);
+ const requestedProfile = getLocaleProfile(langCode);
+ const supported = SUPPORTED_LANGUAGES.find((l) => {
+ return l.code === langCode || l.locale === langCode || l.code === requestedProfile.languageCode;
+ });
if (!supported) {
console.warn(`[i18n] Unsupported language: "${langCode}". Falling back to "en".`);
- langCode = 'en';
}
- await i18n.changeLanguage(langCode);
+ const profile = getLocaleProfile(supported?.locale || 'en-US');
+ await i18n.changeLanguage(profile.languageCode);
try {
- localStorage.setItem(LANGUAGE_STORAGE_KEY, langCode);
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, profile.languageCode);
} catch {
// localStorage may be unavailable (private browsing, etc.)
}
+ persistLocalePreference(profile.code);
- // Update for accessibility & SEO
- document.documentElement.setAttribute('lang', langCode);
- // Update for RTL language support (#184)
- document.documentElement.setAttribute('dir', RTL_LANGUAGES.has(langCode) ? 'rtl' : 'ltr');
+ setCurrentLanguage(profile.languageCode);
+ setCurrentLocale(profile.code);
+ applyLocaleDocumentAttributes(profile);
}, []);
- const isRTL = RTL_LANGUAGES.has(currentLanguage);
+ const localeProfile = getLocaleProfile(currentLocale);
+ const isRTL = RTL_LANGUAGES.has(currentLanguage) || isLocaleRTL(currentLocale);
+ const formatDateTime = useCallback(
+ (date, options) => formatLocaleDateTime(date, currentLocale, options),
+ [currentLocale]
+ );
+ const formatNumber = useCallback(
+ (value, options) => formatLocaleNumber(value, currentLocale, options),
+ [currentLocale]
+ );
+ const formatCurrency = useCallback(
+ (value, currency) => formatLocaleCurrency(value, currentLocale, currency),
+ [currentLocale]
+ );
+ const validateLocale = useCallback(
+ () => validateLocaleFormatting(currentLocale),
+ [currentLocale]
+ );
const value = {
currentLanguage,
+ currentLocale,
changeLanguage,
supportedLanguages: SUPPORTED_LANGUAGES,
+ localeProfile,
+ culturalAdaptations: getCulturalAdaptations(currentLocale),
+ regionalContent: getRegionalContent(currentLocale),
+ formatDateTime,
+ formatNumber,
+ formatCurrency,
+ validateLocale,
isRTL,
};
@@ -79,4 +151,4 @@ export function useI18nContext() {
return ctx;
}
-export default I18nProvider;
\ No newline at end of file
+export default I18nProvider;
diff --git a/src/components/preferences/UserPreferences.jsx b/src/components/preferences/UserPreferences.jsx
index 315dd6af..c704cb9b 100644
--- a/src/components/preferences/UserPreferences.jsx
+++ b/src/components/preferences/UserPreferences.jsx
@@ -5,24 +5,8 @@ import ThemeSettings from './ThemeSettings'
import AccessibilitySettings from './AccessibilitySettings'
import NotificationPreferences from '../notifications/NotificationPreferences'
import { showTestNotification } from '../../utils/offline'
-import {
- Bell,
- Download,
- RefreshCw,
- Share2,
- SlidersHorizontal,
- Upload,
-} from 'lucide-react'
-import {
- applyPreferencePreset,
- createPreferencePreset,
- exportPreferences,
- getPreferenceSyncStatus,
- importPreferences,
- PREFERENCE_PRESETS,
- sharePreferencePreset,
- validatePreferences,
-} from '../../lib/userPreferences'
+import { Bell, Globe2 } from 'lucide-react'
+import { useI18nContext } from '../I18nProvider.jsx'
const TABS = [
{ id: 'general', label: 'General' },
@@ -34,7 +18,19 @@ const TABS = [
]
export default function UserPreferences({ onClose }) {
- const { preferences, update, save, reset, loading } = usePreferences()
+ const { preferences, update, reset, loading } = usePreferences()
+ const {
+ changeLanguage,
+ currentLanguage,
+ currentLocale,
+ supportedLanguages,
+ localeProfile,
+ regionalContent,
+ formatDateTime,
+ formatNumber,
+ formatCurrency,
+ isRTL,
+ } = useI18nContext()
const [activeTab, setActiveTab] = useState('general')
const [saved, setSaved] = useState(false)
const [shareToken, setShareToken] = useState('')
@@ -47,42 +43,9 @@ export default function UserPreferences({ onClose }) {
setTimeout(() => setSaved(false), 1500)
}
- const handleSavePreferences = async (nextPreferences) => {
- await save(nextPreferences)
- setSaved(true)
- setTimeout(() => setSaved(false), 1500)
- }
-
- const handlePreset = async (presetId) => {
- await handleSavePreferences(applyPreferencePreset(presetId, preferences))
- }
-
- const downloadText = (filename, content, type) => {
- const blob = new Blob([content], { type })
- const url = URL.createObjectURL(blob)
- const link = document.createElement('a')
- link.href = url
- link.download = filename
- link.click()
- URL.revokeObjectURL(url)
- }
-
- const handleExport = () => {
- downloadText('stellar-preferences.json', exportPreferences(preferences), 'application/json')
- }
-
- const handleImport = async () => {
- const payload = prompt('Paste exported preferences JSON')
- if (!payload) return
- await handleSavePreferences(importPreferences(payload))
- }
-
- const handleSharePreset = () => {
- const preset = createPreferencePreset('Shared preferences', preferences, {
- description: 'Shared dashboard preference preset',
- shared: true,
- })
- setShareToken(sharePreferencePreset(preset))
+ const handleLanguageChange = async (languageCode) => {
+ await changeLanguage(languageCode)
+ await handleChange('language', languageCode)
}
if (loading) {
@@ -179,16 +142,38 @@ export default function UserPreferences({ onClose }) {
+
+
+
+ {localeProfile.label}
+
+
+
+
+
+
+
+
+ {label}
+ {value}
+
+ )
+}
+
function Toggle({ checked, onChange }) {
return (