SkateSpots/src/contexts/LanguageContext.tsx
SkateSpots 9357386bbd i18n(es): batch 3 part 2 — locale-aware dates & relative times
- New src/lib/dateFnsI18n.ts re-exports date-fns with formatDistanceToNow /
  formatDistanceToNowStrict / formatDistance / format defaulting to the active
  locale, plus dateLocaleCode() for native toLocale{Date,Time}String calls.
- LanguageProvider keeps the locale in sync during render, so the first paint is
  already in-language.
- Codemod: 11 date-fns imports now use the shim; ~39 native date/time calls pass
  the app locale code instead of undefined/[]/hardcoded 'en-US'. Number
  .toLocaleString() calls are deliberately untouched.
2026-10-02 15:19:38 +07:00

87 lines
2.8 KiB
TypeScript

import { useCallback, useEffect, useState, type ReactNode } from 'react';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import {
LanguageContext,
getNestedValue,
interpolate,
translations,
type Language,
} from './language';
import { setDateLocale } from '@/lib/dateFnsI18n';
interface LanguageProviderProps {
children: ReactNode;
}
/**
* Pick the starting language from the visitor's browser on their FIRST visit.
* A device set to Spanish (`es-ES`, `es-MX`, …) lands in Castellano; anything
* else falls back to English. Once the user taps the language toggle their
* choice is stored and always wins.
*/
function detectLanguage(): Language {
if (typeof navigator === 'undefined') return 'en';
const tags = [navigator.language, ...(navigator.languages ?? [])];
return tags.some((tag) => tag?.toLowerCase().startsWith('es')) ? 'es' : 'en';
}
/**
* Provides language state + translation. The context, types and hooks live in
* `./language` so this file exports only the component (React Fast Refresh).
*/
export function LanguageProvider({ children }: LanguageProviderProps) {
const [storedLanguage, setStoredLanguage] = useLocalStorage<Language>('app-language', detectLanguage());
const [language, setLanguageState] = useState<Language>(storedLanguage);
// Sync with localStorage
useEffect(() => {
setLanguageState(storedLanguage);
}, [storedLanguage]);
const setLanguage = useCallback((lang: Language) => {
setLanguageState(lang);
setStoredLanguage(lang);
// Update HTML lang attribute for accessibility
document.documentElement.lang = lang;
}, [setStoredLanguage]);
// Set initial HTML lang attribute
useEffect(() => {
document.documentElement.lang = language;
}, [language]);
const t = useCallback((key: string, params?: Record<string, string | number>): string => {
// Try to get translation in current language
const translation = getNestedValue(translations[language], key);
if (translation) {
return interpolate(translation, params);
}
// Fallback to English
if (language !== 'en') {
const fallback = getNestedValue(translations.en, key);
if (fallback) {
return interpolate(fallback, params);
}
}
// Return key if no translation found (helps identify missing translations)
console.warn(`Missing translation for key: ${key}`);
return key;
}, [language]);
// Currently no RTL languages supported, but ready for future
const isRTL = false;
// Keep date-fns + native toLocale* formatting in the app language. Called
// during render (before children) so the first paint is already in-language;
// it is an idempotent module assignment, not React state.
setDateLocale(language);
return (
<LanguageContext.Provider value={{ language, setLanguage, t, isRTL }}>
{children}
</LanguageContext.Provider>
);
}