SkateSpots/src/contexts/LanguageContext.tsx
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

143 lines
4.1 KiB
TypeScript

import { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
import { useLocalStorage } from '@/hooks/useLocalStorage';
// Supported languages
export type Language = 'en' | 'es';
// Translation key type - allows nested keys like 'nav.home'
type TranslationKey = string;
// Translation values can be strings or nested objects
type TranslationValue = string | { [key: string]: TranslationValue };
// Translations structure
export type Translations = {
[key: string]: TranslationValue;
};
interface LanguageContextType {
/** Current language */
language: Language;
/** Set the current language */
setLanguage: (lang: Language) => void;
/** Translate a key - supports dot notation for nested keys */
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
/** Check if current language is RTL (for future Arabic support, etc.) */
isRTL: boolean;
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
// Import translations
import en from '@/i18n/en';
import es from '@/i18n/es';
const translations: Record<Language, Translations> = { en, es };
/**
* Get nested value from object using dot notation
* e.g., getNestedValue({ nav: { home: 'Home' } }, 'nav.home') => 'Home'
*/
function getNestedValue(obj: Translations, path: string): string | undefined {
const keys = path.split('.');
let current: TranslationValue = obj;
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
} else {
return undefined;
}
}
return typeof current === 'string' ? current : undefined;
}
/**
* Replace parameters in translation string
* e.g., interpolate('Hello {{name}}!', { name: 'World' }) => 'Hello World!'
*/
function interpolate(str: string, params?: Record<string, string | number>): string {
if (!params) return str;
return str.replace(/\{\{(\w+)\}\}/g, (_, key) => {
return params[key]?.toString() ?? `{{${key}}}`;
});
}
interface LanguageProviderProps {
children: ReactNode;
}
export function LanguageProvider({ children }: LanguageProviderProps) {
const [storedLanguage, setStoredLanguage] = useLocalStorage<Language>('app-language', 'en');
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: TranslationKey, 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;
return (
<LanguageContext.Provider value={{ language, setLanguage, t, isRTL }}>
{children}
</LanguageContext.Provider>
);
}
/**
* Hook to access translation functions and language state
*/
export function useTranslation() {
const context = useContext(LanguageContext);
if (context === undefined) {
throw new Error('useTranslation must be used within a LanguageProvider');
}
return context;
}
/**
* Hook to get just the translation function (convenience)
*/
export function useT() {
const { t } = useTranslation();
return t;
}