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>
143 lines
4.1 KiB
TypeScript
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;
|
|
}
|