diff --git a/Views/LiveCaptions.js b/Views/LiveCaptions.js index e84ad73..d8e69c4 100644 --- a/Views/LiveCaptions.js +++ b/Views/LiveCaptions.js @@ -1,10 +1,11 @@ import React from "react"; -import { View, FlatList } from "react-native"; +import { View } from "react-native"; import { Text, Chip, Menu, Button } from "react-native-paper"; import API from "../API.js"; import i18n from "../i18nMessages.js"; const POLL_MS = 1800; +const CAPTION_META_KEYS = new Set(["sequence", "createdAt", "sourceLang", "original", "translations"]); const toLangLabel = (lang = "") => { const normalized = String(lang || "").toLowerCase(); @@ -16,22 +17,45 @@ const toLangLabel = (lang = "") => { return normalized.toUpperCase(); }; +const inferSourceLang = (item) => { + if (!item || typeof item !== "object") return ""; + if (item.sourceLang) return String(item.sourceLang).toLowerCase(); + const original = typeof item.original === "string" ? item.original.trim() : ""; + if (!original) return ""; + const direct = Object.entries(item).find(([key, value]) => !CAPTION_META_KEYS.has(key) && typeof value === "string" && value.trim() === original); + if (direct?.[0]) return String(direct[0]).toLowerCase(); + const nested = Object.entries(item.translations || {}).find(([_, value]) => typeof value === "string" && value.trim() === original); + return nested?.[0] ? String(nested[0]).toLowerCase() : ""; +}; + +const getTextForLang = (item, lang) => { + if (!item || typeof item !== "object") return ""; + const normalizedLang = String(lang || "").toLowerCase(); + if (!normalizedLang || normalizedLang === "original") return item.original || ""; + const direct = typeof item[normalizedLang] === "string" ? item[normalizedLang].trim() : ""; + if (direct) return direct; + const nested = typeof item?.translations?.[normalizedLang] === "string" ? item.translations[normalizedLang].trim() : ""; + if (nested) return nested; + return ""; +}; + const CaptionRow = ({ item, selectedLang }) => { - const translation = item?.translations?.[selectedLang]; + const translation = getTextForLang(item, selectedLang); + const sourceLang = inferSourceLang(item); const displayTranslation = translation || item?.original || ""; - const isFallback = !translation && selectedLang && selectedLang !== "original" && selectedLang !== item?.sourceLang; + const isFallback = !translation && selectedLang && selectedLang !== "original" && selectedLang !== sourceLang; return ( - + - {item?.sourceLang ? `${i18n.t("message.originalLanguage")}: ${toLangLabel(item.sourceLang)}` : i18n.t("message.originalLanguage")} + {sourceLang ? `${i18n.t("message.originalLanguage")}: ${toLangLabel(sourceLang)}` : i18n.t("message.originalLanguage")} - + {item?.original || ""} {i18n.t("message.selectedTranslation")}: {toLangLabel(selectedLang)} - + {displayTranslation} {isFallback ? ( @@ -89,13 +113,52 @@ const LiveCaptions = () => { return () => clearInterval(interval); }, [refresh]); + const latestCaption = captions.length ? captions[captions.length - 1] : null; + + const extractLangsFromCaption = React.useCallback((caption) => { + const langs = new Set(); + if (!caption || typeof caption !== "object") return langs; + if (caption.sourceLang) langs.add(String(caption.sourceLang).toLowerCase()); + + const map = caption.translations; + if (map && typeof map === "object" && !Array.isArray(map)) { + Object.keys(map).forEach((lang) => { + if (lang) langs.add(String(lang).toLowerCase()); + }); + } + + // Backward/alternate payload support: languages at top-level (en, fr, es, etc.) + Object.entries(caption).forEach(([key, value]) => { + if (CAPTION_META_KEYS.has(key)) return; + if (typeof value !== "string" || !value.trim()) return; + const lang = String(key || "").toLowerCase().trim(); + if (!lang) return; + langs.add(lang); + }); + + return langs; + }, []); + const languageOptions = React.useMemo(() => { const unique = new Set(["original", ...availableLanguages]); - return Array.from(unique).filter(Boolean); - }, [availableLanguages]); + captions.forEach((caption) => { + const langs = extractLangsFromCaption(caption); + langs.forEach((lang) => unique.add(lang)); + }); + return Array.from(unique).filter((lang) => !!lang && lang !== "sourceLang" && lang !== "translations"); + }, [availableLanguages, captions, extractLangsFromCaption]); React.useEffect(() => { if (languageOptions.includes(selectedLang)) return; + const appLang = (i18n?.locale || "en").toLowerCase(); + if (languageOptions.includes(appLang)) { + setSelectedLang(appLang); + return; + } + if (languageOptions.includes("en")) { + setSelectedLang("en"); + return; + } if (languageOptions.length) setSelectedLang(languageOptions[0]); }, [languageOptions, selectedLang]); @@ -136,15 +199,8 @@ const LiveCaptions = () => { {isLoading ? {i18n.t("message.loadingLiveCaptions")} : <>} - {!isLoading && captions.length === 0 ? {i18n.t("message.awaitingLiveCaptions")} : <>} - - `${item?.sequence || "caption"}-${index}`} - renderItem={({ item }) => } - contentContainerStyle={{ paddingBottom: 18 }} - style={{ flex: 1 }} - /> + {!isLoading && !latestCaption ? {i18n.t("message.awaitingLiveCaptions")} : <>} + {!isLoading && latestCaption ? : <>} ); };