Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at natb/reorg-docs 250 lines 7.6 kB view raw
1// TypeScript i18next configuration with Fluent and manifest integration 2// modified from https://github.com/inaturalist/iNaturalistReactNative/blob/main/src/i18n/initI18next.js 3 4import i18next from "i18next"; 5import Fluent from "i18next-fluent"; 6import resourcesToBackend from "i18next-resources-to-backend"; 7import "intl-pluralrules"; 8import { initReactI18next } from "react-i18next"; 9 10// Import our manifest directly to avoid circular dependency 11import manifest from "../../locales/manifest.json"; 12import storage from "../storage"; 13 14const LOCALE_STORAGE_KEY = "@streamplace/locale"; 15 16// Try to import expo-localization, but make it optional 17let Localization: typeof import("expo-localization") | null = null; 18try { 19 const localizationModule = require("expo-localization"); 20 // Handle both default and named exports 21 Localization = localizationModule.default 22 ? localizationModule.default 23 : localizationModule; 24} catch { 25 // expo-localization not available, will use browser/fallback detection 26} 27 28function cleanLocaleName(locale: string): string { 29 return locale.replace("_", "-").replace(/@.*/, ""); 30} 31 32export function getLocaleFromSystemLocale(): string { 33 let systemLocale = "en"; 34 35 // Try to get locale from expo-localization if available 36 if (Localization && typeof Localization.getLocales === "function") { 37 try { 38 const locales = Localization.getLocales(); 39 systemLocale = locales?.[0]?.languageTag || "en"; 40 } catch (error) { 41 console.warn("Failed to get locales from expo-localization:", error); 42 } 43 } else if (typeof navigator !== "undefined" && navigator.language) { 44 // Fallback to browser navigator.language 45 systemLocale = navigator.language; 46 } 47 48 const candidateLocale = cleanLocaleName(systemLocale); 49 50 // Check if the full locale is supported (e.g., "en-US") 51 if (manifest.supportedLocales.includes(candidateLocale)) { 52 return candidateLocale; 53 } 54 55 // Check if the language part is supported (e.g., "en" from "en-GB") 56 const lang = candidateLocale.split("-")[0]; 57 const matchingLocale = manifest.supportedLocales.find((locale) => 58 locale.startsWith(lang + "-"), 59 ); 60 61 if (matchingLocale) { 62 return matchingLocale; 63 } 64 65 // Fall back to default locale from manifest 66 return manifest.fallbackChain[0]; 67} 68 69// Cache for the current locale to avoid async lookups 70let cachedLocale: string | null = null; 71 72export async function getCurrentLocale(): Promise<string> { 73 if (cachedLocale) { 74 return cachedLocale; 75 } 76 77 const stored = await storage.getItem(LOCALE_STORAGE_KEY); 78 if (stored && manifest.supportedLocales.includes(stored)) { 79 cachedLocale = stored; 80 return stored; 81 } 82 83 const systemLocale = getLocaleFromSystemLocale(); 84 cachedLocale = systemLocale; 85 return systemLocale; 86} 87 88// Synchronous version for initial load - returns cached or system locale 89export function getCurrentLocaleSync(): string { 90 return cachedLocale || getLocaleFromSystemLocale(); 91} 92 93// Enhanced fallback logic using manifest 94function getFallbackChain(code: string): string[] { 95 const fallbacks: string[] = []; 96 97 if (!code) return manifest.fallbackChain; 98 99 // Regional fallbacks 100 if (code.match(/^es-/)) { 101 fallbacks.push("es-ES"); // Spanish fallback 102 } else if (code.match(/^fr-/)) { 103 fallbacks.push("fr-FR"); // French fallback 104 } else if (code.match(/^pt-/)) { 105 fallbacks.push("pt-BR"); // Portuguese fallback 106 } else if (code.match(/^zh-/)) { 107 fallbacks.push("zh-Hant"); // Chinese fallback 108 } 109 110 // Add manifest fallback chain 111 return [...fallbacks, ...manifest.fallbackChain]; 112} 113 114// Use sync version for initial config - will be updated when storage loads 115const LOCALE = getCurrentLocaleSync(); 116 117export const I18NEXT_CONFIG = { 118 lng: LOCALE, 119 ns: ["common", "settings"], // Common should be first as it's most frequently used 120 defaultNS: "common", 121 interpolation: { 122 escapeValue: false, // React already safes from XSS 123 }, 124 react: { 125 useSuspense: false, // Prevent Android crashes 126 }, 127 i18nFormat: { 128 fluentBundleOptions: { 129 useIsolating: false, 130 functions: { 131 VOWORCON: ([txt]: [string]) => 132 "aeiou".indexOf(txt[0].toLowerCase()) >= 0 ? "vow" : "con", 133 JOIN: (args: string[], opts: { separator?: string } = {}) => 134 args 135 .filter(Boolean) 136 .filter((s) => typeof s === "string") 137 .join(opts.separator || ""), 138 }, 139 }, 140 }, 141 load: "currentOnly", 142 cleanCode: true, 143 fallbackLng: getFallbackChain, 144 supportedLngs: [...manifest.supportedLocales], 145 debug: process.env.NODE_ENV === "development", 146}; 147 148// Import platform-specific translation loader 149// Metro will use i18n-loader.native.ts for React Native, i18n-loader.ts for web 150import { loadTranslationData as platformLoadTranslationData } from "./i18n-loader"; 151 152// Translation loading function with error handling 153async function loadTranslationData( 154 locale: string, 155 namespace: string, 156): Promise<any> { 157 try { 158 const translations = await platformLoadTranslationData(locale, namespace); 159 return translations; 160 } catch (error: any) { 161 console.error( 162 `Failed to load ${namespace} translations for ${locale}:`, 163 error, 164 ); 165 // Return minimal fallback 166 return { 167 loading: "Loading...", 168 error: "Error", 169 cancel: "Cancel", 170 }; 171 } 172} 173 174// Initialize i18next with our configuration 175let initPromise: Promise<typeof i18next> | null = null; 176 177export default async function initI18next( 178 config: any = {}, 179): Promise<typeof i18next> { 180 // Return existing promise if already initializing 181 if (initPromise) { 182 return initPromise; 183 } 184 185 // Load stored locale from storage first 186 const storedLocale = await getCurrentLocale(); 187 188 const finalConfig = { 189 ...I18NEXT_CONFIG, 190 lng: storedLocale, 191 ...config, 192 }; 193 194 initPromise = i18next 195 .use(initReactI18next) 196 .use(Fluent) 197 .use( 198 resourcesToBackend((locale: string, namespace: string, callback: any) => { 199 // Load translations using our manifest-based namespace system 200 loadTranslationData(locale, namespace) 201 .then((translations) => callback(null, translations)) 202 .catch((error) => callback(error, null)); 203 }), 204 ) 205 .init(finalConfig) 206 .then(() => { 207 // Automatically persist language changes to storage 208 i18next.on("languageChanged", (lng) => { 209 if (lng && manifest.supportedLocales.includes(lng)) { 210 cachedLocale = lng; 211 storage.setItem(LOCALE_STORAGE_KEY, lng); 212 } 213 }); 214 return i18next; 215 }); 216 217 return initPromise; 218} 219 220// Utility functions for language management 221export async function changeLanguage(locale: string): Promise<void> { 222 // Storage is handled automatically via languageChanged event 223 await i18next.changeLanguage(locale); 224} 225 226export function getCurrentLanguage(): string { 227 // Return the cached locale preference, not i18next's resolved language 228 // i18next.language may return just "zh" instead of "zh-Hant" 229 return getCurrentLocaleSync(); 230} 231 232export function getSupportedLocales(): string[] { 233 return [...manifest.supportedLocales]; 234} 235 236export function getLanguageInfo(locale: string): any { 237 return manifest.languages[locale] || null; 238} 239 240export function isLocaleSupported(locale: string): boolean { 241 return manifest.supportedLocales.includes(locale); 242} 243 244// Auto-initialize i18next on module load 245// This ensures the instance is ready when used in providers 246initI18next().catch((error) => { 247 console.error("Failed to auto-initialize i18n:", error); 248}); 249 250export { i18next };