Live video on the AT Protocol

base i18n framework

authored by

Natalie B. and committed by
Natalie Bridgers
20ea93db 87db57eb

+2646 -47
js/app/components/provider/NewStreamplaceProvider.tsx

This is a binary file and will not be displayed.

+18 -11
js/app/components/provider/provider.shared.tsx
··· 6 6 import * as Sentry from "@sentry/react-native"; 7 7 import { 8 8 ThemeProvider, 9 + DirectI18nProvider, 9 10 StreamplaceProvider as ZustandStreamplaceProvider, 10 11 } from "@streamplace/components"; 11 12 import { useFonts } from "expo-font"; ··· 87 88 return ( 88 89 <SafeAreaProvider> 89 90 <ThemeProvider forcedTheme="dark"> 90 - <NavigationContainer theme={SPDarkTheme} linking={linking}> 91 - <ReduxProvider store={store}> 92 - <StreamplaceProvider> 93 - <BlueskyProvider> 94 - <NewStreamplaceProvider> 95 - <FontProvider>{children}</FontProvider> 96 - </NewStreamplaceProvider> 97 - </BlueskyProvider> 98 - </StreamplaceProvider> 99 - </ReduxProvider> 100 - </NavigationContainer> 91 + <DirectI18nProvider 92 + enableDynamicLoading={true} 93 + preloadAll={true} 94 + debug={process.env.NODE_ENV === "development"} 95 + > 96 + <NavigationContainer theme={SPDarkTheme} linking={linking}> 97 + <ReduxProvider store={store}> 98 + <StreamplaceProvider> 99 + <BlueskyProvider> 100 + <NewStreamplaceProvider> 101 + <FontProvider>{children}</FontProvider> 102 + </NewStreamplaceProvider> 103 + </BlueskyProvider> 104 + </StreamplaceProvider> 105 + </ReduxProvider> 106 + </NavigationContainer> 107 + </DirectI18nProvider> 101 108 </ThemeProvider> 102 109 </SafeAreaProvider> 103 110 );
+49 -33
js/app/components/settings/settings.tsx
··· 2 2 import { 3 3 Button, 4 4 Input, 5 + Localized, 5 6 Text, 6 - useToast, 7 7 View, 8 8 zero, 9 9 } from "@streamplace/components"; ··· 28 28 const defaultUrl = DEFAULT_URL; 29 29 const [newUrl, setNewUrl] = useState(""); 30 30 const [overrideEnabled, setOverrideEnabled] = useState(false); 31 - const t = useToast(); 32 31 33 32 // are we logged in? 34 33 const loggedIn = useAppSelector( ··· 93 92 ]} 94 93 > 95 94 <View style={[{ flex: 1 }, { paddingRight: 12 }]}> 96 - <Text size="xl">Use Custom Node</Text> 97 - <Text size="lg" color="muted"> 98 - Default: {defaultUrl} 99 - </Text> 95 + <Localized id="use-custom-node"> 96 + <Text size="xl">Use Custom Node</Text> 97 + </Localized> 98 + <Localized id="default-url" vars={{ url: defaultUrl }}> 99 + <Text size="lg" color="muted"> 100 + Default: {defaultUrl} 101 + </Text> 102 + </Localized> 100 103 </View> 101 104 <Switch 102 105 value={overrideEnabled} ··· 115 118 ]} 116 119 > 117 120 <View style={{ flex: 1 }}> 118 - <Input 119 - value={newUrl} 120 - containerStyle={[ 121 - { flex: 1, flexGrow: 1, width: "100%" }, 122 - zero.flex.grow[1], 123 - ]} 124 - variant="default" 125 - numberOfLines={1} 126 - multiline={false} 127 - placeholder={url || "Enter custom node URL"} 128 - placeholderTextColor="#999" 129 - onChangeText={setNewUrl} 130 - onSubmitEditing={onSubmitUrl} 131 - textContentType="URL" 132 - autoCapitalize="none" 133 - autoCorrect={false} 134 - keyboardType="url" 135 - /> 121 + <Localized 122 + id="enter-custom-node-url" 123 + attrs={{ placeholder: true }} 124 + > 125 + <Input 126 + value={newUrl} 127 + containerStyle={[ 128 + { flex: 1, flexGrow: 1, width: "100%" }, 129 + zero.flex.grow[1], 130 + ]} 131 + variant="default" 132 + numberOfLines={1} 133 + multiline={false} 134 + placeholder={url || undefined} 135 + placeholderTextColor="#999" 136 + onChangeText={setNewUrl} 137 + onSubmitEditing={onSubmitUrl} 138 + textContentType="URL" 139 + autoCapitalize="none" 140 + autoCorrect={false} 141 + keyboardType="url" 142 + /> 143 + </Localized> 136 144 </View> 137 145 <Button size="md" variant="secondary" onPress={onSubmitUrl}> 138 - <Text size="lg">Save</Text> 146 + <Localized id="save-button"> 147 + <Text size="lg">Save</Text> 148 + </Localized> 139 149 </Button> 140 150 </View> 141 151 </View> ··· 164 174 }, 165 175 ]} 166 176 > 167 - <Text>Manage Keys</Text> 177 + <Localized id="manage-keys"> 178 + <Text>Manage Keys</Text> 179 + </Localized> 168 180 <Text style={[{ fontSize: 16 }]}>→</Text> 169 181 </View> 170 182 </AQLink> ··· 207 219 ]} 208 220 > 209 221 <View style={[{ flex: 1 }, { paddingRight: 12 }]}> 210 - <Text size="xl"> 211 - Allow {u.host} to record your livestream for debugging and improving 212 - the service 213 - </Text> 214 - <Text size="lg" color="muted"> 215 - Optional 216 - </Text> 222 + <Localized id="debug-recording-title" vars={{ host: u.host }}> 223 + <Text size="xl"> 224 + Allow {u.host} to record your livestream for debugging and 225 + improving the service 226 + </Text> 227 + </Localized> 228 + <Localized id="debug-recording-description"> 229 + <Text size="lg" color="muted"> 230 + Optional 231 + </Text> 232 + </Localized> 217 233 </View> 218 234 <Switch 219 235 value={debugRecordingOn}
+1 -1
js/app/components/settings/updates.tsx
··· 14 14 > 15 15 <View> 16 16 <Text 17 + size="2xl" 17 18 style={[ 18 19 { 19 - fontSize: 24, 20 20 fontWeight: "bold", 21 21 textAlign: "center", 22 22 color: "#fff",
+6 -1
js/app/package.json
··· 17 17 "prepare": "which pod && pnpm run prepare-ios || echo 'not a mac, not installing pods'", 18 18 "prepare-ios": "cd ios && pod install && pnpm run find-node", 19 19 "find-node": "node -p '`NODE_BINARY=${process.argv[0]}`' > ios/.xcode.env.local", 20 - "code-signing-dev": "mkdir -p code-signing-dev/keys code-signing-dev/certs && expo-updates codesigning:generate --key-output-directory code-signing-dev/keys --certificate-output-directory code-signing-dev/certs --certificate-validity-duration-years 10 --certificate-common-name 'Streamplace'" 20 + "code-signing-dev": "mkdir -p code-signing-dev/keys code-signing-dev/certs && expo-updates codesigning:generate --key-output-directory code-signing-dev/keys --certificate-output-directory code-signing-dev/certs --certificate-validity-duration-years 10 --certificate-common-name 'Streamplace'", 21 + "i18n:compile": "cd ../components && pnpm run i18n:compile", 22 + "i18n:watch": "cd ../components && pnpm run i18n:watch" 21 23 }, 22 24 "jest": { 23 25 "preset": "jest-expo" ··· 32 34 "@atproto/oauth-client-browser": "^0.3.31", 33 35 "@bacons/text-decoder": "^0.0.0", 34 36 "@emoji-mart/react": "^1.1.1", 37 + "@fluent/bundle": "^0.19.1", 38 + "@fluent/langneg": "^0.7.0", 39 + "@fluent/react": "^0.15.2", 35 40 "@react-native-firebase/app": "^22.2.1", 36 41 "@react-native-firebase/messaging": "^22.2.1", 37 42 "@react-navigation/bottom-tabs": "^6.6.1",
+6 -1
js/components/package.json
··· 24 24 "@atproto/api": "^0.16.7", 25 25 "@atproto/crypto": "^0.4.4", 26 26 "@emoji-mart/react": "^1.1.1", 27 + "@fluent/bundle": "^0.19.1", 28 + "@fluent/langneg": "^0.7.0", 29 + "@fluent/react": "^0.15.2", 27 30 "@gorhom/bottom-sheet": "^5.1.6", 28 31 "@rn-primitives/dropdown-menu": "^1.2.0", 29 32 "@rn-primitives/portal": "^1.3.0", ··· 54 57 "scripts": { 55 58 "build": "tsc", 56 59 "start": "tsc --watch --preserveWatchOutput", 57 - "prepare": "tsc" 60 + "prepare": "tsc", 61 + "i18n:compile": "node scripts/compile-translations.js", 62 + "i18n:watch": "nodemon scripts/compile-translations.js --watch src/i18n/locales/data/**/*.ftl" 58 63 } 59 64 }
+467
js/components/scripts/compile-translations.js
··· 1 + #!/usr/bin/env node 2 + 3 + const fs = require("fs"); 4 + const path = require("path"); 5 + 6 + // Load language manifest 7 + const MANIFEST_PATH = path.join( 8 + __dirname, 9 + "..", 10 + "src", 11 + "i18n", 12 + "manifest.json", 13 + ); 14 + const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf-8")); 15 + 16 + // Configuration 17 + const LOCALES_DIR = path.join( 18 + __dirname, 19 + "..", 20 + "src", 21 + "i18n", 22 + "locales", 23 + "data", 24 + ); 25 + const LOCALES_BASE_DIR = path.join(__dirname, "..", "src", "i18n", "locales"); 26 + const OUTPUT_FILENAME = "messages.js"; 27 + const INDEX_FILENAME = "index.ts"; 28 + const TRANSLATIONS_FILENAME = "translations.ts"; 29 + const LANGUAGE_CONFIG_FILENAME = "language-config.ts"; 30 + 31 + /** 32 + * Recursively find all .ftl files in a directory 33 + */ 34 + function findFtlFiles(dir) { 35 + const files = []; 36 + const entries = fs.readdirSync(dir, { withFileTypes: true }); 37 + 38 + for (const entry of entries) { 39 + const fullPath = path.join(dir, entry.name); 40 + if (entry.isDirectory()) { 41 + files.push(...findFtlFiles(fullPath)); 42 + } else if (entry.isFile() && entry.name.endsWith(".ftl")) { 43 + files.push(fullPath); 44 + } 45 + } 46 + 47 + return files; 48 + } 49 + 50 + /** 51 + * Read and combine all .ftl files for a locale 52 + */ 53 + function combineLocaleFiles(localeDir) { 54 + const ftlFiles = findFtlFiles(localeDir); 55 + 56 + if (ftlFiles.length === 0) { 57 + console.warn(`⚠️ No .ftl files found in ${localeDir}`); 58 + return ""; 59 + } 60 + 61 + const contents = []; 62 + 63 + for (const filePath of ftlFiles) { 64 + try { 65 + const content = fs.readFileSync(filePath, "utf-8"); 66 + const relativePath = path.relative(localeDir, filePath); 67 + 68 + contents.push(`# === ${relativePath} ===`); 69 + contents.push(content.trim()); 70 + contents.push(""); // Empty line separator 71 + } catch (error) { 72 + console.error(`❌ Error reading ${filePath}:`, error.message); 73 + } 74 + } 75 + 76 + return contents.join("\n"); 77 + } 78 + 79 + /** 80 + * Generate a JavaScript module that exports the translation content 81 + */ 82 + function generateJsModule(content) { 83 + // Escape backticks and ${} for template literal safety 84 + const escapedContent = content.replace(/`/g, "\\`").replace(/\$\{/g, "\\${"); 85 + 86 + return `// Auto-generated by compile-translations.js 87 + // Do not edit this file manually - it will be overwritten 88 + 89 + export const messages = \`${escapedContent}\`; 90 + 91 + // Export for CommonJS compatibility 92 + if (typeof module !== 'undefined' && module.exports) { 93 + module.exports = { messages }; 94 + } 95 + `; 96 + } 97 + 98 + /** 99 + * Generate Metro-compatible index.ts file 100 + */ 101 + function generateIndexFile(locales) { 102 + const camelCaseNames = locales.map((locale) => { 103 + return locale.replace(/-/g, "").toLowerCase() + "Messages"; 104 + }); 105 + 106 + const imports = locales 107 + .map((locale, index) => { 108 + const varName = camelCaseNames[index]; 109 + 110 + // Fallback texts in various languages 111 + // Use simple English fallback for all locales 112 + const fallbackContent = 113 + "# Fallback\\nloading = Loading...\\nerror = Error\\ncancel = Cancel\\nsettings-title = Settings"; 114 + 115 + return `try { 116 + // Try to import compiled translation modules 117 + ${varName} = require("./data/${locale}/messages.js"); 118 + } catch (error) { 119 + console.warn("[locale-mapping] Failed to load ${locale} translations:", error); 120 + ${varName} = { 121 + messages: ${JSON.stringify(fallbackContent)}, 122 + }; 123 + }`; 124 + }) 125 + .join("\n\n"); 126 + 127 + const varDeclarations = camelCaseNames 128 + .map((name) => `let ${name}: { messages: string };`) 129 + .join("\n"); 130 + 131 + const exportObj = locales 132 + .map((locale, index) => ` "${locale}": ${camelCaseNames[index]},`) 133 + .join("\n"); 134 + 135 + const individualExports = camelCaseNames.join(", "); 136 + const supportedLocalesArray = locales.map((l) => `"${l}"`).join(", "); 137 + 138 + return `// Auto-generated by compile-translations.js 139 + // Do not edit this file manually - it will be overwritten 140 + 141 + ${varDeclarations} 142 + 143 + ${imports} 144 + 145 + // Export locale mapping for metro-loader 146 + export const localeMessages = { 147 + ${exportObj} 148 + } as const; 149 + 150 + export { ${individualExports} }; 151 + 152 + export const supportedLocales = [${supportedLocalesArray}] as const; 153 + export type SupportedLocale = (typeof supportedLocales)[number]; 154 + 155 + export function validateLocaleModule( 156 + locale: string, 157 + module: any, 158 + ): module is { messages: string } { 159 + return ( 160 + module && 161 + typeof module === "object" && 162 + "messages" in module && 163 + typeof module.messages === "string" && 164 + module.messages.length > 0 165 + ); 166 + } 167 + 168 + export function getAvailableLocales(): SupportedLocale[] { 169 + return supportedLocales.filter((locale) => { 170 + const module = localeMessages[locale]; 171 + return validateLocaleModule(locale, module); 172 + }); 173 + } 174 + `; 175 + } 176 + 177 + /** 178 + * Generate language configuration file with locale metadata 179 + */ 180 + function generateLanguageConfigFile(locales) { 181 + // Basic language configuration mapping 182 + const languageConfig = { 183 + "en-US": { 184 + code: "en-US", 185 + name: "English", 186 + nativeName: "English", 187 + flag: "🇺🇸", 188 + fallback: "# Fallback\\nloading = Loading...\\nerror = Error", 189 + }, 190 + "pt-BR": { 191 + code: "pt-BR", 192 + name: "Portuguese", 193 + nativeName: "Português", 194 + flag: "🇧🇷", 195 + fallback: "# Fallback\\nloading = Carregando...\\nerror = Erro", 196 + }, 197 + "es-ES": { 198 + code: "es-ES", 199 + name: "Spanish", 200 + nativeName: "Español", 201 + flag: "🇪🇸", 202 + fallback: "# Fallback\\nloading = Cargando...\\nerror = Error", 203 + }, 204 + "zh-TW": { 205 + code: "zh-TW", 206 + name: "Chinese Traditional", 207 + nativeName: "繁體中文", 208 + flag: "🇹🇼", 209 + fallback: "# Fallback\\nloading = 載入中...\\nerror = 錯誤", 210 + }, 211 + "fr-FR": { 212 + code: "fr-FR", 213 + name: "French", 214 + nativeName: "Français", 215 + flag: "🇫🇷", 216 + fallback: "# Fallback\\nloading = Chargement...\\nerror = Erreur", 217 + }, 218 + "de-DE": { 219 + code: "de-DE", 220 + name: "German", 221 + nativeName: "Deutsch", 222 + flag: "🇩🇪", 223 + fallback: "# Fallback\\nloading = Wird geladen...\\nerror = Fehler", 224 + }, 225 + "ja-JP": { 226 + code: "ja-JP", 227 + name: "Japanese", 228 + nativeName: "日本語", 229 + flag: "🇯🇵", 230 + fallback: "# Fallback\\nloading = 読み込み中...\\nerror = エラー", 231 + }, 232 + "ko-KR": { 233 + code: "ko-KR", 234 + name: "Korean", 235 + nativeName: "한국어", 236 + flag: "🇰🇷", 237 + fallback: "# Fallback\\nloading = 로딩 중...\\nerror = 오류", 238 + }, 239 + }; 240 + 241 + const languageInfoEntries = locales 242 + .map((locale) => { 243 + const config = languageConfig[locale] || { 244 + code: locale, 245 + name: locale.replace("-", " "), 246 + nativeName: locale.replace("-", " "), 247 + flag: "🌍", 248 + fallback: "# Fallback\\nloading = Loading...\\nerror = Error", 249 + }; 250 + 251 + return ` "${locale}": { 252 + code: "${config.code}", 253 + name: "${config.name}", 254 + nativeName: "${config.nativeName}", 255 + flag: "${config.flag}", 256 + },`; 257 + }) 258 + .join("\n"); 259 + 260 + const defaultTranslationsEntries = locales 261 + .map((locale) => { 262 + const config = languageConfig[locale] || { 263 + fallback: "# Fallback\\nloading = Loading...\\nerror = Error", 264 + }; 265 + 266 + return ` "${locale}": \`${config.fallback}\`,`; 267 + }) 268 + .join("\n"); 269 + 270 + return `// Auto-generated by compile-translations.js 271 + // Do not edit this file manually - it will be overwritten 272 + 273 + import type { SupportedLocale } from "./index"; 274 + 275 + export interface LanguageInfo { 276 + code: SupportedLocale; 277 + name: string; 278 + nativeName: string; 279 + flag: string; 280 + } 281 + 282 + export const LANGUAGE_INFO: Record<SupportedLocale, LanguageInfo> = { 283 + ${languageInfoEntries} 284 + } as const; 285 + 286 + export const DEFAULT_TRANSLATIONS: Record<SupportedLocale, string> = { 287 + ${defaultTranslationsEntries} 288 + } as const; 289 + 290 + export function getLanguageInfo(locale: SupportedLocale): LanguageInfo { 291 + return LANGUAGE_INFO[locale]; 292 + } 293 + 294 + export function getDefaultTranslation(locale: SupportedLocale): string { 295 + return DEFAULT_TRANSLATIONS[locale]; 296 + } 297 + 298 + export function getAllLanguageInfo(): LanguageInfo[] { 299 + return Object.values(LANGUAGE_INFO); 300 + } 301 + `; 302 + } 303 + 304 + /** 305 + * Generate translations.ts utility file 306 + */ 307 + function generateTranslationsFile(locales) { 308 + const localeUnion = locales.map((l) => `"${l}"`).join(" | "); 309 + 310 + return `// Auto-generated by compile-translations.js 311 + // Do not edit this file manually - it will be overwritten 312 + 313 + import type { TranslationSource } from "../direct-provider"; 314 + import { 315 + dynamicActivate, 316 + loadAllLocales, 317 + loadLocale, 318 + } from "../loaders/metro-loader"; 319 + 320 + /** 321 + * Load all translations dynamically from compiled .ftl files 322 + * 323 + * Usage: 324 + * \`\`\`tsx 325 + * import { loadDynamicTranslations } from './locales/translations'; 326 + * 327 + * const App = () => ( 328 + * <DirectI18nProvider 329 + * enableDynamicLoading={true} 330 + * preloadAll={false} // Only load current locale 331 + * debug={true} 332 + * > 333 + * <YourApp /> 334 + * </DirectI18nProvider> 335 + * ); 336 + * \`\`\` 337 + */ 338 + export async function loadDynamicTranslations( 339 + debug = false, 340 + ): Promise<TranslationSource[]> { 341 + return loadAllLocales(debug); 342 + } 343 + 344 + /** 345 + * Load a specific locale dynamically 346 + * Useful for lazy loading when switching languages 347 + */ 348 + export async function loadDynamicLocale( 349 + locale: ${localeUnion}, 350 + debug = false, 351 + ) { 352 + return loadLocale(locale, debug); 353 + } 354 + 355 + /** 356 + * Activate a locale dynamically (similar to Lingui's approach) 357 + * This loads and activates a locale in one step 358 + */ 359 + export async function activateLocale( 360 + locale: ${localeUnion}, 361 + debug = false, 362 + ) { 363 + return dynamicActivate(locale, undefined, debug); 364 + } 365 + `; 366 + } 367 + 368 + /** 369 + * Main compilation function 370 + */ 371 + function compileTranslations() { 372 + console.log("🌍 Compiling translation files..."); 373 + 374 + if (!fs.existsSync(LOCALES_DIR)) { 375 + console.error(`❌ Locales directory not found: ${LOCALES_DIR}`); 376 + process.exit(1); 377 + } 378 + 379 + // Get supported locales from manifest, but only include those with actual data directories 380 + const manifestLocales = manifest.supportedLocales; 381 + const locales = manifestLocales.filter((locale) => { 382 + const localeDir = path.join(LOCALES_DIR, locale); 383 + return fs.existsSync(localeDir) && fs.statSync(localeDir).isDirectory(); 384 + }); 385 + 386 + if (locales.length === 0) { 387 + console.error(`❌ No locale directories found in ${LOCALES_DIR}`); 388 + process.exit(1); 389 + } 390 + 391 + let totalFiles = 0; 392 + 393 + // Process each locale 394 + for (const locale of locales) { 395 + const localeDir = path.join(LOCALES_DIR, locale); 396 + const outputPath = path.join(localeDir, OUTPUT_FILENAME); 397 + 398 + console.log(`📦 Processing locale: ${locale}`); 399 + 400 + // Combine all .ftl files for this locale 401 + const combinedContent = combineLocaleFiles(localeDir); 402 + 403 + if (!combinedContent.trim()) { 404 + console.warn(`⚠️ Skipping ${locale} - no content found`); 405 + continue; 406 + } 407 + 408 + // Generate JavaScript module 409 + const jsContent = generateJsModule(combinedContent); 410 + 411 + // Write the output file 412 + try { 413 + fs.writeFileSync(outputPath, jsContent, "utf-8"); 414 + console.log(`✅ Generated: ${path.relative(process.cwd(), outputPath)}`); 415 + totalFiles++; 416 + } catch (error) { 417 + console.error(`❌ Error writing ${outputPath}:`, error.message); 418 + } 419 + } 420 + 421 + // Generate Metro-compatible index.ts file 422 + const indexPath = path.join(LOCALES_BASE_DIR, INDEX_FILENAME); 423 + const indexContent = generateIndexFile(locales); 424 + 425 + try { 426 + fs.writeFileSync(indexPath, indexContent, "utf-8"); 427 + console.log(`✅ Generated: ${path.relative(process.cwd(), indexPath)}`); 428 + totalFiles++; 429 + } catch (error) { 430 + console.error(`❌ Error writing ${indexPath}:`, error.message); 431 + } 432 + 433 + // Generate translations.ts utility file 434 + const translationsPath = path.join(LOCALES_BASE_DIR, TRANSLATIONS_FILENAME); 435 + const translationsContent = generateTranslationsFile(locales); 436 + 437 + try { 438 + fs.writeFileSync(translationsPath, translationsContent, "utf-8"); 439 + console.log( 440 + `✅ Generated: ${path.relative(process.cwd(), translationsPath)}`, 441 + ); 442 + totalFiles++; 443 + } catch (error) { 444 + console.error(`❌ Error writing ${translationsPath}:`, error.message); 445 + } 446 + 447 + // Generate language configuration file 448 + const languageConfigPath = path.join( 449 + LOCALES_BASE_DIR, 450 + LANGUAGE_CONFIG_FILENAME, 451 + ); 452 + const languageConfigContent = generateLanguageConfigFile(locales); 453 + 454 + try { 455 + fs.writeFileSync(languageConfigPath, languageConfigContent, "utf-8"); 456 + console.log( 457 + `✅ Generated: ${path.relative(process.cwd(), languageConfigPath)}`, 458 + ); 459 + totalFiles++; 460 + } catch (error) { 461 + console.error(`❌ Error writing ${languageConfigPath}:`, error.message); 462 + } 463 + console.log(`🎉 Compilation complete! ${totalFiles} files generated.`); 464 + } 465 + 466 + // Run the compilation 467 + compileTranslations();
+244
js/components/src/i18n/direct-language-selector.tsx
··· 1 + import { useState } from "react"; 2 + import { ScrollView, Text, TouchableOpacity, View } from "react-native"; 3 + import { 4 + LANGUAGE_INFO as DIRECT_LANGUAGE_INFO, 5 + SupportedLocale as DirectSupportedLocale, 6 + Localized, 7 + useDirectI18n, 8 + } from "./direct-provider"; 9 + 10 + interface DirectLanguageSelectorProps { 11 + /** Display variant */ 12 + variant?: "compact" | "expanded"; 13 + /** Custom styling */ 14 + style?: any; 15 + /** Show loading indicator */ 16 + showLoading?: boolean; 17 + } 18 + 19 + /** 20 + * Simple language selector for DirectI18nProvider 21 + */ 22 + export function DirectLanguageSelector({ 23 + variant = "expanded", 24 + showLoading = true, 25 + style, 26 + }: DirectLanguageSelectorProps) { 27 + const { locale, changeLocale, isLoading } = useDirectI18n(); 28 + const [showDropdown, setShowDropdown] = useState(false); 29 + 30 + const currentLanguage = DIRECT_LANGUAGE_INFO[locale]; 31 + const availableLanguages = Object.values(DIRECT_LANGUAGE_INFO); 32 + 33 + const handleLanguageChange = (newLocale: DirectSupportedLocale) => { 34 + if (newLocale !== locale && !isLoading) { 35 + changeLocale(newLocale); 36 + setShowDropdown(false); 37 + } 38 + }; 39 + 40 + if (variant === "expanded") { 41 + return ( 42 + <View style={[style]}> 43 + <View style={{ marginBottom: 12 }}> 44 + <Text 45 + style={{ 46 + fontSize: 18, 47 + fontWeight: "600", 48 + marginBottom: 4, 49 + color: "white", 50 + }} 51 + > 52 + <Localized id="language-selection">Language</Localized> 53 + </Text> 54 + <Text style={{ fontSize: 14, opacity: 0.7, color: "white" }}> 55 + <Localized id="language-selection-description"> 56 + Choose your preferred language 57 + </Localized> 58 + </Text> 59 + </View> 60 + 61 + <View style={{ gap: 8 }}> 62 + {availableLanguages.map((language) => ( 63 + <TouchableOpacity 64 + key={language.code} 65 + onPress={() => handleLanguageChange(language.code)} 66 + disabled={isLoading} 67 + style={{ 68 + flexDirection: "row", 69 + alignItems: "center", 70 + justifyContent: "space-between", 71 + padding: 12, 72 + borderRadius: 8, 73 + borderWidth: 1, 74 + borderColor: locale === language.code ? "#3b82f6" : "#d1d5db", 75 + backgroundColor: 76 + locale === language.code ? "#eff6ff" : "#ffffff", 77 + opacity: isLoading ? 0.6 : 1, 78 + }} 79 + > 80 + <View 81 + style={{ flexDirection: "row", alignItems: "center", gap: 12 }} 82 + > 83 + <Text style={{ fontSize: 20 }}>{language.flag}</Text> 84 + <View> 85 + <Text style={{ fontSize: 16, fontWeight: "500" }}> 86 + {language.nativeName} 87 + </Text> 88 + <Text style={{ fontSize: 14, opacity: 0.6 }}> 89 + {language.name} 90 + </Text> 91 + </View> 92 + </View> 93 + 94 + {locale === language.code && ( 95 + <View 96 + style={{ 97 + width: 12, 98 + height: 12, 99 + borderRadius: 6, 100 + backgroundColor: "#3b82f6", 101 + }} 102 + /> 103 + )} 104 + </TouchableOpacity> 105 + ))} 106 + </View> 107 + 108 + {showLoading && isLoading && ( 109 + <View style={{ marginTop: 8, alignItems: "center" }}> 110 + <Text style={{ fontSize: 12, opacity: 0.6 }}>Loading...</Text> 111 + </View> 112 + )} 113 + </View> 114 + ); 115 + } 116 + 117 + // Compact variant 118 + return ( 119 + <View style={[{ position: "relative" }, style]}> 120 + <TouchableOpacity 121 + onPress={() => !isLoading && setShowDropdown(!showDropdown)} 122 + disabled={isLoading} 123 + style={{ 124 + flexDirection: "row", 125 + alignItems: "center", 126 + justifyContent: "space-between", 127 + paddingHorizontal: 12, 128 + paddingVertical: 8, 129 + borderRadius: 6, 130 + borderWidth: 1, 131 + borderColor: "#d1d5db", 132 + backgroundColor: "#ffffff", 133 + minWidth: 120, 134 + opacity: isLoading ? 0.6 : 1, 135 + }} 136 + > 137 + <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}> 138 + <Text style={{ fontSize: 16 }}>{currentLanguage.flag}</Text> 139 + <Text style={{ fontSize: 14, fontWeight: "500" }}> 140 + {currentLanguage.nativeName} 141 + </Text> 142 + </View> 143 + 144 + <Text 145 + style={{ 146 + fontSize: 12, 147 + opacity: 0.6, 148 + transform: [{ rotate: showDropdown ? "180deg" : "0deg" }], 149 + }} 150 + > 151 + 152 + </Text> 153 + </TouchableOpacity> 154 + 155 + {showDropdown && ( 156 + <View 157 + style={{ 158 + position: "absolute", 159 + top: "100%", 160 + left: 0, 161 + right: 0, 162 + marginTop: 4, 163 + backgroundColor: "#ffffff", 164 + borderRadius: 6, 165 + borderWidth: 1, 166 + borderColor: "#d1d5db", 167 + shadowColor: "#000000", 168 + shadowOffset: { width: 0, height: 2 }, 169 + shadowOpacity: 0.1, 170 + shadowRadius: 4, 171 + elevation: 3, 172 + zIndex: 1000, 173 + maxHeight: 200, 174 + }} 175 + > 176 + <ScrollView style={{ maxHeight: 180 }}> 177 + {availableLanguages 178 + .filter((language) => language.code !== locale) 179 + .map((language, index, array) => ( 180 + <TouchableOpacity 181 + key={language.code} 182 + onPress={() => handleLanguageChange(language.code)} 183 + disabled={isLoading} 184 + style={{ 185 + flexDirection: "row", 186 + alignItems: "center", 187 + gap: 12, 188 + paddingHorizontal: 12, 189 + paddingVertical: 10, 190 + borderBottomWidth: index < array.length - 1 ? 1 : 0, 191 + borderBottomColor: "#f3f4f6", 192 + opacity: isLoading ? 0.6 : 1, 193 + }} 194 + > 195 + <Text style={{ fontSize: 16 }}>{language.flag}</Text> 196 + <View> 197 + <Text style={{ fontSize: 14, fontWeight: "500" }}> 198 + {language.nativeName} 199 + </Text> 200 + <Text style={{ fontSize: 12, opacity: 0.6 }}> 201 + {language.name} 202 + </Text> 203 + </View> 204 + </TouchableOpacity> 205 + ))} 206 + </ScrollView> 207 + </View> 208 + )} 209 + 210 + {showLoading && isLoading && ( 211 + <View 212 + style={{ 213 + position: "absolute", 214 + top: 0, 215 + right: -30, 216 + justifyContent: "center", 217 + height: "100%", 218 + }} 219 + > 220 + <Text style={{ fontSize: 10, opacity: 0.6 }}>...</Text> 221 + </View> 222 + )} 223 + </View> 224 + ); 225 + } 226 + 227 + /** 228 + * Simple language indicator that shows current locale 229 + */ 230 + export function DirectLanguageIndicator({ style }: { style?: any }) { 231 + const { locale } = useDirectI18n(); 232 + const currentLanguage = DIRECT_LANGUAGE_INFO[locale]; 233 + 234 + return ( 235 + <View 236 + style={[{ flexDirection: "row", alignItems: "center", gap: 6 }, style]} 237 + > 238 + <Text style={{ fontSize: 14 }}>{currentLanguage.flag}</Text> 239 + <Text style={{ fontSize: 12, opacity: 0.7 }}> 240 + {currentLanguage.nativeName} 241 + </Text> 242 + </View> 243 + ); 244 + }
+423
js/components/src/i18n/direct-provider.tsx
··· 1 + // Ultra-simple direct translation provider - no loading complexity 2 + import { FluentBundle, FluentResource } from "@fluent/bundle"; 3 + import { negotiateLanguages } from "@fluent/langneg"; 4 + import { LocalizationProvider, ReactLocalization } from "@fluent/react"; 5 + import React, { 6 + createContext, 7 + useCallback, 8 + useContext, 9 + useEffect, 10 + useMemo, 11 + useState, 12 + } from "react"; 13 + import { loadAllLocales, loadLocale } from "./loaders/dynamic-loader"; 14 + import type { SupportedLocale } from "./locales"; 15 + import { supportedLocales } from "./locales"; 16 + import manifest from "./manifest.json"; 17 + 18 + export type { SupportedLocale }; 19 + export const SUPPORTED_LOCALES = supportedLocales; 20 + export const DEFAULT_LOCALE: SupportedLocale = manifest 21 + .fallbackChain[0] as SupportedLocale; 22 + 23 + export interface LanguageInfo { 24 + code: SupportedLocale; 25 + name: string; 26 + nativeName: string; 27 + flag: string; 28 + fallback?: string; 29 + } 30 + 31 + // Extract language info from manifest for supported locales 32 + export const LANGUAGE_INFO: Record<SupportedLocale, LanguageInfo> = 33 + Object.fromEntries( 34 + supportedLocales.map((locale) => [ 35 + locale, 36 + { 37 + code: locale, 38 + name: manifest.languages[locale]?.name || locale, 39 + nativeName: manifest.languages[locale]?.nativeName || locale, 40 + flag: manifest.languages[locale]?.flag || "🌐", 41 + }, 42 + ]), 43 + ) as Record<SupportedLocale, LanguageInfo>; 44 + 45 + // Simple fallback for critical errors 46 + const MINIMAL_FALLBACK: string = ` 47 + loading = Loading... 48 + error = Error 49 + cancel = Cancel 50 + settings-title = Settings 51 + `; 52 + 53 + // Global translation registry for external sources 54 + const translationRegistry = new Map<SupportedLocale, string>(); 55 + 56 + // Translation source configuration 57 + export interface TranslationSource { 58 + locale: SupportedLocale; 59 + content: string; 60 + } 61 + 62 + interface DirectI18nContextValue { 63 + locale: SupportedLocale; 64 + changeLocale: (locale: SupportedLocale) => void; 65 + isLoading: boolean; 66 + } 67 + 68 + const DirectI18nContext = createContext<DirectI18nContextValue | null>(null); 69 + 70 + function getUserLocale(): SupportedLocale { 71 + try { 72 + const stored = 73 + typeof window !== "undefined" 74 + ? localStorage.getItem("@streamplace/locale") 75 + : null; 76 + 77 + if (stored && SUPPORTED_LOCALES.includes(stored as SupportedLocale)) { 78 + return stored as SupportedLocale; 79 + } 80 + 81 + const systemLocale = Intl.DateTimeFormat().resolvedOptions().locale; 82 + 83 + // Enhanced locale detection 84 + if (systemLocale.startsWith("pt")) return "pt-BR"; 85 + if (systemLocale.startsWith("es")) return "es-ES"; 86 + if (systemLocale.startsWith("zh-TW") || systemLocale === "zh-Hant-TW") 87 + return "zh-TW"; 88 + if (systemLocale.startsWith("en")) return "en-US"; 89 + 90 + const negotiated = negotiateLanguages([systemLocale], SUPPORTED_LOCALES, { 91 + defaultLocale: DEFAULT_LOCALE, 92 + }); 93 + 94 + return (negotiated[0] as SupportedLocale) || DEFAULT_LOCALE; 95 + } catch { 96 + return DEFAULT_LOCALE; 97 + } 98 + } 99 + 100 + function createBundles(locale: SupportedLocale): ReactLocalization { 101 + console.log(`[direct-i18n] Creating bundles for ${locale}`); 102 + 103 + const locales = 104 + locale === DEFAULT_LOCALE ? [locale] : [locale, DEFAULT_LOCALE]; 105 + 106 + function* generateBundles() { 107 + for (const localeCode of locales) { 108 + console.log(`[direct-i18n] Creating bundle for ${localeCode}`); 109 + 110 + const bundle = new FluentBundle(localeCode, { 111 + useIsolating: false, 112 + }); 113 + 114 + // Try external translations first, then fall back to minimal fallback 115 + const translationContent = 116 + translationRegistry.get(localeCode as SupportedLocale) || 117 + MINIMAL_FALLBACK; 118 + 119 + if (translationContent) { 120 + try { 121 + const resource = new FluentResource(translationContent); 122 + const errors = bundle.addResource(resource); 123 + 124 + if (errors.length > 0) { 125 + console.warn( 126 + `[direct-i18n] Bundle errors for ${localeCode}:`, 127 + errors, 128 + ); 129 + } 130 + 131 + console.log( 132 + `[direct-i18n] Added resource to ${localeCode} bundle (${translationContent.length} chars)`, 133 + ); 134 + } catch (error) { 135 + console.error( 136 + `[direct-i18n] Failed to add resource for ${localeCode}:`, 137 + error, 138 + ); 139 + } 140 + } 141 + 142 + yield bundle; 143 + } 144 + } 145 + 146 + const l10n = new ReactLocalization(generateBundles()); 147 + console.log(`[direct-i18n] Created ReactLocalization for ${locale}`); 148 + 149 + return l10n; 150 + } 151 + 152 + interface DirectI18nProviderProps { 153 + children: React.ReactNode; 154 + /** External translation sources */ 155 + translations?: TranslationSource[]; 156 + /** Enable debug logging */ 157 + debug?: boolean; 158 + /** Enable dynamic loading from compiled .ftl files */ 159 + enableDynamicLoading?: boolean; 160 + /** Preload all locales on mount (only with dynamic loading) */ 161 + preloadAll?: boolean; 162 + } 163 + 164 + export function DirectI18nProvider({ 165 + children, 166 + translations, 167 + debug = false, 168 + enableDynamicLoading = false, 169 + preloadAll = false, 170 + }: DirectI18nProviderProps) { 171 + const [locale, setLocale] = useState<SupportedLocale>(getUserLocale()); 172 + const [isLoading, setIsLoading] = useState(enableDynamicLoading); 173 + const [dynamicTranslations, setDynamicTranslations] = useState< 174 + TranslationSource[] 175 + >([]); 176 + 177 + // Initialize dynamic or static translations on mount 178 + useEffect(() => { 179 + const initializeTranslations = async () => { 180 + if (enableDynamicLoading) { 181 + if (debug) { 182 + console.log( 183 + `[direct-i18n] Initializing dynamic loading for ${locale}`, 184 + ); 185 + } 186 + 187 + try { 188 + if (preloadAll) { 189 + // Load all locales 190 + const allTranslations = await loadAllLocales(debug); 191 + setDynamicTranslations(allTranslations); 192 + 193 + allTranslations.forEach(({ locale, content }) => { 194 + translationRegistry.set(locale, content); 195 + if (debug) { 196 + console.log( 197 + `[direct-i18n] Registered ${locale} (${content.length} chars)`, 198 + ); 199 + } 200 + }); 201 + } else { 202 + // Load only current locale 203 + const content = await loadLocale(locale, debug); 204 + const translationSource = { locale, content }; 205 + setDynamicTranslations([translationSource]); 206 + translationRegistry.set(locale, content); 207 + 208 + if (debug) { 209 + console.log( 210 + `[direct-i18n] Registered ${locale} (${content.length} chars)`, 211 + ); 212 + } 213 + } 214 + } catch (error) { 215 + console.error( 216 + `[direct-i18n] Failed to load dynamic translations:`, 217 + error, 218 + ); 219 + // Fall back to defaults 220 + } finally { 221 + setIsLoading(false); 222 + } 223 + } else if (translations) { 224 + // Static translations 225 + if (debug) { 226 + console.log( 227 + `[direct-i18n] Registering ${translations.length} external translations`, 228 + ); 229 + } 230 + 231 + translations.forEach(({ locale, content }) => { 232 + translationRegistry.set(locale, content); 233 + if (debug) { 234 + console.log( 235 + `[direct-i18n] Registered ${locale} (${content.length} chars)`, 236 + ); 237 + } 238 + }); 239 + setIsLoading(false); 240 + } else { 241 + setIsLoading(false); 242 + } 243 + }; 244 + 245 + initializeTranslations(); 246 + }, [translations, debug, enableDynamicLoading, preloadAll]); 247 + 248 + if (debug) { 249 + console.log(`[direct-i18n] Provider initialized with locale: ${locale}`); 250 + } 251 + 252 + const l10n = useMemo(() => { 253 + if (debug) { 254 + console.log(`[direct-i18n] Creating localization for ${locale}`); 255 + } 256 + return createBundles(locale); 257 + }, [locale]); 258 + 259 + const changeLocale = useCallback( 260 + async (newLocale: SupportedLocale) => { 261 + if (newLocale === locale) return; 262 + 263 + if (debug) { 264 + console.log( 265 + `[direct-i18n] Changing locale from ${locale} to ${newLocale}`, 266 + ); 267 + } 268 + 269 + // Check if we need to load translations (only show loading if we do) 270 + const needsLoading = 271 + enableDynamicLoading && !translationRegistry.has(newLocale); 272 + 273 + if (needsLoading) { 274 + setIsLoading(true); 275 + } 276 + 277 + try { 278 + if (needsLoading) { 279 + // Load the new locale dynamically 280 + if (debug) { 281 + console.log(`[direct-i18n] Loading ${newLocale} dynamically`); 282 + } 283 + 284 + const content = await loadLocale(newLocale, debug); 285 + translationRegistry.set(newLocale, content); 286 + 287 + // Update dynamic translations state 288 + setDynamicTranslations((prev) => { 289 + const existing = prev.find((t) => t.locale === newLocale); 290 + if (existing) { 291 + return prev.map((t) => 292 + t.locale === newLocale ? { locale: newLocale, content } : t, 293 + ); 294 + } else { 295 + return [...prev, { locale: newLocale, content }]; 296 + } 297 + }); 298 + } 299 + 300 + if (typeof window !== "undefined") { 301 + localStorage.setItem("@streamplace/locale", newLocale); 302 + } 303 + setLocale(newLocale); 304 + } catch (error) { 305 + console.error("[direct-i18n] Failed to change locale:", error); 306 + } finally { 307 + if (needsLoading) { 308 + setIsLoading(false); 309 + } 310 + } 311 + }, 312 + [locale, debug, enableDynamicLoading], 313 + ); 314 + 315 + const contextValue: DirectI18nContextValue = { 316 + locale, 317 + changeLocale, 318 + isLoading, 319 + }; 320 + 321 + return ( 322 + <DirectI18nContext.Provider value={contextValue}> 323 + <LocalizationProvider l10n={l10n}>{children}</LocalizationProvider> 324 + </DirectI18nContext.Provider> 325 + ); 326 + } 327 + 328 + export function useDirectI18n(): DirectI18nContextValue { 329 + const context = useContext(DirectI18nContext); 330 + if (!context) { 331 + throw new Error("useDirectI18n must be used within DirectI18nProvider"); 332 + } 333 + return context; 334 + } 335 + 336 + // Utility functions for external translation management 337 + export function registerTranslation( 338 + locale: SupportedLocale, 339 + content: string, 340 + ): void { 341 + translationRegistry.set(locale, content); 342 + console.log( 343 + `[direct-i18n] Registered translation for ${locale} (${content.length} chars)`, 344 + ); 345 + } 346 + 347 + export function registerTranslations(translations: TranslationSource[]): void { 348 + translations.forEach(({ locale, content }) => { 349 + registerTranslation(locale, content); 350 + }); 351 + } 352 + 353 + export function clearTranslations(): void { 354 + translationRegistry.clear(); 355 + console.log("[direct-i18n] Cleared all registered translations"); 356 + } 357 + 358 + // Manifest utilities 359 + export function isSupportedLocale(locale: string): locale is SupportedLocale { 360 + return manifest.supportedLocales.includes(locale as SupportedLocale); 361 + } 362 + 363 + export function getLanguageInfo(locale: SupportedLocale): LanguageInfo | null { 364 + return LANGUAGE_INFO[locale] || null; 365 + } 366 + 367 + export function getFallbackChain(): SupportedLocale[] { 368 + return manifest.fallbackChain as SupportedLocale[]; 369 + } 370 + 371 + export function getDefaultLocale(): SupportedLocale { 372 + return DEFAULT_LOCALE; 373 + } 374 + 375 + export function getAllSupportedLocales(): SupportedLocale[] { 376 + return [...manifest.supportedLocales] as SupportedLocale[]; 377 + } 378 + 379 + // Validation utilities 380 + export function validateManifest(): { 381 + isValid: boolean; 382 + errors: string[]; 383 + } { 384 + const errors: string[] = []; 385 + 386 + if (!manifest.supportedLocales || !Array.isArray(manifest.supportedLocales)) { 387 + errors.push("Missing or invalid supportedLocales array"); 388 + } 389 + 390 + if (!manifest.fallbackChain || !Array.isArray(manifest.fallbackChain)) { 391 + errors.push("Missing or invalid fallbackChain array"); 392 + } 393 + 394 + if (!manifest.languages || typeof manifest.languages !== "object") { 395 + errors.push("Missing or invalid languages object"); 396 + } 397 + 398 + // Check that all supported locales have language info 399 + if (manifest.supportedLocales && manifest.languages) { 400 + for (const locale of manifest.supportedLocales) { 401 + if (!manifest.languages[locale]) { 402 + errors.push(`Missing language info for supported locale: ${locale}`); 403 + } 404 + } 405 + } 406 + 407 + // Check that fallback chain contains valid locales 408 + if (manifest.fallbackChain && manifest.supportedLocales) { 409 + for (const locale of manifest.fallbackChain) { 410 + if (!manifest.supportedLocales.includes(locale)) { 411 + errors.push(`Fallback locale ${locale} is not in supportedLocales`); 412 + } 413 + } 414 + } 415 + 416 + return { 417 + isValid: errors.length === 0, 418 + errors, 419 + }; 420 + } 421 + 422 + // Re-export Fluent components for convenience 423 + export { Localized, useLocalization, withLocalization } from "@fluent/react";
+47
js/components/src/i18n/index.ts
··· 1 + // Clean i18n exports - only what's actually being used 2 + 3 + // Main provider and types 4 + export { 5 + DirectI18nProvider, 6 + clearTranslations, 7 + registerTranslation, 8 + registerTranslations, 9 + useDirectI18n, 10 + } from "./direct-provider"; 11 + 12 + // Metro-compatible dynamic loading exports 13 + export { 14 + clearCache, 15 + dynamicActivate, 16 + getAvailableLocales, 17 + getCacheStats, 18 + isLocaleAvailable, 19 + loadAllLocales, 20 + loadLocale, 21 + loadLocales, 22 + preloadLocales, 23 + } from "./loaders/metro-loader"; 24 + 25 + export type { 26 + LanguageInfo as DirectLanguageInfo, 27 + SupportedLocale as DirectSupportedLocale, 28 + TranslationSource, 29 + } from "./direct-provider"; 30 + 31 + export { 32 + DEFAULT_LOCALE as DIRECT_DEFAULT_LOCALE, 33 + LANGUAGE_INFO as DIRECT_LANGUAGE_INFO, 34 + SUPPORTED_LOCALES as DIRECT_SUPPORTED_LOCALES, 35 + } from "./direct-provider"; 36 + 37 + // Language selector components 38 + export { 39 + DirectLanguageIndicator, 40 + DirectLanguageSelector, 41 + } from "./direct-language-selector"; 42 + 43 + // Re-export Fluent components for convenience 44 + export { Localized, useLocalization, withLocalization } from "@fluent/react"; 45 + 46 + // Re-export Fluent core for advanced usage if needed 47 + export { FluentBundle, FluentResource } from "@fluent/bundle";
+227
js/components/src/i18n/loaders/dynamic-loader.ts
··· 1 + // Manifest-based loader using pre-compiled translation modules 2 + import type { TranslationSource } from "../direct-provider"; 3 + import { 4 + type SupportedLocale, 5 + getAvailableLocales, 6 + localeMessages, 7 + supportedLocales, 8 + validateLocaleModule, 9 + } from "../locales/index"; 10 + 11 + // Cache to avoid re-processing the same translations 12 + const translationCache = new Map<SupportedLocale, string>(); 13 + 14 + /** 15 + * Get translation content from the manifest 16 + * This is synchronous since all translations are statically imported 17 + */ 18 + function getTranslationFromManifest(locale: SupportedLocale): string { 19 + const module = localeMessages[locale]; 20 + 21 + if (!validateLocaleModule(locale, module)) { 22 + throw new Error(`Invalid or missing translation module for ${locale}`); 23 + } 24 + 25 + return module.messages; 26 + } 27 + 28 + /** 29 + * Load translations for a specific locale 30 + * Returns cached version if already processed 31 + */ 32 + export async function loadLocale( 33 + locale: SupportedLocale, 34 + debug = false, 35 + ): Promise<string> { 36 + if (debug) { 37 + console.log(`[manifest-loader] Loading locale: ${locale}`); 38 + } 39 + 40 + // Return cached version if available 41 + if (translationCache.has(locale)) { 42 + if (debug) { 43 + console.log(`[manifest-loader] Using cached translations for ${locale}`); 44 + } 45 + return translationCache.get(locale)!; 46 + } 47 + 48 + try { 49 + // Get translations from the manifest (synchronous) 50 + const messages = getTranslationFromManifest(locale); 51 + 52 + // Cache the result 53 + translationCache.set(locale, messages); 54 + 55 + if (debug) { 56 + console.log( 57 + `[manifest-loader] Successfully loaded ${locale} (${messages.length} chars)`, 58 + ); 59 + } 60 + 61 + return messages; 62 + } catch (error) { 63 + console.error(`[manifest-loader] Failed to load ${locale}:`, error); 64 + throw error; 65 + } 66 + } 67 + 68 + /** 69 + * Load multiple locales at once 70 + */ 71 + export async function loadLocales( 72 + locales: SupportedLocale[], 73 + debug = false, 74 + ): Promise<TranslationSource[]> { 75 + if (debug) { 76 + console.log(`[manifest-loader] Loading multiple locales:`, locales); 77 + } 78 + 79 + const results = await Promise.allSettled( 80 + locales.map(async (locale) => ({ 81 + locale, 82 + content: await loadLocale(locale, debug), 83 + })), 84 + ); 85 + 86 + const translations: TranslationSource[] = []; 87 + const errors: { locale: SupportedLocale; error: any }[] = []; 88 + 89 + results.forEach((result, index) => { 90 + const locale = locales[index]; 91 + 92 + if (result.status === "fulfilled") { 93 + translations.push(result.value); 94 + } else { 95 + errors.push({ locale, error: result.reason }); 96 + console.error( 97 + `[manifest-loader] Failed to load ${locale}:`, 98 + result.reason, 99 + ); 100 + } 101 + }); 102 + 103 + if (errors.length > 0 && debug) { 104 + console.warn( 105 + `[manifest-loader] Failed to load ${errors.length}/${locales.length} locales`, 106 + ); 107 + } 108 + 109 + return translations; 110 + } 111 + 112 + /** 113 + * Load all supported locales 114 + * Uses getAvailableLocales to only load successfully imported locales 115 + */ 116 + export async function loadAllLocales( 117 + debug = false, 118 + ): Promise<TranslationSource[]> { 119 + const availableLocales = getAvailableLocales(); 120 + if (debug) { 121 + console.log( 122 + `[manifest-loader] Available locales from manifest:`, 123 + availableLocales, 124 + ); 125 + } 126 + return loadLocales(availableLocales, debug); 127 + } 128 + 129 + /** 130 + * Activate a locale by loading it and updating the i18n system 131 + * Similar to Lingui's dynamicActivate pattern 132 + */ 133 + export async function dynamicActivate( 134 + locale: SupportedLocale, 135 + onLoad?: (translationSource: TranslationSource) => void, 136 + debug = false, 137 + ): Promise<void> { 138 + if (debug) { 139 + console.log(`[manifest-loader] Activating locale: ${locale}`); 140 + } 141 + 142 + try { 143 + const content = await loadLocale(locale, debug); 144 + const translationSource: TranslationSource = { locale, content }; 145 + 146 + if (onLoad) { 147 + onLoad(translationSource); 148 + } 149 + 150 + if (debug) { 151 + console.log(`[manifest-loader] Successfully activated ${locale}`); 152 + } 153 + } catch (error) { 154 + console.error(`[manifest-loader] Failed to activate ${locale}:`, error); 155 + throw error; 156 + } 157 + } 158 + 159 + /** 160 + * Preload translations for faster switching 161 + * Since all translations are already statically imported, this just processes them 162 + */ 163 + export async function preloadLocales( 164 + locales: SupportedLocale[], 165 + debug = false, 166 + ): Promise<void> { 167 + if (debug) { 168 + console.log(`[manifest-loader] Preloading locales:`, locales); 169 + } 170 + 171 + // Process in background without waiting 172 + loadLocales(locales, debug).catch((error) => { 173 + if (debug) { 174 + console.warn(`[manifest-loader] Preloading failed:`, error); 175 + } 176 + }); 177 + } 178 + 179 + /** 180 + * Clear the translation cache 181 + * Useful for development or when forcing a reload 182 + */ 183 + export function clearCache(debug = false): void { 184 + if (debug) { 185 + console.log(`[manifest-loader] Clearing translation cache`); 186 + } 187 + 188 + translationCache.clear(); 189 + } 190 + 191 + /** 192 + * Get cache stats for debugging 193 + */ 194 + export function getCacheStats(): { 195 + cached: SupportedLocale[]; 196 + loading: SupportedLocale[]; 197 + totalCached: number; 198 + manifestLocales: readonly SupportedLocale[]; 199 + availableLocales: SupportedLocale[]; 200 + } { 201 + return { 202 + cached: Array.from(translationCache.keys()), 203 + loading: [], // No loading promises needed since everything is synchronous 204 + totalCached: translationCache.size, 205 + manifestLocales: supportedLocales, 206 + availableLocales: getAvailableLocales(), 207 + }; 208 + } 209 + 210 + /** 211 + * Check if a locale is available in the manifest 212 + */ 213 + export function isLocaleAvailable(locale: SupportedLocale): boolean { 214 + try { 215 + const module = localeMessages[locale]; 216 + return validateLocaleModule(locale, module); 217 + } catch { 218 + return false; 219 + } 220 + } 221 + 222 + /** 223 + * Get all supported locales from the manifest 224 + */ 225 + export function getSupportedLocales(): readonly SupportedLocale[] { 226 + return supportedLocales; 227 + }
+236
js/components/src/i18n/loaders/metro-loader.ts
··· 1 + // Metro-compatible translation loader using static imports 2 + import type { TranslationSource } from "../direct-provider"; 3 + import type { SupportedLocale } from "../locales"; 4 + import { 5 + localeMessages, 6 + supportedLocales, 7 + validateLocaleModule, 8 + } from "../locales"; 9 + 10 + // Use statically imported locale messages from mapping 11 + // Metro can analyze these at build time 12 + 13 + // Cache to avoid re-loading the same translations 14 + const translationCache = new Map<SupportedLocale, string>(); 15 + const loadingPromises = new Map<SupportedLocale, Promise<string>>(); 16 + 17 + /** 18 + * Load a translation module using Metro-compatible static imports 19 + */ 20 + function loadTranslationModule(locale: SupportedLocale): string { 21 + const module = localeMessages[locale]; 22 + 23 + if (!validateLocaleModule(locale, module)) { 24 + throw new Error(`Invalid or missing translation module for ${locale}`); 25 + } 26 + 27 + return module.messages; 28 + } 29 + 30 + /** 31 + * Load translations for a specific locale (Metro-compatible) 32 + * Returns cached version if already loaded 33 + */ 34 + export async function loadLocale( 35 + locale: SupportedLocale, 36 + debug = false, 37 + ): Promise<string> { 38 + if (debug) { 39 + console.log(`[metro-loader] Loading locale: ${locale}`); 40 + } 41 + 42 + // Return cached version if available 43 + if (translationCache.has(locale)) { 44 + if (debug) { 45 + console.log(`[metro-loader] Using cached translations for ${locale}`); 46 + } 47 + return translationCache.get(locale)!; 48 + } 49 + 50 + // Return existing promise if already loading 51 + if (loadingPromises.has(locale)) { 52 + if (debug) { 53 + console.log(`[metro-loader] Waiting for in-progress load of ${locale}`); 54 + } 55 + return loadingPromises.get(locale)!; 56 + } 57 + 58 + // Start loading (wrap synchronous loading in Promise for consistent API) 59 + const loadingPromise = new Promise<string>((resolve, reject) => { 60 + try { 61 + const messages = loadTranslationModule(locale); 62 + 63 + // Cache the result 64 + translationCache.set(locale, messages); 65 + 66 + if (debug) { 67 + console.log( 68 + `[metro-loader] Successfully loaded ${locale} (${messages.length} chars)`, 69 + ); 70 + } 71 + 72 + resolve(messages); 73 + } catch (error) { 74 + reject(error); 75 + } 76 + }); 77 + 78 + loadingPromises.set(locale, loadingPromise); 79 + 80 + try { 81 + const result = await loadingPromise; 82 + return result; 83 + } catch (error) { 84 + // Clean up failed loading promise 85 + loadingPromises.delete(locale); 86 + throw error; 87 + } finally { 88 + // Clean up completed loading promise 89 + loadingPromises.delete(locale); 90 + } 91 + } 92 + 93 + /** 94 + * Load multiple locales at once 95 + */ 96 + export async function loadLocales( 97 + locales: SupportedLocale[], 98 + debug = false, 99 + ): Promise<TranslationSource[]> { 100 + if (debug) { 101 + console.log(`[metro-loader] Loading multiple locales:`, locales); 102 + } 103 + 104 + const results = await Promise.allSettled( 105 + locales.map(async (locale) => ({ 106 + locale, 107 + content: await loadLocale(locale, debug), 108 + })), 109 + ); 110 + 111 + const translations: TranslationSource[] = []; 112 + const errors: { locale: SupportedLocale; error: any }[] = []; 113 + 114 + results.forEach((result, index) => { 115 + const locale = locales[index]; 116 + 117 + if (result.status === "fulfilled") { 118 + translations.push(result.value); 119 + } else { 120 + errors.push({ locale, error: result.reason }); 121 + console.error(`[metro-loader] Failed to load ${locale}:`, result.reason); 122 + } 123 + }); 124 + 125 + if (errors.length > 0 && debug) { 126 + console.warn( 127 + `[metro-loader] Failed to load ${errors.length}/${locales.length} locales`, 128 + ); 129 + } 130 + 131 + return translations; 132 + } 133 + 134 + /** 135 + * Load all supported locales 136 + */ 137 + export async function loadAllLocales( 138 + debug = false, 139 + ): Promise<TranslationSource[]> { 140 + return loadLocales([...supportedLocales], debug); 141 + } 142 + 143 + /** 144 + * Activate a locale by loading it and updating the i18n system 145 + * Metro-compatible version of dynamicActivate 146 + */ 147 + export async function dynamicActivate( 148 + locale: SupportedLocale, 149 + onLoad?: (translationSource: TranslationSource) => void, 150 + debug = false, 151 + ): Promise<void> { 152 + if (debug) { 153 + console.log(`[metro-loader] Activating locale: ${locale}`); 154 + } 155 + 156 + try { 157 + const content = await loadLocale(locale, debug); 158 + const translationSource: TranslationSource = { locale, content }; 159 + 160 + if (onLoad) { 161 + onLoad(translationSource); 162 + } 163 + 164 + if (debug) { 165 + console.log(`[metro-loader] Successfully activated ${locale}`); 166 + } 167 + } catch (error) { 168 + console.error(`[metro-loader] Failed to activate ${locale}:`, error); 169 + throw error; 170 + } 171 + } 172 + 173 + /** 174 + * Preload translations for faster switching 175 + * Useful for loading non-active locales in the background 176 + */ 177 + export async function preloadLocales( 178 + locales: SupportedLocale[], 179 + debug = false, 180 + ): Promise<void> { 181 + if (debug) { 182 + console.log(`[metro-loader] Preloading locales:`, locales); 183 + } 184 + 185 + // Load in background without waiting 186 + loadLocales(locales, debug).catch((error) => { 187 + if (debug) { 188 + console.warn(`[metro-loader] Preloading failed:`, error); 189 + } 190 + }); 191 + } 192 + 193 + /** 194 + * Clear the translation cache 195 + * Useful for development or when forcing a reload 196 + */ 197 + export function clearCache(debug = false): void { 198 + if (debug) { 199 + console.log(`[metro-loader] Clearing translation cache`); 200 + } 201 + 202 + translationCache.clear(); 203 + loadingPromises.clear(); 204 + } 205 + 206 + /** 207 + * Get cache stats for debugging 208 + */ 209 + export function getCacheStats(): { 210 + cached: SupportedLocale[]; 211 + loading: SupportedLocale[]; 212 + totalCached: number; 213 + } { 214 + return { 215 + cached: Array.from(translationCache.keys()), 216 + loading: Array.from(loadingPromises.keys()), 217 + totalCached: translationCache.size, 218 + }; 219 + } 220 + 221 + /** 222 + * Check if a locale is available for loading 223 + */ 224 + export function isLocaleAvailable(locale: SupportedLocale): boolean { 225 + const module = localeMessages[locale]; 226 + return validateLocaleModule(locale, module); 227 + } 228 + 229 + /** 230 + * Get all available locales 231 + */ 232 + export function getAvailableLocales(): SupportedLocale[] { 233 + return (Object.keys(localeMessages) as SupportedLocale[]).filter((locale) => 234 + isLocaleAvailable(locale), 235 + ); 236 + }
+71
js/components/src/i18n/locales/data/en-US/messages.js
··· 1 + // Auto-generated by compile-translations.js 2 + // Do not edit this file manually - it will be overwritten 3 + 4 + export const messages = `# === settings.ftl === 5 + # Settings Page Translations - English (US) 6 + 7 + ## App Version 8 + app-version = Streamplace v{ $version } 9 + 10 + ## Custom Node Settings 11 + use-custom-node = Use Custom Node 12 + default-url = Default: { $url } 13 + enter-custom-node-url = Enter custom node URL 14 + save-button = SAVE 15 + 16 + ## Language Settings 17 + language-selection = Language 18 + language-selection-description = Choose your preferred language 19 + 20 + ## Debug Recording 21 + debug-recording-title = Allow { $host } to record your livestream for debugging and improving the service 22 + debug-recording-description = Optional 23 + 24 + ## Key Management 25 + manage-keys = Manage Keys 26 + 27 + ## General UI 28 + settings-title = Settings 29 + loading = Loading... 30 + error = Error 31 + cancel = Cancel 32 + confirm = Confirm 33 + 34 + ## Demo and Testing 35 + welcome-user = Welcome, { $username }! 36 + notification-count = { $count -> 37 + [0] No notifications 38 + [1] One notification 39 + *[other] { $count } notifications 40 + } 41 + search-placeholder = Search... 42 + message-input = Enter your message... 43 + 44 + ## Status Messages 45 + success = Success 46 + warning = Warning 47 + info = Information 48 + close = Close 49 + open = Open 50 + delete = Delete 51 + edit = Edit 52 + create = Create 53 + update = Update 54 + refresh = Refresh 55 + 56 + ## Actions 57 + save = Save 58 + cancel-button = Cancel 59 + ok = OK 60 + yes = Yes 61 + no = No 62 + continue = Continue 63 + back = Back 64 + next = Next 65 + finish = Finish 66 + `; 67 + 68 + // Export for CommonJS compatibility 69 + if (typeof module !== "undefined" && module.exports) { 70 + module.exports = { messages }; 71 + }
+61
js/components/src/i18n/locales/data/en-US/settings.ftl
··· 1 + # Settings Page Translations - English (US) 2 + 3 + ## App Version 4 + app-version = Streamplace v{ $version } 5 + 6 + ## Custom Node Settings 7 + use-custom-node = Use Custom Node 8 + default-url = Default: { $url } 9 + enter-custom-node-url = Enter custom node URL 10 + save-button = SAVE 11 + 12 + ## Language Settings 13 + language-selection = Language 14 + language-selection-description = Choose your preferred language 15 + 16 + ## Debug Recording 17 + debug-recording-title = Allow { $host } to record your livestream for debugging and improving the service 18 + debug-recording-description = Optional 19 + 20 + ## Key Management 21 + manage-keys = Manage Keys 22 + 23 + ## General UI 24 + settings-title = Settings 25 + loading = Loading... 26 + error = Error 27 + cancel = Cancel 28 + confirm = Confirm 29 + 30 + ## Demo and Testing 31 + welcome-user = Welcome, { $username }! 32 + notification-count = { $count -> 33 + [0] No notifications 34 + [1] One notification 35 + *[other] { $count } notifications 36 + } 37 + search-placeholder = Search... 38 + message-input = Enter your message... 39 + 40 + ## Status Messages 41 + success = Success 42 + warning = Warning 43 + info = Information 44 + close = Close 45 + open = Open 46 + delete = Delete 47 + edit = Edit 48 + create = Create 49 + update = Update 50 + refresh = Refresh 51 + 52 + ## Actions 53 + save = Save 54 + cancel-button = Cancel 55 + ok = OK 56 + yes = Yes 57 + no = No 58 + continue = Continue 59 + back = Back 60 + next = Next 61 + finish = Finish
+71
js/components/src/i18n/locales/data/es-ES/messages.js
··· 1 + // Auto-generated by compile-translations.js 2 + // Do not edit this file manually - it will be overwritten 3 + 4 + export const messages = `# === settings.ftl === 5 + # Settings Page Translations - Spanish (Spain) 6 + 7 + ## App Version 8 + app-version = Streamplace v{ $version } 9 + 10 + ## Custom Node Settings 11 + use-custom-node = Usar Nodo Personalizado 12 + default-url = Predeterminado: { $url } 13 + enter-custom-node-url = Introduce la URL del nodo personalizado 14 + save-button = GUARDAR 15 + 16 + ## Language Settings 17 + language-selection = Idioma 18 + language-selection-description = Elige tu idioma preferido 19 + 20 + ## Debug Recording 21 + debug-recording-title = Permitir que { $host } grabe tu retransmisión en directo para depuración y mejora del servicio 22 + debug-recording-description = Opcional 23 + 24 + ## Key Management 25 + manage-keys = Gestionar Claves 26 + 27 + ## General UI 28 + settings-title = Configuración 29 + loading = Cargando... 30 + error = Error 31 + cancel = Cancelar 32 + confirm = Confirmar 33 + 34 + ## Demo and Testing 35 + welcome-user = ¡Bienvenido, { $username }! 36 + notification-count = { $count -> 37 + [0] Sin notificaciones 38 + [1] Una notificación 39 + *[other] { $count } notificaciones 40 + } 41 + search-placeholder = Buscar... 42 + message-input = Introduce tu mensaje... 43 + 44 + ## Status Messages 45 + success = Éxito 46 + warning = Aviso 47 + info = Información 48 + close = Cerrar 49 + open = Abrir 50 + delete = Eliminar 51 + edit = Editar 52 + create = Crear 53 + update = Actualizar 54 + refresh = Actualizar 55 + 56 + ## Actions 57 + save = Guardar 58 + cancel-button = Cancelar 59 + ok = Aceptar 60 + yes = Sí 61 + no = No 62 + continue = Continuar 63 + back = Volver 64 + next = Siguiente 65 + finish = Finalizar 66 + `; 67 + 68 + // Export for CommonJS compatibility 69 + if (typeof module !== "undefined" && module.exports) { 70 + module.exports = { messages }; 71 + }
+61
js/components/src/i18n/locales/data/es-ES/settings.ftl
··· 1 + # Settings Page Translations - Spanish (Spain) 2 + 3 + ## App Version 4 + app-version = Streamplace v{ $version } 5 + 6 + ## Custom Node Settings 7 + use-custom-node = Usar Nodo Personalizado 8 + default-url = Predeterminado: { $url } 9 + enter-custom-node-url = Introduce la URL del nodo personalizado 10 + save-button = GUARDAR 11 + 12 + ## Language Settings 13 + language-selection = Idioma 14 + language-selection-description = Elige tu idioma preferido 15 + 16 + ## Debug Recording 17 + debug-recording-title = Permitir que { $host } grabe tu retransmisión en directo para depuración y mejora del servicio 18 + debug-recording-description = Opcional 19 + 20 + ## Key Management 21 + manage-keys = Gestionar Claves 22 + 23 + ## General UI 24 + settings-title = Configuración 25 + loading = Cargando... 26 + error = Error 27 + cancel = Cancelar 28 + confirm = Confirmar 29 + 30 + ## Demo and Testing 31 + welcome-user = ¡Bienvenido, { $username }! 32 + notification-count = { $count -> 33 + [0] Sin notificaciones 34 + [1] Una notificación 35 + *[other] { $count } notificaciones 36 + } 37 + search-placeholder = Buscar... 38 + message-input = Introduce tu mensaje... 39 + 40 + ## Status Messages 41 + success = Éxito 42 + warning = Aviso 43 + info = Información 44 + close = Cerrar 45 + open = Abrir 46 + delete = Eliminar 47 + edit = Editar 48 + create = Crear 49 + update = Actualizar 50 + refresh = Actualizar 51 + 52 + ## Actions 53 + save = Guardar 54 + cancel-button = Cancelar 55 + ok = Aceptar 56 + yes = Sí 57 + no = No 58 + continue = Continuar 59 + back = Volver 60 + next = Siguiente 61 + finish = Finalizar
+71
js/components/src/i18n/locales/data/fr-FR/messages.js
··· 1 + // Auto-generated by compile-translations.js 2 + // Do not edit this file manually - it will be overwritten 3 + 4 + export const messages = `# === settings.ftl === 5 + # Settings Page Translations - French (France) 6 + 7 + ## App Version 8 + app-version = Streamplace v{ $version } 9 + 10 + ## Custom Node Settings 11 + use-custom-node = Utiliser un nœud personnalisé 12 + default-url = Par défaut : { $url } 13 + enter-custom-node-url = Saisir l'URL du nœud personnalisé 14 + save-button = ENREGISTRER 15 + 16 + ## Language Settings 17 + language-selection = Langue 18 + language-selection-description = Choisissez votre langue préférée 19 + 20 + ## Debug Recording 21 + debug-recording-title = Autoriser { $host } à enregistrer votre diffusion en direct pour le débogage et l'amélioration du service 22 + debug-recording-description = Optionnel 23 + 24 + ## Key Management 25 + manage-keys = Gérer les clés 26 + 27 + ## General UI 28 + settings-title = Paramètres 29 + loading = Chargement... 30 + error = Erreur 31 + cancel = Annuler 32 + confirm = Confirmer 33 + 34 + ## Demo and Testing 35 + welcome-user = Bienvenue, { $username } ! 36 + notification-count = { $count -> 37 + [0] Aucune notification 38 + [1] Une notification 39 + *[other] { $count } notifications 40 + } 41 + search-placeholder = Rechercher... 42 + message-input = Saisissez votre message... 43 + 44 + ## Status Messages 45 + success = Succès 46 + warning = Attention 47 + info = Information 48 + close = Fermer 49 + open = Ouvrir 50 + delete = Supprimer 51 + edit = Modifier 52 + create = Créer 53 + update = Mettre à jour 54 + refresh = Actualiser 55 + 56 + ## Actions 57 + save = Enregistrer 58 + cancel-button = Annuler 59 + ok = OK 60 + yes = Oui 61 + no = Non 62 + continue = Continuer 63 + back = Retour 64 + next = Suivant 65 + finish = Terminer 66 + `; 67 + 68 + // Export for CommonJS compatibility 69 + if (typeof module !== "undefined" && module.exports) { 70 + module.exports = { messages }; 71 + }
+61
js/components/src/i18n/locales/data/fr-FR/settings.ftl
··· 1 + # Settings Page Translations - French (France) 2 + 3 + ## App Version 4 + app-version = Streamplace v{ $version } 5 + 6 + ## Custom Node Settings 7 + use-custom-node = Utiliser un nœud personnalisé 8 + default-url = Par défaut : { $url } 9 + enter-custom-node-url = Saisir l'URL du nœud personnalisé 10 + save-button = ENREGISTRER 11 + 12 + ## Language Settings 13 + language-selection = Langue 14 + language-selection-description = Choisissez votre langue préférée 15 + 16 + ## Debug Recording 17 + debug-recording-title = Autoriser { $host } à enregistrer votre diffusion en direct pour le débogage et l'amélioration du service 18 + debug-recording-description = Optionnel 19 + 20 + ## Key Management 21 + manage-keys = Gérer les clés 22 + 23 + ## General UI 24 + settings-title = Paramètres 25 + loading = Chargement... 26 + error = Erreur 27 + cancel = Annuler 28 + confirm = Confirmer 29 + 30 + ## Demo and Testing 31 + welcome-user = Bienvenue, { $username } ! 32 + notification-count = { $count -> 33 + [0] Aucune notification 34 + [1] Une notification 35 + *[other] { $count } notifications 36 + } 37 + search-placeholder = Rechercher... 38 + message-input = Saisissez votre message... 39 + 40 + ## Status Messages 41 + success = Succès 42 + warning = Attention 43 + info = Information 44 + close = Fermer 45 + open = Ouvrir 46 + delete = Supprimer 47 + edit = Modifier 48 + create = Créer 49 + update = Mettre à jour 50 + refresh = Actualiser 51 + 52 + ## Actions 53 + save = Enregistrer 54 + cancel-button = Annuler 55 + ok = OK 56 + yes = Oui 57 + no = Non 58 + continue = Continuer 59 + back = Retour 60 + next = Suivant 61 + finish = Terminer
+71
js/components/src/i18n/locales/data/pt-BR/messages.js
··· 1 + // Auto-generated by compile-translations.js 2 + // Do not edit this file manually - it will be overwritten 3 + 4 + export const messages = `# === settings.ftl === 5 + # Traduções da Página de Configurações - Português (Brasil) 6 + 7 + ## Versão do Aplicativo 8 + app-version = Streamplace v{ $version } 9 + 10 + ## Configurações de Nó Personalizado 11 + use-custom-node = Usar Nó Personalizado 12 + default-url = Padrão: { $url } 13 + enter-custom-node-url = Digite a URL do nó personalizado 14 + save-button = SALVAR 15 + 16 + ## Configurações de Idioma 17 + language-selection = Idioma 18 + language-selection-description = Escolha seu idioma preferido 19 + 20 + ## Gravação de Depuração 21 + debug-recording-title = Permitir que { $host } grave sua transmissão ao vivo para depuração e melhoria do serviço 22 + debug-recording-description = Opcional 23 + 24 + ## Gerenciamento de Chaves 25 + manage-keys = Gerenciar Chaves 26 + 27 + ## Interface Geral 28 + settings-title = Configurações 29 + loading = Carregando... 30 + error = Erro 31 + cancel = Cancelar 32 + confirm = Confirmar 33 + 34 + ## Demonstração e Testes 35 + welcome-user = Bem-vindo, { $username }! 36 + notification-count = { $count -> 37 + [0] Nenhuma notificação 38 + [1] Uma notificação 39 + *[other] { $count } notificações 40 + } 41 + search-placeholder = Pesquisar... 42 + message-input = Digite sua mensagem... 43 + 44 + ## Mensagens de Status 45 + success = Sucesso 46 + warning = Aviso 47 + info = Informação 48 + close = Fechar 49 + open = Abrir 50 + delete = Excluir 51 + edit = Editar 52 + create = Criar 53 + update = Atualizar 54 + refresh = Atualizar 55 + 56 + ## Ações 57 + save = Salvar 58 + cancel-button = Cancelar 59 + ok = OK 60 + yes = Sim 61 + no = Não 62 + continue = Continuar 63 + back = Voltar 64 + next = Próximo 65 + finish = Finalizar 66 + `; 67 + 68 + // Export for CommonJS compatibility 69 + if (typeof module !== "undefined" && module.exports) { 70 + module.exports = { messages }; 71 + }
+61
js/components/src/i18n/locales/data/pt-BR/settings.ftl
··· 1 + # Traduções da Página de Configurações - Português (Brasil) 2 + 3 + ## Versão do Aplicativo 4 + app-version = Streamplace v{ $version } 5 + 6 + ## Configurações de Nó Personalizado 7 + use-custom-node = Usar Nó Personalizado 8 + default-url = Padrão: { $url } 9 + enter-custom-node-url = Digite a URL do nó personalizado 10 + save-button = SALVAR 11 + 12 + ## Configurações de Idioma 13 + language-selection = Idioma 14 + language-selection-description = Escolha seu idioma preferido 15 + 16 + ## Gravação de Depuração 17 + debug-recording-title = Permitir que { $host } grave sua transmissão ao vivo para depuração e melhoria do serviço 18 + debug-recording-description = Opcional 19 + 20 + ## Gerenciamento de Chaves 21 + manage-keys = Gerenciar Chaves 22 + 23 + ## Interface Geral 24 + settings-title = Configurações 25 + loading = Carregando... 26 + error = Erro 27 + cancel = Cancelar 28 + confirm = Confirmar 29 + 30 + ## Demonstração e Testes 31 + welcome-user = Bem-vindo, { $username }! 32 + notification-count = { $count -> 33 + [0] Nenhuma notificação 34 + [1] Uma notificação 35 + *[other] { $count } notificações 36 + } 37 + search-placeholder = Pesquisar... 38 + message-input = Digite sua mensagem... 39 + 40 + ## Mensagens de Status 41 + success = Sucesso 42 + warning = Aviso 43 + info = Informação 44 + close = Fechar 45 + open = Abrir 46 + delete = Excluir 47 + edit = Editar 48 + create = Criar 49 + update = Atualizar 50 + refresh = Atualizar 51 + 52 + ## Ações 53 + save = Salvar 54 + cancel-button = Cancelar 55 + ok = OK 56 + yes = Sim 57 + no = Não 58 + continue = Continuar 59 + back = Voltar 60 + next = Próximo 61 + finish = Finalizar
+71
js/components/src/i18n/locales/data/zh-TW/messages.js
··· 1 + // Auto-generated by compile-translations.js 2 + // Do not edit this file manually - it will be overwritten 3 + 4 + export const messages = `# === settings.ftl === 5 + # Settings Page Translations - Chinese Traditional (Taiwan) 6 + 7 + ## App Version 8 + app-version = Streamplace v{ $version } 9 + 10 + ## Custom Node Settings 11 + use-custom-node = 使用自訂節點 12 + default-url = 預設:{ $url } 13 + enter-custom-node-url = 輸入自訂節點網址 14 + save-button = 儲存 15 + 16 + ## Language Settings 17 + language-selection = 語言 18 + language-selection-description = 選擇您偏好的語言 19 + 20 + ## Debug Recording 21 + debug-recording-title = 允許 { $host } 錄製您的直播串流以進行除錯和服務改善 22 + debug-recording-description = 可選項目 23 + 24 + ## Key Management 25 + manage-keys = 管理金鑰 26 + 27 + ## General UI 28 + settings-title = 設定 29 + loading = 載入中... 30 + error = 錯誤 31 + cancel = 取消 32 + confirm = 確認 33 + 34 + ## Demo and Testing 35 + welcome-user = 歡迎,{ $username }! 36 + notification-count = { $count -> 37 + [0] 無通知 38 + [1] 一則通知 39 + *[other] { $count } 則通知 40 + } 41 + search-placeholder = 搜尋... 42 + message-input = 請輸入您的訊息... 43 + 44 + ## Status Messages 45 + success = 成功 46 + warning = 警告 47 + info = 資訊 48 + close = 關閉 49 + open = 開啟 50 + delete = 刪除 51 + edit = 編輯 52 + create = 建立 53 + update = 更新 54 + refresh = 重新整理 55 + 56 + ## Actions 57 + save = 儲存 58 + cancel-button = 取消 59 + ok = 確定 60 + yes = 是 61 + no = 否 62 + continue = 繼續 63 + back = 返回 64 + next = 下一步 65 + finish = 完成 66 + `; 67 + 68 + // Export for CommonJS compatibility 69 + if (typeof module !== "undefined" && module.exports) { 70 + module.exports = { messages }; 71 + }
+61
js/components/src/i18n/locales/data/zh-TW/settings.ftl
··· 1 + # Settings Page Translations - Chinese Traditional (Taiwan) 2 + 3 + ## App Version 4 + app-version = Streamplace v{ $version } 5 + 6 + ## Custom Node Settings 7 + use-custom-node = 使用自訂節點 8 + default-url = 預設:{ $url } 9 + enter-custom-node-url = 輸入自訂節點網址 10 + save-button = 儲存 11 + 12 + ## Language Settings 13 + language-selection = 語言 14 + language-selection-description = 選擇您偏好的語言 15 + 16 + ## Debug Recording 17 + debug-recording-title = 允許 { $host } 錄製您的直播串流以進行除錯和服務改善 18 + debug-recording-description = 可選項目 19 + 20 + ## Key Management 21 + manage-keys = 管理金鑰 22 + 23 + ## General UI 24 + settings-title = 設定 25 + loading = 載入中... 26 + error = 錯誤 27 + cancel = 取消 28 + confirm = 確認 29 + 30 + ## Demo and Testing 31 + welcome-user = 歡迎,{ $username }! 32 + notification-count = { $count -> 33 + [0] 無通知 34 + [1] 一則通知 35 + *[other] { $count } 則通知 36 + } 37 + search-placeholder = 搜尋... 38 + message-input = 請輸入您的訊息... 39 + 40 + ## Status Messages 41 + success = 成功 42 + warning = 警告 43 + info = 資訊 44 + close = 關閉 45 + open = 開啟 46 + delete = 刪除 47 + edit = 編輯 48 + create = 建立 49 + update = 更新 50 + refresh = 重新整理 51 + 52 + ## Actions 53 + save = 儲存 54 + cancel-button = 取消 55 + ok = 確定 56 + yes = 是 57 + no = 否 58 + continue = 繼續 59 + back = 返回 60 + next = 下一步 61 + finish = 完成
+103
js/components/src/i18n/locales/index.ts
··· 1 + // Auto-generated by compile-translations.js 2 + // Do not edit this file manually - it will be overwritten 3 + 4 + let enusMessages: { messages: string }; 5 + let ptbrMessages: { messages: string }; 6 + let esesMessages: { messages: string }; 7 + let zhtwMessages: { messages: string }; 8 + let frfrMessages: { messages: string }; 9 + 10 + try { 11 + // Try to import compiled translation modules 12 + enusMessages = require("./data/en-US/messages.js"); 13 + } catch (error) { 14 + console.warn("[locale-mapping] Failed to load en-US translations:", error); 15 + enusMessages = { 16 + messages: 17 + "# Fallback\\nloading = Loading...\\nerror = Error\\ncancel = Cancel\\nsettings-title = Settings", 18 + }; 19 + } 20 + 21 + try { 22 + // Try to import compiled translation modules 23 + ptbrMessages = require("./data/pt-BR/messages.js"); 24 + } catch (error) { 25 + console.warn("[locale-mapping] Failed to load pt-BR translations:", error); 26 + ptbrMessages = { 27 + messages: 28 + "# Fallback\\nloading = Loading...\\nerror = Error\\ncancel = Cancel\\nsettings-title = Settings", 29 + }; 30 + } 31 + 32 + try { 33 + // Try to import compiled translation modules 34 + esesMessages = require("./data/es-ES/messages.js"); 35 + } catch (error) { 36 + console.warn("[locale-mapping] Failed to load es-ES translations:", error); 37 + esesMessages = { 38 + messages: 39 + "# Fallback\\nloading = Loading...\\nerror = Error\\ncancel = Cancel\\nsettings-title = Settings", 40 + }; 41 + } 42 + 43 + try { 44 + // Try to import compiled translation modules 45 + zhtwMessages = require("./data/zh-TW/messages.js"); 46 + } catch (error) { 47 + console.warn("[locale-mapping] Failed to load zh-TW translations:", error); 48 + zhtwMessages = { 49 + messages: 50 + "# Fallback\\nloading = Loading...\\nerror = Error\\ncancel = Cancel\\nsettings-title = Settings", 51 + }; 52 + } 53 + 54 + try { 55 + // Try to import compiled translation modules 56 + frfrMessages = require("./data/fr-FR/messages.js"); 57 + } catch (error) { 58 + console.warn("[locale-mapping] Failed to load fr-FR translations:", error); 59 + frfrMessages = { 60 + messages: 61 + "# Fallback\\nloading = Loading...\\nerror = Error\\ncancel = Cancel\\nsettings-title = Settings", 62 + }; 63 + } 64 + 65 + // Export locale mapping for metro-loader 66 + export const localeMessages = { 67 + "en-US": enusMessages, 68 + "pt-BR": ptbrMessages, 69 + "es-ES": esesMessages, 70 + "zh-TW": zhtwMessages, 71 + "fr-FR": frfrMessages, 72 + } as const; 73 + 74 + export { enusMessages, esesMessages, frfrMessages, ptbrMessages, zhtwMessages }; 75 + 76 + export const supportedLocales = [ 77 + "en-US", 78 + "pt-BR", 79 + "es-ES", 80 + "zh-TW", 81 + "fr-FR", 82 + ] as const; 83 + export type SupportedLocale = (typeof supportedLocales)[number]; 84 + 85 + export function validateLocaleModule( 86 + locale: string, 87 + module: any, 88 + ): module is { messages: string } { 89 + return ( 90 + module && 91 + typeof module === "object" && 92 + "messages" in module && 93 + typeof module.messages === "string" && 94 + module.messages.length > 0 95 + ); 96 + } 97 + 98 + export function getAvailableLocales(): SupportedLocale[] { 99 + return supportedLocales.filter((locale) => { 100 + const module = localeMessages[locale]; 101 + return validateLocaleModule(locale, module); 102 + }); 103 + }
+64
js/components/src/i18n/locales/language-config.ts
··· 1 + // Auto-generated by compile-translations.js 2 + // Do not edit this file manually - it will be overwritten 3 + 4 + import type { SupportedLocale } from "./index"; 5 + 6 + export interface LanguageInfo { 7 + code: SupportedLocale; 8 + name: string; 9 + nativeName: string; 10 + flag: string; 11 + } 12 + 13 + export const LANGUAGE_INFO: Record<SupportedLocale, LanguageInfo> = { 14 + "en-US": { 15 + code: "en-US", 16 + name: "English", 17 + nativeName: "English", 18 + flag: "🇺🇸", 19 + }, 20 + "pt-BR": { 21 + code: "pt-BR", 22 + name: "Portuguese", 23 + nativeName: "Português", 24 + flag: "🇧🇷", 25 + }, 26 + "es-ES": { 27 + code: "es-ES", 28 + name: "Spanish", 29 + nativeName: "Español", 30 + flag: "🇪🇸", 31 + }, 32 + "zh-TW": { 33 + code: "zh-TW", 34 + name: "Chinese Traditional", 35 + nativeName: "繁體中文", 36 + flag: "🇹🇼", 37 + }, 38 + "fr-FR": { 39 + code: "fr-FR", 40 + name: "French", 41 + nativeName: "Français", 42 + flag: "🇫🇷", 43 + }, 44 + } as const; 45 + 46 + export const DEFAULT_TRANSLATIONS: Record<SupportedLocale, string> = { 47 + "en-US": `# Fallback\nloading = Loading...\nerror = Error`, 48 + "pt-BR": `# Fallback\nloading = Carregando...\nerror = Erro`, 49 + "es-ES": `# Fallback\nloading = Cargando...\nerror = Error`, 50 + "zh-TW": `# Fallback\nloading = 載入中...\nerror = 錯誤`, 51 + "fr-FR": `# Fallback\nloading = Chargement...\nerror = Erreur`, 52 + } as const; 53 + 54 + export function getLanguageInfo(locale: SupportedLocale): LanguageInfo { 55 + return LANGUAGE_INFO[locale]; 56 + } 57 + 58 + export function getDefaultTranslation(locale: SupportedLocale): string { 59 + return DEFAULT_TRANSLATIONS[locale]; 60 + } 61 + 62 + export function getAllLanguageInfo(): LanguageInfo[] { 63 + return Object.values(LANGUAGE_INFO); 64 + }
+55
js/components/src/i18n/locales/translations.ts
··· 1 + // Auto-generated by compile-translations.js 2 + // Do not edit this file manually - it will be overwritten 3 + 4 + import type { TranslationSource } from "../direct-provider"; 5 + import { 6 + dynamicActivate, 7 + loadAllLocales, 8 + loadLocale, 9 + } from "../loaders/metro-loader"; 10 + 11 + /** 12 + * Load all translations dynamically from compiled .ftl files 13 + * 14 + * Usage: 15 + * ```tsx 16 + * import { loadDynamicTranslations } from './locales/translations'; 17 + * 18 + * const App = () => ( 19 + * <DirectI18nProvider 20 + * enableDynamicLoading={true} 21 + * preloadAll={false} // Only load current locale 22 + * debug={true} 23 + * > 24 + * <YourApp /> 25 + * </DirectI18nProvider> 26 + * ); 27 + * ``` 28 + */ 29 + export async function loadDynamicTranslations( 30 + debug = false, 31 + ): Promise<TranslationSource[]> { 32 + return loadAllLocales(debug); 33 + } 34 + 35 + /** 36 + * Load a specific locale dynamically 37 + * Useful for lazy loading when switching languages 38 + */ 39 + export async function loadDynamicLocale( 40 + locale: "en-US" | "pt-BR" | "es-ES" | "zh-TW" | "fr-FR", 41 + debug = false, 42 + ) { 43 + return loadLocale(locale, debug); 44 + } 45 + 46 + /** 47 + * Activate a locale dynamically (similar to Lingui's approach) 48 + * This loads and activates a locale in one step 49 + */ 50 + export async function activateLocale( 51 + locale: "en-US" | "pt-BR" | "es-ES" | "zh-TW" | "fr-FR", 52 + debug = false, 53 + ) { 54 + return dynamicActivate(locale, undefined, debug); 55 + }
+36
js/components/src/i18n/manifest.json
··· 1 + { 2 + "supportedLocales": ["en-US", "pt-BR", "es-ES", "zh-TW", "fr-FR"], 3 + "fallbackChain": ["en-US"], 4 + "languages": { 5 + "en-US": { 6 + "code": "en-US", 7 + "name": "English", 8 + "nativeName": "English", 9 + "flag": "🇺🇸" 10 + }, 11 + "pt-BR": { 12 + "code": "pt-BR", 13 + "name": "Portuguese", 14 + "nativeName": "Português", 15 + "flag": "🇧🇷" 16 + }, 17 + "es-ES": { 18 + "code": "es-ES", 19 + "name": "Spanish", 20 + "nativeName": "Español", 21 + "flag": "🇪🇸" 22 + }, 23 + "zh-TW": { 24 + "code": "zh-TW", 25 + "name": "Chinese Traditional", 26 + "nativeName": "繁體中文", 27 + "flag": "🇹🇼" 28 + }, 29 + "fr-FR": { 30 + "code": "fr-FR", 31 + "name": "French", 32 + "nativeName": "Français", 33 + "flag": "🇫🇷" 34 + } 35 + } 36 + }
+4
js/components/src/index.tsx
··· 24 24 25 25 export * from "./hooks"; 26 26 27 + // Internationalization system exports 28 + export * from "./i18n"; 29 + export * as I18n from "./i18n"; 30 + 27 31 // Theme system exports 28 32 export * from "./lib/theme"; 29 33