From 545f7bd7c31944cc7d621001e22a9ff41d32aae3 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 23 Jul 2025 09:09:42 +0300 Subject: [PATCH] added translations --- package-lock.json | 83 +++++++++- package.json | 5 +- scripts/generate_locales.js | 36 ++++ src/components/IndexManager/IndexManager.tsx | 69 ++++---- .../MultiPublish/MultiPublishDialog.tsx | 22 ++- .../ResourceList/ResourceListDisplay.tsx | 1 - .../ResourceListPreloadedDisplay.tsx | 1 - .../VideoPlayer/SubtitleManager.tsx | 49 ++++-- src/components/VideoPlayer/VideoControls.tsx | 156 +++++++----------- .../VideoPlayer/VideoControlsBar.tsx | 15 +- src/components/VideoPlayer/VideoPlayer.tsx | 2 - src/context/GlobalProvider.tsx | 5 + src/hooks/useIframe.tsx | 44 +++++ src/hooks/useLibTranslation.tsx | 7 + src/i18n/compiled-i18n.json | 86 ++++++++++ src/i18n/i18n.ts | 27 +++ src/i18n/locales/en/lib-core.json | 70 ++++++++ src/i18n/locales/es/lib-core.json | 5 + src/i18n/processors.ts | 53 ++++++ src/state/publishes.ts | 2 +- 20 files changed, 574 insertions(+), 164 deletions(-) create mode 100755 scripts/generate_locales.js create mode 100644 src/hooks/useIframe.tsx create mode 100644 src/hooks/useLibTranslation.tsx create mode 100644 src/i18n/compiled-i18n.json create mode 100644 src/i18n/i18n.ts create mode 100644 src/i18n/locales/en/lib-core.json create mode 100644 src/i18n/locales/es/lib-core.json create mode 100644 src/i18n/processors.ts diff --git a/package-lock.json b/package-lock.json index 15811f5..8fcfbd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "qapp-core", - "version": "1.0.37", + "version": "1.0.46", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "qapp-core", - "version": "1.0.37", + "version": "1.0.46", "license": "MIT", "dependencies": { "@tanstack/react-virtual": "^3.13.2", @@ -16,10 +16,12 @@ "dayjs": "^1.11.13", "dexie": "^4.0.11", "dompurify": "^3.2.4", + "i18next": "^25.3.2", "idb-keyval": "^6.2.2", "iso-639-1": "^3.1.5", "react-dropzone": "^14.3.8", "react-hot-toast": "^2.5.2", + "react-i18next": "^15.6.1", "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.16.0", "react-rnd": "^10.5.2", @@ -2395,6 +2397,46 @@ "dev": true, "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.3.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.2.tgz", + "integrity": "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/idb-keyval": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", @@ -3217,6 +3259,32 @@ "react-dom": ">=16" } }, + "node_modules/react-i18next": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.1.tgz", + "integrity": "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-idle-timer": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz", @@ -3947,7 +4015,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4024,6 +4092,15 @@ "global": "^4.3.1" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index 5624d06..51f49d9 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "dist" ], "scripts": { - "build": "tsup", + "generate-i18n": "./scripts/generate_locales.js", + "build": "npm run generate-i18n && tsup", "prepare": "npm run build", "clean": "rm -rf dist" }, @@ -30,10 +31,12 @@ "dayjs": "^1.11.13", "dexie": "^4.0.11", "dompurify": "^3.2.4", + "i18next": "^25.3.2", "idb-keyval": "^6.2.2", "iso-639-1": "^3.1.5", "react-dropzone": "^14.3.8", "react-hot-toast": "^2.5.2", + "react-i18next": "^15.6.1", "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.16.0", "react-rnd": "^10.5.2", diff --git a/scripts/generate_locales.js b/scripts/generate_locales.js new file mode 100755 index 0000000..6174dda --- /dev/null +++ b/scripts/generate_locales.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node + +const fg = require('fast-glob'); +const fs = require('fs'); +const path = require('path'); + +const LOCALES_DIR = path.resolve(__dirname, '../src/i18n/locales'); +const OUTPUT_FILE = path.join(__dirname, '../src/i18n/compiled-i18n.json'); + +(async () => { + const files = await fg('**/*.json', { cwd: LOCALES_DIR, absolute: true }); + + const resources = {}; + const supportedLanguages = new Set(); + + for (const filePath of files) { + const parts = filePath.split(path.sep); + const lang = parts[parts.length - 2]; + const ns = path.basename(filePath, '.json'); + + supportedLanguages.add(lang); + + const json = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + + if (!resources[lang]) resources[lang] = {}; + resources[lang][ns] = json; + } + + // Save compiled resources and languages + fs.writeFileSync( + OUTPUT_FILE, + JSON.stringify({ resources, supportedLanguages: Array.from(supportedLanguages) }, null, 2) + ); + + console.log('✅ i18n resources generated.'); +})(); diff --git a/src/components/IndexManager/IndexManager.tsx b/src/components/IndexManager/IndexManager.tsx index fa4c0e8..86fd56f 100644 --- a/src/components/IndexManager/IndexManager.tsx +++ b/src/components/IndexManager/IndexManager.tsx @@ -39,6 +39,8 @@ import { useModal } from "../useModal"; import { createAvatarLink } from "../../utils/qortal"; import { extractComponents } from "../../utils/text"; import NavigateNextIcon from "@mui/icons-material/NavigateNext"; +import { useLibTranslation } from "../../hooks/useLibTranslation"; +import { t } from "i18next"; const uid = new ShortUniqueId({ length: 10, dictionary: "alphanum" }); @@ -57,6 +59,8 @@ interface PropsIndexManager { const cleanString = (str: string) => str.replace(/\s{2,}/g, ' ').trim().toLocaleLowerCase(); export const IndexManager = ({ username }: PropsIndexManager) => { + const { t } = useLibTranslation(); + const open = useIndexStore((state) => state.open); const setOpen = useIndexStore((state) => state.setOpen); const [title, setTitle] = useState(""); @@ -123,7 +127,7 @@ export const IndexManager = ({ username }: PropsIndexManager) => { }, }} > - Index manager + {t("index.title")} { + const { t } = useLibTranslation(); + return ( <> @@ -222,7 +228,7 @@ const EntryMode = ({ width: "100%", }} > - Create new index + {t("index.create_new_index")} {/* - Add metadata + {t("index.add_metadata")} {hasMetadata && } @@ -288,12 +294,14 @@ const AddMetadata = ({ setDescription, setTitle, }: PropsAddMetadata) => { + const { t } = useLibTranslation(); + const publish = usePublish(); const disableButton = !title.trim() || !description.trim() || !name || !link; const createMetadata = async () => { - const loadId = showLoading("Publishing metadata..."); + const loadId = showLoading(t("index.publishing_metadata")); try { const identifierWithoutHash = name + link; const identifier = await hashWordWithoutPublicSalt( @@ -313,7 +321,7 @@ const AddMetadata = ({ }); if (res?.signature) { - showSuccess("Successfully published metadata"); + showSuccess(t("index.published_metadata")); publish.updatePublish( { identifier, @@ -325,7 +333,7 @@ const AddMetadata = ({ } } catch (error) { const message = - error instanceof Error ? error.message : "Failed to publish metadata"; + error instanceof Error ? error.message : t("index.failed_metadata"); showError(message); } finally { dismissToast(loadId); @@ -350,7 +358,7 @@ const AddMetadata = ({ setMode(1)}> - Example of how it could look like: + {t("index.example")} - Title + {t("index.metadata.title")} setTitle(e.target.value)} size="small" - placeholder="Add a title for the link" + placeholder={t("index.metadata_title_placeholder")} slotProps={{ htmlInput: { maxLength: 50 }, }} @@ -452,19 +460,19 @@ const AddMetadata = ({ variant="caption" color={title.length >= 50 ? "error" : "text.secondary"} > - {title.length}/{50} characters + {title.length}/{50} {` ${t("index.characters")}`} } /> - Description + {t("index.metadata.description")} setDescription(e.target.value)} size="small" - placeholder="Add a description for the link" + placeholder={t("index.metadata_description_placeholder")} slotProps={{ htmlInput: { maxLength: 120 }, }} @@ -473,7 +481,7 @@ const AddMetadata = ({ variant="caption" color={description.length >= 120 ? "error" : "text.secondary"} > - {description.length}/{120} characters + {description.length}/{120} {` ${t("index.characters")}`} } /> @@ -486,7 +494,7 @@ const AddMetadata = ({ disabled={disableButton} variant="contained" > - Publish metadata + {t("actions.publish_metadata")} @@ -507,6 +515,8 @@ const CreateIndex = ({ category, rootName, }: PropsCreateIndex) => { + const { t } = useLibTranslation(); + const [terms, setTerms] = useState([]); const publish = usePublish(); const [size, setSize] = useState(0); @@ -588,7 +598,7 @@ const CreateIndex = ({ const disableButton = (terms.length === 0 && !recommendedSelection) || !name || !link; const createIndex = async () => { - const loadId = showLoading("Publishing index..."); + const loadId = showLoading(t("index.publishing_index")); try { const hashedRootName = await hashWordWithoutPublicSalt(rootName, 20); const hashedLink = await hashWordWithoutPublicSalt(link, 20); @@ -603,7 +613,7 @@ const CreateIndex = ({ }); if (res?.signature) { - showSuccess("Successfully published index"); + showSuccess(t("index.published_index")); publish.updatePublish( { identifier, @@ -616,7 +626,7 @@ const CreateIndex = ({ } } catch (error) { const message = - error instanceof Error ? error.message : "Failed to publish index"; + error instanceof Error ? error.message : t("index.failed_index"); showError(message); } finally { dismissToast(loadId); @@ -645,7 +655,7 @@ const CreateIndex = ({ {recommendedIndices?.length > 0 && ( <> - Recommended Indices + {t("index.recommended_indices")} - } label="Add search term" /> + } label={t("index.add_search_term")} /> {!recommendedSelection && ( @@ -681,7 +691,7 @@ const CreateIndex = ({ maxLength={17} items={terms} onlyStrings - label="search terms" + label={t("index.search_terms")} setItems={async (termsNew: string[]) => { try { if (terms?.length === 1 && termsNew?.length === 2) { @@ -704,8 +714,10 @@ const CreateIndex = ({ shouldRecommendMax && fullSize > 230 ? "visible" : "hidden", }} > - It is recommended to keep your term character count below{" "} - {recommendedSize} characters + + {t("index.recommendation_size", { + recommendedSize + })} @@ -716,7 +728,7 @@ const CreateIndex = ({ disabled={disableButton} variant="contained" > - Publish index + {t("actions.publish_index")} - Adding multiple indices + {t("index.multiple_title")} - Subsequent indices will keep your publish fees lower, but they will - have less strength in future search results. + {t("index.multiple_description")} - + @@ -762,6 +773,8 @@ const YourIndices = ({ category, rootName, }: PropsCreateIndex) => { + const { t } = useLibTranslation(); + const [terms, setTerms] = useState([]); const publish = usePublish(); const [size, setSize] = useState(0); diff --git a/src/components/MultiPublish/MultiPublishDialog.tsx b/src/components/MultiPublish/MultiPublishDialog.tsx index b4f0f7d..e9e0f6a 100644 --- a/src/components/MultiPublish/MultiPublishDialog.tsx +++ b/src/components/MultiPublish/MultiPublishDialog.tsx @@ -22,6 +22,8 @@ import { ResourceToPublish } from "../../types/qortalRequests/types"; import { QortalGetMetadata } from "../../types/interfaces/resources"; import ErrorIcon from '@mui/icons-material/Error'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import { useLibTranslation } from "../../hooks/useLibTranslation"; +import { t } from "i18next"; export interface MultiplePublishError { error: { @@ -31,6 +33,8 @@ export interface MultiplePublishError { export const MultiPublishDialogComponent = () => { + const { t } = useLibTranslation(); + const { resources, isPublishing, @@ -177,7 +181,7 @@ export const MultiPublishDialogComponent = () => { disableAutoFocus disableRestoreFocus > - Publishing Status + {t("multi_publish.title")} {publishError && ( @@ -215,7 +219,7 @@ export const MultiPublishDialogComponent = () => { gap: '10px' }}> - Publish failed + {t("multi_publish.publish_failed")} )} @@ -229,7 +233,7 @@ export const MultiPublishDialogComponent = () => { reject(new Error('Canceled Publish')); reset(); }}> - Close + {t("actions.close")} {failedResources?.length > 0 && ( )} @@ -306,14 +310,14 @@ useEffect(() => { - File Chunk {publishStatus?.chunks || 0}/{publishStatus?.totalChunks || 0} ({chunkPercent.toFixed(0)}%) + {t("multi_publish.file_chunk")} {publishStatus?.chunks || 0}/{publishStatus?.totalChunks || 0} ({chunkPercent.toFixed(0)}%) - File Processing ({publishStatus?.processed ? 100 : processingStart ? processingPercent.toFixed(0) : '0'}%) + {t("multi_publish.file_processing")} ({publishStatus?.processed ? 100 : processingStart ? processingPercent.toFixed(0) : '0'}%) @@ -321,14 +325,14 @@ useEffect(() => { {publishStatus?.processed && ( - Published successfully + {t("multi_publish.success")} )} {publishStatus?.retry && !publishStatus?.error && !publishStatus?.processed && ( - Publish failed. Attempting retry... + {t("multi_publish.attempt_retry")} )} @@ -336,7 +340,7 @@ useEffect(() => { - Publish failed. {publishStatus?.error?.reason || 'Unknown error'} + {t("multi_publish.publish_failed")} - {publishStatus?.error?.reason || 'Unknown error'} )} diff --git a/src/components/ResourceList/ResourceListDisplay.tsx b/src/components/ResourceList/ResourceListDisplay.tsx index 5b817a4..bbcaefc 100644 --- a/src/components/ResourceList/ResourceListDisplay.tsx +++ b/src/components/ResourceList/ResourceListDisplay.tsx @@ -373,7 +373,6 @@ removeFromList(listName, displayLimit) getResourceMoreList(displayLimit) }, [getResourceMoreList]) - console.log('listToDisplay', listToDisplay) return (
{ + const { t } = useLibTranslation(); + const [mode, setMode] = useState(1); const [isOpenPublish, setIsOpenPublish] = useState(false); const { lists, identifierOperations, auth } = useGlobal(); @@ -308,7 +311,7 @@ const SubtitleManagerComponent = ({ fontSize: "0.85rem", }} > - Subtitles + {t("subtitle.subtitles")} - No subtitles + {t("subtitle.no_subtitles")} )} @@ -393,7 +396,7 @@ const SubtitleManagerComponent = ({ disabled={showAll} onClick={() => setShowAll(true)} > - Load community subs + {t("subtitle.load_community_subs")} @@ -424,6 +427,8 @@ const PublisherSubtitles = ({ onBack, currentSubTrack, }: PublisherSubtitlesProps) => { + const { t } = useLibTranslation(); + return ( <> - Off + {t("subtitle.off")} {!currentSubTrack ? : } @@ -470,6 +475,8 @@ const PublishSubtitles = ({ setIsOpen, mySubtitles, }: PublishSubtitlesProps) => { + const { t } = useLibTranslation(); + const [language, setLanguage] = useState(null); const [subtitles, setSubtitles] = useState([]); const [isPublishing, setIsPublishing] = useState(false); @@ -488,7 +495,7 @@ const PublishSubtitles = ({ }; newSubtitles.push(newSubtitle); } catch (error) { - console.error("Failed to parse audio file:", error); + console.error("Failed to convert to base64:", error); } } setSubtitles((prev) => [...newSubtitles, ...prev]); @@ -538,11 +545,13 @@ const PublishSubtitles = ({ let loadId; try { setIsPublishing(true); - loadId = showLoading("Deleting subtitle..."); + loadId = showLoading(t("subtitle.deleting_subtitle")); await lists.deleteResource([sub]); - showSuccess("Deleted subtitle"); + showSuccess(t("subtitle.deleted")); } catch (error) { - showError(error instanceof Error ? error.message : "Unable to delete"); + showError( + error instanceof Error ? error.message : t("subtitle.unable_delete") + ); } finally { setIsPublishing(false); dismissToast(loadId); @@ -553,12 +562,14 @@ const PublishSubtitles = ({ let loadId; try { setIsPublishing(true); - loadId = showLoading("Publishing subtitles..."); + loadId = showLoading(t("subtitle.publishing")); await publishHandler(subtitles); - showSuccess("Subtitles published"); + showSuccess(t("subtitle.published")); setSubtitles([]); } catch (error) { - showError(error instanceof Error ? error.message : "Unable to publish"); + showError( + error instanceof Error ? error.message : t("subtitle.unable_publish") + ); } finally { dismissToast(loadId); setIsPublishing(false); @@ -587,7 +598,7 @@ const PublishSubtitles = ({ }, }} > - My Subtitles + {t("subtitle.my_subtitles")} - - + + @@ -651,7 +662,7 @@ const PublishSubtitles = ({ variant="contained" > - Import subtitles + {t("subtitle.import_subtitles")} {subtitles?.map((sub, i) => { @@ -697,7 +708,7 @@ const PublishSubtitles = ({ size="small" color="secondary" > - remove + {t("actions.remove")} @@ -739,7 +750,7 @@ const PublishSubtitles = ({ disabled={disableButton} variant="contained" > - Publish + {t("actions.publish")} )} @@ -849,6 +860,8 @@ interface MySubtitleProps { onDelete: (subtitle: QortalGetMetadata) => void; } const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => { + const { t } = useLibTranslation(); + const { resource, isLoading, error } = usePublish(2, "JSON", sub); return ( { size="small" color="secondary" > - delete + {t("actions.delete")} diff --git a/src/components/VideoPlayer/VideoControls.tsx b/src/components/VideoPlayer/VideoControls.tsx index d737f9c..545e842 100644 --- a/src/components/VideoPlayer/VideoControls.tsx +++ b/src/components/VideoPlayer/VideoControls.tsx @@ -3,9 +3,7 @@ import { Box, ButtonBase, Divider, - Fade, IconButton, - Popover, Popper, Slider, Typography, @@ -13,11 +11,9 @@ import { } from "@mui/material"; export const fontSizeExSmall = "60%"; export const fontSizeSmall = "80%"; -import AspectRatioIcon from "@mui/icons-material/AspectRatio"; import { Fullscreen, Pause, - PictureInPicture, PlayArrow, Refresh, VolumeOff, @@ -25,17 +21,20 @@ import { } from "@mui/icons-material"; import { formatTime } from "../../utils/time.js"; import { CustomFontTooltip } from "./CustomFontTooltip.js"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import SlowMotionVideoIcon from "@mui/icons-material/SlowMotionVideo"; const buttonPaddingBig = "6px"; const buttonPaddingSmall = "4px"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos"; import CheckIcon from "@mui/icons-material/Check"; +import { useLibTranslation } from "../../hooks/useLibTranslation.js"; export const PlayButton = ({ togglePlay, isPlaying, isScreenSmall }: any) => { + const { t } = useLibTranslation(); + return ( - + { }; export const ReloadButton = ({ reloadVideo, isScreenSmall }: any) => { + const { t } = useLibTranslation(); + return ( - + { const sliderRef = useRef(null); const [isDragging, setIsDragging] = useState(false); @@ -117,26 +118,25 @@ export const ProgressSlider = ({ const debounceTimeoutRef = useRef(null); const previousBlobUrlRef = useRef(null); -const handleMouseMove = (e: React.MouseEvent) => { - const slider = sliderRef.current; - if (!slider) return; + const handleMouseMove = (e: React.MouseEvent) => { + const slider = sliderRef.current; + if (!slider) return; - const rect = slider.getBoundingClientRect(); - const x = e.clientX - rect.left; - const percent = x / rect.width; - const time = Math.min(Math.max(0, percent * duration), duration); + const rect = slider.getBoundingClientRect(); + const x = e.clientX - rect.left; + const percent = x / rect.width; + const time = Math.min(Math.max(0, percent * duration), duration); - // Position anchor element at the correct spot - if (hoverAnchorRef.current) { - hoverAnchorRef.current.style.left = `${x}px`; - } + // Position anchor element at the correct spot + if (hoverAnchorRef.current) { + hoverAnchorRef.current.style.left = `${x}px`; + } - setHoverX(e.clientX); // optional – can be removed unless used elsewhere - setShowDuration(time); - + setHoverX(e.clientX); // optional – can be removed unless used elsewhere + setShowDuration(time); - if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current); -}; + if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current); + }; const handleMouseLeave = () => { lastRequestedTimeRef.current = null; setThumbnailUrl(null); @@ -166,14 +166,14 @@ const handleMouseMove = (e: React.MouseEvent) => { e.stopPropagation(); }; - useEffect(()=> { - if(!isOnTimeline) return - if(hoverX){ - isOnTimeline.current = true + useEffect(() => { + if (!isOnTimeline) return; + if (hoverX) { + isOnTimeline.current = true; } else { - isOnTimeline.current = false + isOnTimeline.current = false; } - }, [hoverX]) + }, [hoverX]); return ( { padding: isVideoPlayerSmall ? "0px" : "0px 10px", }} > - + { }; export const VideoTime = ({ progress, isScreenSmall, duration }: any) => { + const { t } = useLibTranslation(); + return ( { }; const VolumeButton = ({ isMuted, toggleMute }: any) => { + const { t } = useLibTranslation(); + return ( - + { + const { t } = useLibTranslation(); + const [isOpen, setIsOpen] = useState(false); const btnRef = useRef(null); const theme = useTheme(); @@ -383,7 +385,7 @@ export const PlaybackRate = ({ return ( <> @@ -403,55 +405,15 @@ export const PlaybackRate = ({ ); }; -export const ObjectFitButton = ({ toggleObjectFit, isScreenSmall }: any) => { - return ( - - toggleObjectFit()} - > - - - - ); -}; - -export const PictureInPictureButton = ({ - isFullscreen, - toggleRef, - togglePictureInPicture, - isScreenSmall, -}: any) => { - return ( - <> - {!isFullscreen && ( - - - - - - )} - - ); -}; - export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => { + const { t } = useLibTranslation(); + return ( - + { + const { t } = useLibTranslation(); + const theme = useTheme(); const ref = useRef(null); @@ -539,7 +503,7 @@ export const PlayBackMenu = ({ fontSize: "0.85rem", }} > - Playback speed + {t("video.playback_speed")} diff --git a/src/components/VideoPlayer/VideoControlsBar.tsx b/src/components/VideoPlayer/VideoControlsBar.tsx index 118cfcd..9017561 100644 --- a/src/components/VideoPlayer/VideoControlsBar.tsx +++ b/src/components/VideoPlayer/VideoControlsBar.tsx @@ -12,6 +12,8 @@ import { import SubtitlesIcon from "@mui/icons-material/Subtitles"; import { CustomFontTooltip } from "./CustomFontTooltip"; import { RefObject } from "react"; +import { useLibTranslation } from "../../hooks/useLibTranslation"; +import i18n from "../../i18n/i18n"; interface VideoControlsBarProps { canPlay: boolean; isScreenSmall: boolean; @@ -71,8 +73,10 @@ export const VideoControlsBar = ({ openPlaybackMenu, togglePictureInPicture, isVideoPlayerSmall, - isOnTimeline + isOnTimeline, }: VideoControlsBarProps) => { + const { t } = useLibTranslation(); + const showMobileControls = isScreenSmall && canPlay; const controlGroupSX = { @@ -145,8 +149,11 @@ export const VideoControlsBar = ({ increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} /> - {/* */} - + - {/* */} + diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index c72c40f..31fb382 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -631,10 +631,8 @@ export const VideoPlayer = ({ playerRef.current?.playbackRate(playbackRate); playerRef.current?.volume(volume); const key = `${resource.service}-${resource.name}-${resource.identifier}` - console.log('key', key) if (key) { const savedProgress = getProgress(key); - console.log('savedProgress', savedProgress) if (typeof savedProgress === "number") { playerRef.current?.currentTime(savedProgress); } diff --git a/src/context/GlobalProvider.tsx b/src/context/GlobalProvider.tsx index 572c972..1af4665 100644 --- a/src/context/GlobalProvider.tsx +++ b/src/context/GlobalProvider.tsx @@ -17,6 +17,7 @@ import { useProgressStore } from "../state/video"; import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer"; import { MultiPublishDialog } from "../components/MultiPublish/MultiPublishDialog"; import { useMultiplePublishStore } from "../state/multiplePublish"; +import { useIframe } from "../hooks/useIframe"; // ✅ Define Global Context Type interface GlobalContextType { @@ -52,6 +53,8 @@ export const GlobalProvider = ({ config, toastStyle = {}, }: GlobalProviderProps) => { + + useIframe() // ✅ Call hooks and pass in options dynamically const auth = useAuth(config?.auth || {}); const isPublishing = useMultiplePublishStore((s) => s.isPublishing); @@ -94,6 +97,7 @@ export const GlobalProvider = ({ return ( + {config?.enableGlobalVideoFeature && } {isPublishing && } @@ -108,6 +112,7 @@ export const GlobalProvider = ({ {children} + ); }; diff --git a/src/hooks/useIframe.tsx b/src/hooks/useIframe.tsx new file mode 100644 index 0000000..e548f3c --- /dev/null +++ b/src/hooks/useIframe.tsx @@ -0,0 +1,44 @@ +import { useEffect } from "react"; +import { supportedLanguages } from "../i18n/i18n"; +import i18n from '../i18n/i18n' +type Language = "de" | "en" | "es" | "fr" | "it" | "ja" | "ru" | "zh"; +type Theme = "dark" | "light"; + +interface CustomWindow extends Window { + _qdnTheme: Theme; + _qdnLang: Language; +} +const customWindow = window as unknown as CustomWindow; + + +export const useIframe = () => { + + useEffect(() => { + const languageDefault = customWindow?._qdnLang; + + if (supportedLanguages?.includes(languageDefault)) { + i18n.changeLanguage(languageDefault); + } + + function handleNavigation(event: { + data: { + action: string; + language: Language; + }; + }) { + + if (event.data?.action === "LANGUAGE_CHANGED" && event.data.language) { + if (!supportedLanguages?.includes(event.data.language)) return; + + i18n.changeLanguage(event.data.language); + } + } + + window.addEventListener("message", handleNavigation); + + return () => { + window.removeEventListener("message", handleNavigation); + }; + }, []); + return; +}; diff --git a/src/hooks/useLibTranslation.tsx b/src/hooks/useLibTranslation.tsx new file mode 100644 index 0000000..cd9d3bd --- /dev/null +++ b/src/hooks/useLibTranslation.tsx @@ -0,0 +1,7 @@ +// src/hooks/useLibTranslation.ts +import { useTranslation as useTranslationOriginal } from 'react-i18next'; +import libI18n from '../i18n/i18n' + +export function useLibTranslation(ns?: string | string[]) { + return useTranslationOriginal(ns || 'lib-core', { i18n: libI18n }); +} diff --git a/src/i18n/compiled-i18n.json b/src/i18n/compiled-i18n.json new file mode 100644 index 0000000..516d2b3 --- /dev/null +++ b/src/i18n/compiled-i18n.json @@ -0,0 +1,86 @@ +{ + "resources": { + "en": { + "lib-core": { + "subtitle": { + "subtitles": "Subtitles", + "no_subtitles": "No subtitles", + "load_community_subs": "Load community subtitles", + "off": "Off", + "deleting_subtitle": "Deleting subtitle...", + "deleted": "Deleted subtitle", + "unable_delete": "unable to delete", + "publishing": "Publishing subtitles...", + "published": "Subtitles published", + "unable_publish": "Unable to publish", + "my_subtitles": "My Subtitles", + "new": "New", + "existing": "Existing", + "import_subtitles": "Import subtitles" + }, + "actions": { + "remove": "remove", + "publish": "publish", + "delete": "delete", + "publish_metadata": "publish metadata", + "publish_index": "publish index", + "cancel": "cancel", + "continue": "continue", + "close": "close", + "retry": "retry" + }, + "video": { + "playback_speed": "Playback speed", + "toggle_fullscreen": "Toggle Fullscreen (F)", + "video_speed": "Video Speed. Increase (+ or >), Decrease (- or <)", + "toggle_mute": "Toggle Mute (M), Raise (UP), Lower (DOWN)", + "seek_video": "Seek video in 10% increments (0-9)", + "reload_video": "Reload Video (R)", + "play_pause": "Pause/Play (Spacebar)" + }, + "index": { + "title": "Index Manager", + "create_new_index": "Create new index", + "add_metadata": "Add metadata", + "publishing_metadata": "Publishing metadata...", + "published_metadata": "Successfully published metadata", + "failed_metadata": "Failed to publish metadata", + "example": "Example of how it could look like:", + "metadata_title": "Title", + "metadata_description": "Description", + "metadata_title_placeholder": "Add a title for the link", + "metadata_description_placeholder": "Add a description for the link", + "characters": "characters", + "publishing_index": "Publishing index...", + "published_index": "Successfully published index", + "failed_index": "Failed to publish index", + "recommended_indices": "Recommended Indices", + "add_search_term": "Add search term", + "search_terms": "search terms", + "recommendation_size": "It is recommended to keep your term character count below {{recommendedSize}} characters", + "multiple_title": "Adding multiple indices", + "multiple_description": "Subsequent indices will keep your publish fees lower, but they will have less strength in future search results." + }, + "multi_publish": { + "title": "Publishing Status", + "publish_failed": "Publish Failed", + "file_chunk": "File Chunk", + "file_processing": "File Processing", + "success": "Published successfully", + "attempt_retry": "Publish failed. Attempting retry..." + } + } + }, + "es": { + "lib-core": { + "subtitle": { + "subtitles": "subtitles es" + } + } + } + }, + "supportedLanguages": [ + "en", + "es" + ] +} \ No newline at end of file diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts new file mode 100644 index 0000000..7aeb64d --- /dev/null +++ b/src/i18n/i18n.ts @@ -0,0 +1,27 @@ +import { createInstance } from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import compiled from './compiled-i18n.json'; + +export const supportedLanguages = compiled.supportedLanguages; + +const libI18n = createInstance(); // ✅ this avoids conflict with consumer app + +libI18n + .use(initReactI18next) + .init({ + resources: compiled.resources, + supportedLngs: compiled.supportedLanguages, + fallbackLng: 'en', + lng: typeof navigator !== 'undefined' ? navigator.language : 'en', + defaultNS: 'lib-core', + ns: ['lib-core'], + interpolation: { + escapeValue: false, + }, + react: { + useSuspense: false, + }, + debug: false, + }); + +export default libI18n; diff --git a/src/i18n/locales/en/lib-core.json b/src/i18n/locales/en/lib-core.json new file mode 100644 index 0000000..382051b --- /dev/null +++ b/src/i18n/locales/en/lib-core.json @@ -0,0 +1,70 @@ +{ + "subtitle": { + "subtitles": "Subtitles", + "no_subtitles": "No subtitles", + "load_community_subs": "Load community subtitles", + "off": "Off", + "deleting_subtitle": "Deleting subtitle...", + "deleted": "Deleted subtitle", + "unable_delete": "unable to delete", + "publishing": "Publishing subtitles...", + "published": "Subtitles published", + "unable_publish": "Unable to publish", + "my_subtitles": "My Subtitles", + "new": "New", + "existing": "Existing", + "import_subtitles": "Import subtitles" + }, + "actions": { + "remove": "remove", + "publish": "publish", + "delete": "delete", + "publish_metadata": "publish metadata", + "publish_index": "publish index", + "cancel": "cancel", + "continue": "continue", + "close": "close", + "retry": "retry" + }, + "video": { + "playback_speed": "Playback speed", + "toggle_fullscreen": "Toggle Fullscreen (F)", + "video_speed": "Video Speed. Increase (+ or >), Decrease (- or <)", + "toggle_mute": "Toggle Mute (M), Raise (UP), Lower (DOWN)", + "seek_video": "Seek video in 10% increments (0-9)", + "reload_video": "Reload Video (R)", + "play_pause": "Pause/Play (Spacebar)" + }, + "index": { + "title": "Index Manager", + "create_new_index": "Create new index", + "add_metadata": "Add metadata", + "publishing_metadata": "Publishing metadata...", + "published_metadata": "Successfully published metadata", + "failed_metadata": "Failed to publish metadata", + "example": "Example of how it could look like:", + "metadata_title": "Title", + "metadata_description": "Description", + "metadata_title_placeholder": "Add a title for the link", + "metadata_description_placeholder": "Add a description for the link", + "characters": "characters", + "publishing_index": "Publishing index...", + "published_index": "Successfully published index", + "failed_index": "Failed to publish index", + "recommended_indices": "Recommended Indices", + "add_search_term": "Add search term", + "search_terms": "search terms", + "recommendation_size": "It is recommended to keep your term character count below {{recommendedSize}} characters", + "multiple_title": "Adding multiple indices", + "multiple_description": "Subsequent indices will keep your publish fees lower, but they will have less strength in future search results." + + }, + "multi_publish": { + "title": "Publishing Status", + "publish_failed": "Publish Failed", + "file_chunk": "File Chunk", + "file_processing": "File Processing", + "success": "Published successfully", + "attempt_retry": "Publish failed. Attempting retry..." + } +} diff --git a/src/i18n/locales/es/lib-core.json b/src/i18n/locales/es/lib-core.json new file mode 100644 index 0000000..a58a056 --- /dev/null +++ b/src/i18n/locales/es/lib-core.json @@ -0,0 +1,5 @@ +{ + "subtitle": { + "subtitles": "subtitles es" + } +} \ No newline at end of file diff --git a/src/i18n/processors.ts b/src/i18n/processors.ts new file mode 100644 index 0000000..5baa335 --- /dev/null +++ b/src/i18n/processors.ts @@ -0,0 +1,53 @@ +export const capitalizeAll = { + type: 'postProcessor', + name: 'capitalizeAll', + process: (value: string) => value.toUpperCase(), +}; + +export const capitalizeEachFirstChar = { + type: 'postProcessor', + name: 'capitalizeEachFirstChar', + process: (value: string) => { + if (!value?.trim()) return value; + + const leadingSpaces = value.match(/^\s*/)?.[0] || ''; + const trailingSpaces = value.match(/\s*$/)?.[0] || ''; + + const core = value + .trim() + .split(/\s+/) + .map( + (word) => + word.charAt(0).toLocaleUpperCase() + word.slice(1).toLocaleLowerCase() + ) + .join(' '); + + return leadingSpaces + core + trailingSpaces; + }, +}; +export const capitalizeFirstChar = { + type: 'postProcessor', + name: 'capitalizeFirstChar', + process: (value: string) => value.charAt(0).toUpperCase() + value.slice(1), +}; + +export const capitalizeFirstWord = { + type: 'postProcessor', + name: 'capitalizeFirstWord', + process: (value: string) => { + if (!value?.trim()) return value; + + const trimmed = value.trimStart(); + const firstSpaceIndex = trimmed.indexOf(' '); + + if (firstSpaceIndex === -1) { + return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); + } + + const firstWord = trimmed.slice(0, firstSpaceIndex); + const restOfString = trimmed.slice(firstSpaceIndex); + const trailingSpaces = value.slice(trimmed.length); + + return firstWord.toUpperCase() + restOfString + trailingSpaces; + }, +}; diff --git a/src/state/publishes.ts b/src/state/publishes.ts index de3bd81..a06afc5 100644 --- a/src/state/publishes.ts +++ b/src/state/publishes.ts @@ -196,7 +196,7 @@ startGlobalDownload: ( let isPaused = false const callFunction = async (build?: boolean, isRecalling?: boolean) => { try { - console.log('retryAttempts', retryAttempts, tries) + if ((isCalling || isPaused) && !build) return; isCalling = true; statusMap[resourceId] = getResourceStatus(resourceId);