import React from "react"; 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(); if (!normalized || normalized === "original") return i18n.t("message.originalLanguage"); if (normalized === "en") return i18n.t("message.languageEnglish"); if (normalized === "es") return i18n.t("message.languageSpanish"); if (normalized === "fr") return i18n.t("message.languageFrench"); if (normalized === "da") return i18n.t("message.languageDanish"); 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 = getTextForLang(item, selectedLang); const sourceLang = inferSourceLang(item); const displayTranslation = translation || item?.original || ""; const isFallback = !translation && selectedLang && selectedLang !== "original" && selectedLang !== sourceLang; return ( {sourceLang ? `${i18n.t("message.originalLanguage")}: ${toLangLabel(sourceLang)}` : i18n.t("message.originalLanguage")} {item?.original || ""} {i18n.t("message.selectedTranslation")}: {toLangLabel(selectedLang)} {displayTranslation} {isFallback ? ( {i18n.t("message.translationFallback")} ) : <>} ); }; const LiveCaptions = () => { const [captions, setCaptions] = React.useState([]); const [availableLanguages, setAvailableLanguages] = React.useState([]); const [selectedLang, setSelectedLang] = React.useState((i18n?.locale || "en").toLowerCase()); const [isLoading, setIsLoading] = React.useState(true); const [langMenuVisible, setLangMenuVisible] = React.useState(false); const latestSequenceRef = React.useRef(0); const refresh = React.useCallback(async (initial = false) => { const sinceSequence = initial ? undefined : latestSequenceRef.current; const response = await API.getLiveCaptions(sinceSequence, 50); const incoming = Array.isArray(response?.captions) ? response.captions : []; const languages = Array.isArray(response?.availableLanguages) ? response.availableLanguages : []; if (initial) { setCaptions(incoming); } else if (incoming.length > 0) { setCaptions((prev) => { const bySequence = new Map(prev.map((item) => [item.sequence, item])); incoming.forEach((item) => { if (!item || !Number.isFinite(item.sequence)) return; bySequence.set(item.sequence, item); }); return Array.from(bySequence.values()).sort((a, b) => (a.sequence || 0) - (b.sequence || 0)).slice(-150); }); } if (Number.isFinite(response?.latestSequence)) { latestSequenceRef.current = response.latestSequence; } else if (incoming.length) { const maxSeq = incoming.reduce((acc, item) => Math.max(acc, Number(item?.sequence) || 0), latestSequenceRef.current); latestSequenceRef.current = maxSeq; } setAvailableLanguages(languages); setIsLoading(false); }, []); React.useEffect(() => { refresh(true); const interval = setInterval(() => { refresh(false); }, POLL_MS); 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]); 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]); return ( {i18n.t("message.liveCaptions")} {i18n.t("message.liveCaptionsSubtitle")} {i18n.t("message.liveNow")} {i18n.t("message.translationLanguage")} setLangMenuVisible(false)} anchor={ } > {languageOptions.map((lang) => ( { setSelectedLang(lang); setLangMenuVisible(false); }} /> ))} {isLoading ? {i18n.t("message.loadingLiveCaptions")} : <>} {!isLoading && !latestCaption ? {i18n.t("message.awaitingLiveCaptions")} : <>} {!isLoading && latestCaption ? : <>} ); }; export default LiveCaptions;