diff --git a/js/app/components/settings/settings.tsx b/js/app/components/settings/settings.tsx index 2b00e8a46..023b4eaa7 100644 --- a/js/app/components/settings/settings.tsx +++ b/js/app/components/settings/settings.tsx @@ -36,7 +36,7 @@ export function Settings() { const [newUrl, setNewUrl] = useState(""); const [overrideEnabled, setOverrideEnabled] = useState(false); const [languageSearchQuery, setLanguageSearchQuery] = useState(""); - const { t, i18n } = useTranslation(); + const { t, i18n } = useTranslation("settings"); // are we logged in? const loggedIn = useAppSelector( diff --git a/js/app/components/settings/updates.native.tsx b/js/app/components/settings/updates.native.tsx index 9e92344d4..e7d5f56b4 100644 --- a/js/app/components/settings/updates.native.tsx +++ b/js/app/components/settings/updates.native.tsx @@ -17,7 +17,7 @@ export function Updates() { const updateInfo = ExpoUpdates.useUpdates(); const { currentlyRunning, isUpdateAvailable, isUpdatePending } = updateInfo; const toast = useToast(); - const { t } = useTranslation(); + const { t } = useTranslation("settings"); console.log(`updateInfo: ${JSON.stringify(updateInfo)}`); diff --git a/js/app/components/settings/updates.tsx b/js/app/components/settings/updates.tsx index 4dedabfd5..6a7c606c5 100644 --- a/js/app/components/settings/updates.tsx +++ b/js/app/components/settings/updates.tsx @@ -5,7 +5,7 @@ import pkg from "../../package.json"; // maybe someday some PWA update stuff will live here export function Updates() { - const { t } = useTranslation(); + const { t } = useTranslation("settings"); return ( ios/.xcode.env.local", "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'", diff --git a/js/components/locales/en-US/common.ftl b/js/components/locales/en-US/common.ftl new file mode 100644 index 000000000..64ec45806 --- /dev/null +++ b/js/components/locales/en-US/common.ftl @@ -0,0 +1,41 @@ +# Common UI Translations - English (US) + +## General UI +loading = Loading... +error = Error +cancel = Cancel +confirm = Confirm +close = Close +open = Open +ok = OK +yes = Yes +no = No +continue = Continue +back = Back +next = Next +finish = Finish + +## Actions +save = Save +delete = Delete +edit = Edit +create = Create +update = Update +refresh = Refresh + +## Status Messages +success = Success +warning = Warning +info = Information + +## Input Placeholders +search-placeholder = Search... +message-input = Enter your message... + +## Demo and Testing +welcome-user = Welcome, { $username }! +notification-count = { $count -> + [0] No notifications + [1] One notification + *[other] { $count } notifications +} diff --git a/js/components/locales/en-US/settings.ftl b/js/components/locales/en-US/settings.ftl index 83854f671..3047a075d 100644 --- a/js/components/locales/en-US/settings.ftl +++ b/js/components/locales/en-US/settings.ftl @@ -36,42 +36,5 @@ debug-recording-description = Optional ## Key Management manage-keys = Manage Keys -## General UI +## Settings Page Specific settings-title = Settings -loading = Loading... -error = Error -cancel = Cancel -confirm = Confirm - -## Demo and Testing -welcome-user = Welcome, { $username }! -notification-count = { $count -> - [0] No notifications - [1] One notification - *[other] { $count } notifications -} -search-placeholder = Search... -message-input = Enter your message... - -## Status Messages -success = Success -warning = Warning -info = Information -close = Close -open = Open -delete = Delete -edit = Edit -create = Create -update = Update -refresh = Refresh - -## Actions -save = Save -cancel-button = Cancel -ok = OK -yes = Yes -no = No -continue = Continue -back = Back -next = Next -finish = Finish diff --git a/js/components/locales/es-ES/common.ftl b/js/components/locales/es-ES/common.ftl new file mode 100644 index 000000000..a1a8267fb --- /dev/null +++ b/js/components/locales/es-ES/common.ftl @@ -0,0 +1,41 @@ +# Common UI Translations - Spanish (Spain) + +## General UI +loading = Cargando... +error = Error +cancel = Cancelar +confirm = Confirmar +close = Cerrar +open = Abrir +ok = OK +yes = Sí +no = No +continue = Continuar +back = Atrás +next = Siguiente +finish = Finalizar + +## Actions +save = Guardar +delete = Eliminar +edit = Editar +create = Crear +update = Actualizar +refresh = Actualizar + +## Status Messages +success = Éxito +warning = Advertencia +info = Información + +## Input Placeholders +search-placeholder = Buscar... +message-input = Escribe tu mensaje... + +## Demo and Testing +welcome-user = ¡Bienvenido, { $username }! +notification-count = { $count -> + [0] Sin notificaciones + [1] Una notificación + *[other] { $count } notificaciones +} diff --git a/js/components/locales/fr-FR/common.ftl b/js/components/locales/fr-FR/common.ftl new file mode 100644 index 000000000..79fcd8f4d --- /dev/null +++ b/js/components/locales/fr-FR/common.ftl @@ -0,0 +1,41 @@ +# Common UI Translations - French (France) + +## General UI +loading = Chargement... +error = Erreur +cancel = Annuler +confirm = Confirmer +close = Fermer +open = Ouvrir +ok = OK +yes = Oui +no = Non +continue = Continuer +back = Retour +next = Suivant +finish = Terminer + +## Actions +save = Enregistrer +delete = Supprimer +edit = Modifier +create = Créer +update = Mettre à jour +refresh = Actualiser + +## Status Messages +success = Succès +warning = Avertissement +info = Information + +## Input Placeholders +search-placeholder = Rechercher... +message-input = Entrez votre message... + +## Demo and Testing +welcome-user = Bienvenue, { $username } ! +notification-count = { $count -> + [0] Aucune notification + [1] Une notification + *[other] { $count } notifications +} diff --git a/js/components/locales/pt-BR/common.ftl b/js/components/locales/pt-BR/common.ftl new file mode 100644 index 000000000..5ae0b0548 --- /dev/null +++ b/js/components/locales/pt-BR/common.ftl @@ -0,0 +1,41 @@ +# Common UI Translations - Portuguese (Brazil) + +## General UI +loading = Carregando... +error = Erro +cancel = Cancelar +confirm = Confirmar +close = Fechar +open = Abrir +ok = OK +yes = Sim +no = Não +continue = Continuar +back = Voltar +next = Próximo +finish = Concluir + +## Actions +save = Salvar +delete = Excluir +edit = Editar +create = Criar +update = Atualizar +refresh = Atualizar + +## Status Messages +success = Sucesso +warning = Aviso +info = Informação + +## Input Placeholders +search-placeholder = Pesquisar... +message-input = Digite sua mensagem... + +## Demo and Testing +welcome-user = Bem-vindo, { $username }! +notification-count = { $count -> + [0] Nenhuma notificação + [1] Uma notificação + *[other] { $count } notificações +} diff --git a/js/components/locales/zh-Hant/common.ftl b/js/components/locales/zh-Hant/common.ftl new file mode 100644 index 000000000..c42c0f78b --- /dev/null +++ b/js/components/locales/zh-Hant/common.ftl @@ -0,0 +1,41 @@ +# Common UI Translations - Chinese (Traditional) + +## General UI +loading = 載入中... +error = 錯誤 +cancel = 取消 +confirm = 確認 +close = 關閉 +open = 開啟 +ok = 確定 +yes = 是 +no = 否 +continue = 繼續 +back = 返回 +next = 下一步 +finish = 完成 + +## Actions +save = 儲存 +delete = 刪除 +edit = 編輯 +create = 建立 +update = 更新 +refresh = 重新整理 + +## Status Messages +success = 成功 +warning = 警告 +info = 資訊 + +## Input Placeholders +search-placeholder = 搜尋... +message-input = 輸入您的訊息... + +## Demo and Testing +welcome-user = 歡迎,{ $username }! +notification-count = { $count -> + [0] 無通知 + [1] 一則通知 + *[other] { $count } 則通知 +} diff --git a/js/components/package.json b/js/components/package.json index 9d5d9f486..7edb5054e 100644 --- a/js/components/package.json +++ b/js/components/package.json @@ -17,8 +17,10 @@ "author": "Streamplace", "license": "MIT", "devDependencies": { + "@fluent/syntax": "^0.19.0", "@types/sdp-transform": "^2.15.0", - "tsup": "^8.5.0" + "tsup": "^8.5.0", + "nodemon": "^3.1.10" }, "dependencies": { "@atproto/api": "^0.16.7", diff --git a/js/components/scripts/compile-translations.js b/js/components/scripts/compile-translations.js index a7e9e8107..81c1e3428 100644 --- a/js/components/scripts/compile-translations.js +++ b/js/components/scripts/compile-translations.js @@ -2,6 +2,7 @@ const fs = require("fs"); const path = require("path"); +const { parse: parseFtl } = require("@fluent/syntax"); // Load language manifest const MANIFEST_PATH = path.join(__dirname, "..", "locales", "manifest.json"); @@ -10,21 +11,20 @@ const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf-8")); // Configuration const LOCALES_SOURCE_DIR = path.join(__dirname, "..", "locales"); const LOCALES_OUTPUT_DIR = path.join(__dirname, "..", "public", "locales"); -const OUTPUT_FILENAME = "messages.json"; /** - * Recursively find all .ftl files in a directory + * Find all .ftl files in a directory (non-recursive, just top-level) */ function findFtlFiles(dir) { const files = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...findFtlFiles(fullPath)); - } else if (entry.isFile() && entry.name.endsWith(".ftl")) { - files.push(fullPath); + if (entry.isFile() && entry.name.endsWith(".ftl")) { + files.push({ + path: path.join(dir, entry.name), + namespace: path.basename(entry.name, ".ftl"), + }); } } @@ -32,111 +32,106 @@ function findFtlFiles(dir) { } /** - * Read and combine all .ftl files for a locale + * Parse FTL content using @fluent/syntax and extract messages */ -function combineLocaleFiles(localeDir) { - const ftlFiles = findFtlFiles(localeDir); +function extractMessagesFromFtl(content) { + const messages = {}; + + try { + const resource = parseFtl(content); + + for (const entry of resource.body) { + // Only process Message entries (not Comments, Terms, etc.) + if (entry.type === "Message" && entry.id) { + const key = entry.id.name; + + // For now, just serialize the pattern back to FTL format + // i18next-fluent will handle the actual parsing at runtime + if (entry.value) { + messages[key] = serializePattern(entry.value); + } + } + } + } catch (error) { + console.error("Error parsing FTL:", error.message); + throw error; + } - if (ftlFiles.length === 0) { - console.warn(`⚠️ No .ftl files found in ${localeDir}`); + return messages; +} + +/** + * Serialize a Fluent pattern back to FTL string format + * This is a simple serializer - @fluent/syntax has a full serializer + * but we just need the pattern text for JSON storage + */ +function serializePattern(pattern) { + if (!pattern || !pattern.elements) { return ""; } - const contents = []; - - for (const filePath of ftlFiles) { - try { - const content = fs.readFileSync(filePath, "utf-8"); - const relativePath = path.relative(localeDir, filePath); + let result = ""; - contents.push(`# === ${relativePath} ===`); - contents.push(content.trim()); - contents.push(""); // Empty line separator - } catch (error) { - console.error(`❌ Error reading ${filePath}:`, error.message); + for (const element of pattern.elements) { + if (element.type === "TextElement") { + result += element.value; + } else if (element.type === "Placeable") { + result += serializePlaceable(element); } } - return contents.join("\n"); + return result; } /** - * Parse FTL content and generate JSON with key-value pairs - * Handles multiline constructs like pluralization rules + * Serialize a placeable (variables, select expressions, etc.) */ -function generateJsonFromFtl(content) { - const translations = {}; - const lines = content.split("\n"); - - let currentKey = null; - let currentValue = []; - let insideMultiline = false; - let braceCount = 0; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmed = line.trim(); - - // Skip comments and empty lines when not inside a multiline construct - if ( - !insideMultiline && - (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("##")) - ) { - continue; - } +function serializePlaceable(placeable) { + if (!placeable.expression) { + return "{}"; + } - // Check for new key = value pair - const keyMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*(.*)$/); + const expr = placeable.expression; - if (keyMatch && !insideMultiline) { - // Save previous key if exists - if (currentKey) { - translations[currentKey] = currentValue.join("\n").trim(); - } + if (expr.type === "VariableReference") { + return `{$${expr.id.name}}`; + } else if (expr.type === "SelectExpression") { + let result = `{${serializeInlineExpression(expr.selector)} ->`; - const [, key, value] = keyMatch; - currentKey = key; - currentValue = [value]; - - // Check if this starts a multiline construct (contains opening brace) - const openBraces = (value.match(/\{/g) || []).length; - const closeBraces = (value.match(/\}/g) || []).length; - braceCount = openBraces - closeBraces; - - if (braceCount > 0) { - insideMultiline = true; - } else { - // Single line value, save immediately - translations[currentKey] = value.trim(); - currentKey = null; - currentValue = []; - } - } else if (insideMultiline && currentKey) { - // Continue accumulating multiline value - currentValue.push(line); // Preserve original formatting including indentation - - // Count braces to detect end of multiline construct - const openBraces = (line.match(/\{/g) || []).length; - const closeBraces = (line.match(/\}/g) || []).length; - braceCount += openBraces - closeBraces; - - if (braceCount <= 0) { - // End of multiline construct - translations[currentKey] = currentValue.join("\n").trim(); - currentKey = null; - currentValue = []; - insideMultiline = false; - braceCount = 0; - } + for (const variant of expr.variants) { + const key = + variant.key.type === "Identifier" + ? variant.key.name + : `[${variant.key.value}]`; + + result += `\n ${variant.default ? "*" : ""}${key} ${serializePattern(variant.value)}`; } + + result += "\n }"; + return result; + } else if (expr.type === "FunctionReference") { + const args = expr.arguments.positional + .map((arg) => serializeInlineExpression(arg)) + .join(", "); + return `{${expr.id.name}(${args})}`; } - // Handle any remaining key - if (currentKey) { - translations[currentKey] = currentValue.join("\n").trim(); + return "{}"; +} + +/** + * Serialize inline expressions (for function arguments, etc.) + */ +function serializeInlineExpression(expr) { + if (expr.type === "VariableReference") { + return `$${expr.id.name}`; + } else if (expr.type === "NumberLiteral") { + return expr.value; + } else if (expr.type === "StringLiteral") { + return `"${expr.value}"`; } - return JSON.stringify(translations, null, 2); + return ""; } /** @@ -155,7 +150,7 @@ function compileTranslations() { fs.mkdirSync(LOCALES_OUTPUT_DIR, { recursive: true }); } - // Get supported locales from manifest, but only include those with actual data directories + // Get supported locales from manifest const manifestLocales = manifest.supportedLocales; const locales = manifestLocales.filter((locale) => { const localeDir = path.join(LOCALES_SOURCE_DIR, locale); @@ -173,7 +168,6 @@ function compileTranslations() { for (const locale of locales) { const localeSourceDir = path.join(LOCALES_SOURCE_DIR, locale); const localeOutputDir = path.join(LOCALES_OUTPUT_DIR, locale); - const outputPath = path.join(localeOutputDir, OUTPUT_FILENAME); console.log(`📦 Processing locale: ${locale}`); @@ -182,28 +176,46 @@ function compileTranslations() { fs.mkdirSync(localeOutputDir, { recursive: true }); } - // Combine all .ftl files for this locale - const combinedContent = combineLocaleFiles(localeSourceDir); + // Find all .ftl files for this locale + const ftlFiles = findFtlFiles(localeSourceDir); - if (!combinedContent.trim()) { - console.warn(`⚠️ Skipping ${locale} - no content found`); + if (ftlFiles.length === 0) { + console.warn(`⚠️ No .ftl files found in ${localeSourceDir}`); continue; } - // Generate JSON from FTL content - const jsonContent = generateJsonFromFtl(combinedContent); - - // Write the output file - try { - fs.writeFileSync(outputPath, jsonContent, "utf-8"); - console.log(`✅ Generated: ${path.relative(process.cwd(), outputPath)}`); - totalFiles++; - } catch (error) { - console.error(`❌ Error writing ${outputPath}:`, error.message); + // Process each .ftl file as a separate namespace + for (const { path: ftlPath, namespace } of ftlFiles) { + try { + const content = fs.readFileSync(ftlPath, "utf-8"); + const messages = extractMessagesFromFtl(content); + + if (Object.keys(messages).length === 0) { + console.warn(`⚠️ No messages found in ${ftlPath}`); + continue; + } + + // Write namespace JSON file + const outputPath = path.join(localeOutputDir, `${namespace}.json`); + fs.writeFileSync( + outputPath, + JSON.stringify(messages, null, 2), + "utf-8", + ); + + console.log( + ` ✅ ${namespace}: ${Object.keys(messages).length} keys → ${path.relative(process.cwd(), outputPath)}`, + ); + totalFiles++; + } catch (error) { + console.error(`❌ Error processing ${ftlPath}:`, error.message); + } } } - console.log(`🎉 Compilation complete! ${totalFiles} files generated.`); + console.log( + `🎉 Compilation complete! ${totalFiles} namespace files generated.`, + ); } // Run the compilation diff --git a/js/components/src/i18n/i18next-config.ts b/js/components/src/i18n/i18next-config.ts index 1088bd897..67f82821f 100644 --- a/js/components/src/i18n/i18next-config.ts +++ b/js/components/src/i18n/i18next-config.ts @@ -116,11 +116,13 @@ const LOCALE = getCurrentLocaleSync(); export const I18NEXT_CONFIG = { lng: LOCALE, + ns: ["common", "settings"], // Common should be first as it's most frequently used + defaultNS: "common", interpolation: { - escapeValue: false, + escapeValue: false, // React already safes from XSS }, react: { - useSuspense: false, + useSuspense: false, // Prevent Android crashes }, i18nFormat: { fluentBundleOptions: { @@ -137,54 +139,67 @@ export const I18NEXT_CONFIG = { }, }, load: "currentOnly", + cleanCode: true, fallbackLng: getFallbackChain, supportedLngs: [...manifest.supportedLocales], debug: process.env.NODE_ENV === "development", }; -// Translation loading function that loads compiled JSON files -async function loadTranslationData(locale: string): Promise { +// Translation loading function that loads compiled JSON files per namespace +async function loadTranslationData( + locale: string, + namespace: string, +): Promise { try { let translations: any = {}; try { // For web environments, load from public directory if (typeof window !== "undefined") { - const response = await fetch(`/locales/${locale}/messages.json`); + const response = await fetch(`/locales/${locale}/${namespace}.json`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } translations = await response.json(); } else { // For React Native, use static requires for bundler compatibility - switch (locale) { - case "en": - case "en-US": - translations = require("../../public/locales/en-US/messages.json"); - break; - case "pt": - case "pt-BR": - translations = require("../../public/locales/pt-BR/messages.json"); - break; - case "es": - case "es-ES": - translations = require("../../public/locales/es-ES/messages.json"); - break; - case "zh": - case "zh-Hant": - translations = require("../../public/locales/zh-Hant/messages.json"); - break; - case "fr": - case "fr-FR": - translations = require("../../public/locales/fr-FR/messages.json"); - break; - default: - throw new Error(`No static translation file for locale: ${locale}`); + // Map base language codes to full locales + const fullLocale = locale.includes("-") + ? locale + : { + en: "en-US", + pt: "pt-BR", + es: "es-ES", + zh: "zh-Hant", + fr: "fr-FR", + }[locale] || locale; + + // Static requires for React Native bundler + const localeNamespaceKey = `${fullLocale}/${namespace}`; + const translationMap: Record = { + "en-US/common": require("../../public/locales/en-US/common.json"), + "pt-BR/common": require("../../public/locales/pt-BR/common.json"), + "es-ES/common": require("../../public/locales/es-ES/common.json"), + "zh-Hant/common": require("../../public/locales/zh-Hant/common.json"), + "fr-FR/common": require("../../public/locales/fr-FR/common.json"), + "en-US/settings": require("../../public/locales/en-US/settings.json"), + "pt-BR/settings": require("../../public/locales/pt-BR/settings.json"), + "es-ES/settings": require("../../public/locales/es-ES/settings.json"), + "zh-Hant/settings": require("../../public/locales/zh-Hant/settings.json"), + "fr-FR/settings": require("../../public/locales/fr-FR/settings.json"), + }; + + translations = translationMap[localeNamespaceKey]; + + if (!translations) { + throw new Error( + `No static translation mapping for ${localeNamespaceKey}`, + ); } } } catch (loadError: any) { throw new Error( - `Failed to load translations for ${locale}: ${loadError.message}`, + `Failed to load ${namespace} translations for ${locale}: ${loadError.message}`, ); } @@ -194,13 +209,15 @@ async function loadTranslationData(locale: string): Promise { return translations; } catch (error: any) { - console.error(`Failed to load translations for ${locale}:`, error); + console.error( + `Failed to load ${namespace} translations for ${locale}:`, + error, + ); // Return minimal fallback return { loading: "Loading...", error: "Error", cancel: "Cancel", - "settings-title": "Settings", }; } } @@ -230,8 +247,8 @@ export default async function initI18next( .use(Fluent) .use( resourcesToBackend((locale: string, namespace: string, callback: any) => { - // Load translations using our manifest-based system - loadTranslationData(locale) + // Load translations using our manifest-based namespace system + loadTranslationData(locale, namespace) .then((translations) => callback(null, translations)) .catch((error) => callback(error, null)); }), diff --git a/js/components/src/i18n/provider.tsx b/js/components/src/i18n/provider.tsx index 7c443a0a3..b5af475e9 100644 --- a/js/components/src/i18n/provider.tsx +++ b/js/components/src/i18n/provider.tsx @@ -19,13 +19,29 @@ export function I18nProvider({ children, i18n }: I18nProviderProps) { useEffect(() => { // Check if both initialized and language is loaded const checkReady = () => { - if (i18n.isInitialized && i18n.hasLoadedNamespace("translation")) { + const initialized = i18n.isInitialized; + const hasCommon = i18n.hasLoadedNamespace("common"); + console.log("i18n ready check:", { + initialized, + hasCommon, + language: i18n.language, + }); + + if (initialized && hasCommon) { setIsReady(true); } }; checkReady(); + // Fallback: if not ready after 300ms, just proceed anyway + const fallbackTimeout = setTimeout(() => { + if (!isReady) { + console.log("i18n ready timeout - proceeding anyway"); + setIsReady(true); + } + }, 300); + // Listen for both initialization and language loaded events const handleInitialized = () => { checkReady(); @@ -39,11 +55,12 @@ export function I18nProvider({ children, i18n }: I18nProviderProps) { i18n.on("loaded", handleLanguageLoaded); return () => { + clearTimeout(fallbackTimeout); i18n.off("initialized", handleInitialized); i18n.off("languageChanged", handleLanguageLoaded); i18n.off("loaded", handleLanguageLoaded); }; - }, [i18n]); + }, [i18n, isReady]); if (!isReady) { return null; diff --git a/js/docs/src/content/docs/guides/localizing/for-devs.md b/js/docs/src/content/docs/guides/localizing/for-devs.md index 4514ed321..9aedf7798 100644 --- a/js/docs/src/content/docs/guides/localizing/for-devs.md +++ b/js/docs/src/content/docs/guides/localizing/for-devs.md @@ -29,12 +29,14 @@ This is **NOT**: import { useTranslation } from 'react-i18next'; function MyComponent() { + // Use default namespace (common) const { t } = useTranslation(); return (
- {/* Simple translation */} -

{t('settings-title')}

+ {/* Simple translation from common namespace */} + + {/* With variables */}

{t('welcome-user', { username: 'Alice' })}

@@ -44,6 +46,18 @@ function MyComponent() {
); } + +function SettingsComponent() { + // Specify a namespace for settings-specific translations + const { t } = useTranslation('settings'); + + return ( +
+

{t('settings-title')}

+

{t('language-selection-description')}

+
+ ); +} ``` 2. For complex React components, use the `Trans` function from `react-i18next` @@ -106,22 +120,35 @@ function HelpText() { Settings You have 5 new messages -// ✅ Use translation keys +// ✅ Use translation keys with appropriate namespace +// Common UI elements use default namespace + + + +// Settings-specific use settings namespace +const { t } = useTranslation('settings'); {t('settings-title')} -{t('message-count', { count: 5 })} ``` -2. Add English translations (or others if you're proficient) to the .ftl files - in `js/components/locales/`: +2. Add translations to the appropriate .ftl files in `js/components/locales/`: + +**For common UI elements** (buttons, labels, etc.), use `common.ftl`: + +```fluent +# Edit: js/components/locales/en-US/common.ftl +save = Save +cancel = Cancel +loading = Loading... +error = Error +``` + +**For feature-specific translations**, use the appropriate namespace file: ```fluent # Edit: js/components/locales/en-US/settings.ftl settings-title = Settings -message-count = { $count -> - [0] No messages - [1] You have one new message - *[other] You have { $count } new messages -} +language-selection = Language +language-selection-description = Choose your preferred language ``` 3. Compile the translations to JSON: @@ -132,7 +159,8 @@ pnpm i18n:compile ``` This reads the `.ftl` files and outputs compiled JSON to -`js/components/public/locales/{locale}/messages.json`. +`js/components/public/locales/{locale}/{namespace}.json` (e.g., `common.json`, +`settings.json`). 4. For web: the compiled files in `public/locales/` are served as static assets and loaded on demand. @@ -146,24 +174,26 @@ The i18n system is centralized in `@streamplace/components`: ``` js/components/ -├── locales/ # Source .ftl files +├── locales/ # Source .ftl files (organized by namespace) │ ├── en-US/ -│ │ └── *.ftl +│ │ ├── common.ftl # Common UI elements (buttons, labels, etc.) +│ │ └── settings.ftl # Settings-specific translations │ ├── pt-BR/ │ ├── es-ES/ -│ ├── zh-TW/ +│ ├── zh-Hant/ │ └── fr-FR/ ├── src/i18n/ │ ├── manifest.json # Supported locales and metadata -│ ├── i18next-config.ts # Bootstrap configuration +│ ├── i18next-config.ts # Bootstrap configuration with namespace setup │ ├── provider.tsx # React provider components │ └── index.ts # Public exports -├── public/locales/ # Compiled JSON output +├── public/locales/ # Compiled JSON output (by locale and namespace) │ ├── en-US/ -│ │ └── messages.json +│ │ ├── common.json +│ │ └── settings.json │ └── ... └── scripts/ - ├── compile-translations.js + ├── compile-translations.js # Uses @fluent/syntax to parse .ftl files └── extract-i18n.js ``` @@ -173,6 +203,30 @@ The app imports i18n from `@streamplace/components`: import { i18next, useTranslation } from "@streamplace/components"; ``` +## Namespaces + +Translations are organized into **namespaces** to keep related translations +together and improve code organization: + +- **`common`** (default): General UI elements used across the app + + - Buttons: save, cancel, delete, edit, etc. + - Status messages: loading, error, success, warning + - Common actions: yes, no, continue, back, next + +- **`settings`**: Settings page translations + - App version and update messages + - Language selection + - Custom node configuration + - Debug recording settings + +When adding new feature areas, create a new namespace `.ftl` file: + +1. Create `locales/{locale}/feature.ftl` for each locale +2. Add the namespace to `I18NEXT_CONFIG.ns` array in `i18next-config.ts` +3. Add static `require()` entries for React Native in the translation map +4. Use `useTranslation('feature')` in your components + ## Available scripts In `js/components`: @@ -217,7 +271,7 @@ platform and uses the appropriate loading strategy. "en-US", "pt-BR", "es-ES", - "zh-TW", + "zh-Hant", "fr-FR", "new-LOCALE" ], @@ -232,18 +286,29 @@ platform and uses the appropriate loading strategy. } ``` -2. Create the locale directory and .ftl files in - `js/components/locales/new-LOCALE/` +2. Create the locale directory and copy .ftl files from another locale: + +```bash +cd js/components/locales +mkdir new-LOCALE +cp en-US/*.ftl new-LOCALE/ +``` + +3. Translate the .ftl files in `js/components/locales/new-LOCALE/` -3. Add a static `require()` case in `js/components/src/i18n/i18next-config.ts`: +4. Add static `require()` entries for all namespaces in + `js/components/src/i18n/i18next-config.ts`: ```ts -case "new-LOCALE": - translations = require("../../public/locales/new-LOCALE/messages.json"); - break; +const translationMap: Record = { + // ... existing entries + "new-LOCALE/common": require("../../public/locales/new-LOCALE/common.json"), + "new-LOCALE/settings": require("../../public/locales/new-LOCALE/settings.json"), + // Add any other namespaces you've created +}; ``` -4. Run `pnpm i18n:compile` to generate the JSON file +5. Run `pnpm i18n:compile` to generate the JSON files You can also [view the official Fluent docs](https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fac69997..72a6d16a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -537,9 +537,15 @@ importers: specifier: ^5.0.5 version: 5.0.5(immer@10.1.1)(react@19.0.0)(use-sync-external-store@1.2.2(react@19.0.0)) devDependencies: + '@fluent/syntax': + specifier: ^0.19.0 + version: 0.19.0 '@types/sdp-transform': specifier: ^2.15.0 version: 2.15.0 + nodemon: + specifier: ^3.1.10 + version: 3.1.10 tsup: specifier: ^8.5.0 version: 8.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.5.1) @@ -7993,6 +7999,9 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + ignore-walk@6.0.5: resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -9616,6 +9625,11 @@ packages: resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==} engines: {node: '>=6.0.0'} + nodemon@3.1.10: + resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} + engines: {node: '>=10'} + hasBin: true + nopt@6.0.0: resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -10434,6 +10448,9 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -11283,6 +11300,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -11828,6 +11849,10 @@ packages: resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} engines: {node: '>=10'} + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -12080,6 +12105,9 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + underscore.string@3.3.6: resolution: {integrity: sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==} @@ -12970,7 +12998,7 @@ snapshots: '@astrojs/telemetry@3.2.1': dependencies: ci-info: 4.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) dlv: 1.1.3 dset: 3.1.4 is-docker: 3.0.0 @@ -14254,7 +14282,7 @@ snapshots: '@babel/traverse': 7.25.9 '@babel/types': 7.26.0 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -14322,7 +14350,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -14867,7 +14895,7 @@ snapshots: '@babel/parser': 7.26.2 '@babel/template': 7.25.9 '@babel/types': 7.26.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -14879,7 +14907,7 @@ snapshots: '@babel/parser': 7.27.3 '@babel/template': 7.27.2 '@babel/types': 7.27.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -14997,7 +15025,7 @@ snapshots: '@electron/get': 3.1.0 chalk: 4.1.2 commander: 4.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 10.1.0 listr2: 7.0.2 semver: 7.7.1 @@ -15012,7 +15040,7 @@ snapshots: '@electron/rebuild': 3.6.0 '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) find-up: 5.0.0 fs-extra: 10.1.0 log-symbols: 4.1.0 @@ -15040,7 +15068,7 @@ snapshots: '@electron/rebuild': 3.6.0 '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fast-glob: 3.3.2 filenamify: 4.3.0 find-up: 5.0.0 @@ -15157,7 +15185,7 @@ snapshots: '@electron-forge/shared-types': 7.5.0 '@electron-forge/web-multi-logger': 7.5.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fast-glob: 3.3.2 fs-extra: 10.1.0 html-webpack-plugin: 5.6.0(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) @@ -15190,7 +15218,7 @@ snapshots: '@aws-sdk/types': 3.679.0 '@electron-forge/publisher-static': 7.5.0 '@electron-forge/shared-types': 7.5.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - aws-crt - bluebird @@ -15218,7 +15246,7 @@ snapshots: dependencies: '@electron-forge/shared-types': 7.5.0 '@malept/cross-spawn-promise': 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 10.1.0 username: 5.1.0 transitivePeerDependencies: @@ -15292,7 +15320,7 @@ snapshots: '@electron/get@2.0.3': dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) env-paths: 2.2.1 fs-extra: 8.1.0 got: 11.8.6 @@ -15306,7 +15334,7 @@ snapshots: '@electron/get@3.1.0': dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) env-paths: 2.2.1 fs-extra: 8.1.0 got: 11.8.6 @@ -15320,7 +15348,7 @@ snapshots: '@electron/notarize@2.4.0': dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 9.1.0 promise-retry: 2.0.1 transitivePeerDependencies: @@ -15329,7 +15357,7 @@ snapshots: '@electron/osx-sign@1.3.1': dependencies: compare-version: 0.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 10.1.0 isbinaryfile: 4.0.10 minimist: 1.2.8 @@ -15345,7 +15373,7 @@ snapshots: '@electron/osx-sign': 1.3.1 '@electron/universal': 2.0.1 '@electron/windows-sign': 1.1.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) extract-zip: 2.0.1 filenamify: 4.3.0 fs-extra: 11.2.0 @@ -15365,7 +15393,7 @@ snapshots: dependencies: '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) detect-libc: 2.0.4 fs-extra: 10.1.0 got: 11.8.6 @@ -15385,7 +15413,7 @@ snapshots: dependencies: '@electron/asar': 3.2.13 '@malept/cross-spawn-promise': 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) dir-compare: 4.2.0 fs-extra: 11.2.0 minimatch: 9.0.5 @@ -15396,7 +15424,7 @@ snapshots: '@electron/windows-sign@1.1.3': dependencies: cross-dirname: 0.1.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 11.2.0 minimist: 1.2.8 postject: 1.0.0-alpha.6 @@ -15506,7 +15534,7 @@ snapshots: '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -15516,7 +15544,7 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.1 @@ -15566,7 +15594,7 @@ snapshots: ci-info: 3.9.0 compression: 1.7.4 connect: 3.7.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) env-editor: 0.4.2 freeport-async: 2.0.0 getenv: 2.0.0 @@ -15616,7 +15644,7 @@ snapshots: '@expo/plist': 0.3.4 '@expo/sdk-runtime-versions': 1.0.0 chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) getenv: 1.0.0 glob: 10.4.5 resolve-from: 5.0.0 @@ -15635,7 +15663,7 @@ snapshots: '@expo/plist': 0.2.0 '@expo/sdk-runtime-versions': 1.0.0 chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) getenv: 1.0.0 glob: 10.4.5 resolve-from: 5.0.0 @@ -15707,7 +15735,7 @@ snapshots: '@expo/env@0.4.0': dependencies: chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) dotenv: 16.4.5 dotenv-expand: 11.0.6 getenv: 1.0.0 @@ -15717,7 +15745,7 @@ snapshots: '@expo/env@1.0.5': dependencies: chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) dotenv: 16.4.5 dotenv-expand: 11.0.6 getenv: 1.0.0 @@ -15729,7 +15757,7 @@ snapshots: '@expo/spawn-async': 1.7.2 arg: 5.0.2 chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) find-up: 5.0.0 getenv: 2.0.0 ignore: 5.3.1 @@ -15774,7 +15802,7 @@ snapshots: '@expo/json-file': 9.0.0 '@expo/spawn-async': 1.7.2 chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 9.1.0 getenv: 1.0.0 glob: 10.4.5 @@ -15797,7 +15825,7 @@ snapshots: '@expo/json-file': 9.1.4 '@expo/spawn-async': 1.7.2 chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) dotenv: 16.4.5 dotenv-expand: 11.0.6 getenv: 1.0.0 @@ -15853,7 +15881,7 @@ snapshots: '@expo/image-utils': 0.7.4 '@expo/json-file': 9.1.4 '@react-native/normalize-colors': 0.79.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) resolve-from: 5.0.0 semver: 7.7.1 xml2js: 0.6.0 @@ -19032,7 +19060,7 @@ snapshots: '@typescript-eslint/types': 8.13.0 '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.13.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 9.14.0(jiti@2.4.2) optionalDependencies: typescript: 5.6.3 @@ -19048,7 +19076,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 @@ -19062,7 +19090,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.13.0 '@typescript-eslint/visitor-keys': 8.13.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -19270,13 +19298,13 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color agent-base@7.1.1: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -19534,7 +19562,7 @@ snapshots: common-ancestor-path: 1.0.1 cookie: 1.0.2 cssesc: 3.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) deterministic-object-hash: 2.0.2 devalue: 5.1.1 diff: 5.2.0 @@ -19757,7 +19785,7 @@ snapshots: babel-plugin-react-native-web: 0.19.13 babel-plugin-syntax-hermes-parser: 0.25.1 babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.26.0) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) react-refresh: 0.14.2 resolve-from: 5.0.0 transitivePeerDependencies: @@ -19784,7 +19812,7 @@ snapshots: babel-plugin-react-native-web: 0.19.13 babel-plugin-syntax-hermes-parser: 0.25.1 babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.26.0) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) react-refresh: 0.14.2 resolve-from: 5.0.0 transitivePeerDependencies: @@ -20771,9 +20799,11 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.0: + debug@4.4.0(supports-color@5.5.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 decamelize-keys@1.1.1: dependencies: @@ -21001,7 +21031,7 @@ snapshots: dependencies: '@malept/cross-spawn-promise': 1.1.1 asar: 3.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 9.1.0 glob: 7.2.3 lodash: 4.17.21 @@ -21017,7 +21047,7 @@ snapshots: electron-installer-debian@3.2.0: dependencies: '@malept/cross-spawn-promise': 1.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) electron-installer-common: 0.10.3 fs-extra: 9.1.0 get-folder-size: 2.0.1 @@ -21031,7 +21061,7 @@ snapshots: electron-installer-dmg@5.0.1: dependencies: '@types/appdmg': 0.5.5 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) minimist: 1.2.8 optionalDependencies: appdmg: 0.6.6 @@ -21042,7 +21072,7 @@ snapshots: electron-installer-redhat@3.4.0: dependencies: '@malept/cross-spawn-promise': 1.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) electron-installer-common: 0.10.3 fs-extra: 9.1.0 lodash: 4.17.21 @@ -21065,7 +21095,7 @@ snapshots: electron-winstaller@5.4.0: dependencies: '@electron/asar': 3.2.13 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 7.0.1 lodash: 4.17.21 temp: 0.9.4 @@ -21388,7 +21418,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -21751,7 +21781,7 @@ snapshots: expo-system-ui@5.0.8(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native-web@0.20.0(encoding@0.1.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)): dependencies: '@react-native/normalize-colors': 0.79.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) expo: 53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10) react-native: 0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) optionalDependencies: @@ -21931,7 +21961,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -22161,7 +22191,7 @@ snapshots: flora-colossus@2.0.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 10.1.0 transitivePeerDependencies: - supports-color @@ -22179,7 +22209,7 @@ snapshots: follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fontfaceobserver@2.3.0: {} @@ -22348,7 +22378,7 @@ snapshots: galactus@1.0.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) flora-colossus: 2.0.0 fs-extra: 10.1.0 transitivePeerDependencies: @@ -23080,14 +23110,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -23126,14 +23156,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -23225,6 +23255,8 @@ snapshots: ieee754@1.2.1: {} + ignore-by-default@1.0.1: {} + ignore-walk@6.0.5: dependencies: minimatch: 9.0.5 @@ -23333,7 +23365,7 @@ snapshots: dependencies: '@ioredis/commands': 1.3.1 cluster-key-slot: 1.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -23786,7 +23818,7 @@ snapshots: dependencies: '@types/express': 4.17.21 '@types/jsonwebtoken': 9.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) jose: 4.15.9 limiter: 1.1.5 lru-memoizer: 2.3.0 @@ -24647,7 +24679,7 @@ snapshots: metro-file-map@0.82.4: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -24743,7 +24775,7 @@ snapshots: chalk: 4.1.2 ci-info: 2.0.0 connect: 3.7.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -25032,7 +25064,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -25354,6 +25386,19 @@ snapshots: nodemailer@6.10.1: {} + nodemon@3.1.10: + dependencies: + chokidar: 3.6.0 + debug: 4.4.0(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.1 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + nopt@6.0.0: dependencies: abbrev: 1.1.1 @@ -26284,6 +26329,8 @@ snapshots: proxy-from-env@1.1.0: {} + pstree.remy@1.1.8: {} + pump@3.0.0: dependencies: end-of-stream: 1.4.4 @@ -26822,7 +26869,7 @@ snapshots: read-binary-file-arch@1.0.6: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -27591,6 +27638,10 @@ snapshots: dependencies: is-arrayish: 0.3.2 + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.1 + sisteransi@1.0.5: {} sitemap@8.0.0: @@ -27627,7 +27678,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -27635,7 +27686,7 @@ snapshots: socks-proxy-agent@8.0.4: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -27697,7 +27748,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -27708,7 +27759,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -27969,7 +28020,7 @@ snapshots: sumchecker@3.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -28186,6 +28237,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + touch@3.1.1: {} + tr46@0.0.3: {} tr46@1.0.1: @@ -28294,7 +28347,7 @@ snapshots: cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) esbuild: 0.25.3 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 @@ -28320,7 +28373,7 @@ snapshots: tuf-js@2.2.1: dependencies: '@tufjs/models': 2.0.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) make-fetch-happen: 13.0.1 transitivePeerDependencies: - supports-color @@ -28444,6 +28497,8 @@ snapshots: uncrypto@0.1.3: {} + undefsafe@2.0.5: {} + underscore.string@3.3.6: dependencies: sprintf-js: 1.1.3