added translations

This commit is contained in:
2025-07-23 09:09:42 +03:00
parent af68acff64
commit 545f7bd7c3
20 changed files with 574 additions and 164 deletions

83
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

36
scripts/generate_locales.js Executable file
View File

@@ -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.');
})();

View File

@@ -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) => {
},
}}
>
<DialogTitle>Index manager</DialogTitle>
<DialogTitle>{t("index.title")}</DialogTitle>
<IconButton
aria-label="close"
onClick={handleClose}
@@ -196,6 +200,8 @@ const EntryMode = ({
username,
hasMetadata,
}: PropsEntryMode) => {
const { t } = useLibTranslation();
return (
<>
<DialogContent>
@@ -222,7 +228,7 @@ const EntryMode = ({
width: "100%",
}}
>
<Typography>Create new index</Typography>
<Typography>{t("index.create_new_index")}</Typography>
</Box>
</ButtonBase>
{/* <ButtonBase
@@ -260,7 +266,7 @@ const EntryMode = ({
alignItems: "center",
}}
>
<Typography>Add metadata</Typography>
<Typography>{t("index.add_metadata")}</Typography>
{hasMetadata && <CheckCircleIcon color="success" />}
</Box>
</ButtonBase>
@@ -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 = ({
<IconButton disabled={mode === 1} onClick={() => setMode(1)}>
<ArrowBackIosIcon />
</IconButton>
<Typography>Example of how it could look like:</Typography>
<Typography>{t("index.example")}</Typography>
<Card sx={{
width: '100%',
padding: '5px'
@@ -437,12 +445,12 @@ const AddMetadata = ({
</Box>
</Card>
<Box>
<Typography>Title</Typography>
<Typography>{t("index.metadata.title")}</Typography>
<TextField
value={title}
onChange={(e) => 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")}`}
</Typography>
}
/>
</Box>
<Box>
<Typography>Description</Typography>
<Typography>{t("index.metadata.description")}</Typography>
<TextField
fullWidth
value={description}
onChange={(e) => 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")}`}
</Typography>
}
/>
@@ -486,7 +494,7 @@ const AddMetadata = ({
disabled={disableButton}
variant="contained"
>
Publish metadata
{t("actions.publish_metadata")}
</Button>
</DialogActions>
</>
@@ -507,6 +515,8 @@ const CreateIndex = ({
category,
rootName,
}: PropsCreateIndex) => {
const { t } = useLibTranslation();
const [terms, setTerms] = useState<string[]>([]);
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 = ({
</IconButton>
{recommendedIndices?.length > 0 && (
<>
<Typography>Recommended Indices</Typography>
<Typography>{t("index.recommended_indices")}</Typography>
<RadioGroup
aria-labelledby="demo-controlled-radio-buttons-group"
name="controlled-radio-buttons-group"
@@ -673,7 +683,7 @@ const CreateIndex = ({
value={recommendedSelection}
onChange={handleChange}
>
<FormControlLabel value="" control={<Radio />} label="Add search term" />
<FormControlLabel value="" control={<Radio />} label={t("index.add_search_term")} />
</RadioGroup>
<Spacer height="10px" />
{!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
})}
</Typography>
</Box>
</Box>
@@ -716,7 +728,7 @@ const CreateIndex = ({
disabled={disableButton}
variant="contained"
>
Publish index
{t("actions.publish_index")}
</Button>
</DialogActions>
<Dialog
@@ -733,18 +745,17 @@ const CreateIndex = ({
}}
>
<DialogTitle id="alert-dialog-title">
Adding multiple indices
{t("index.multiple_title")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Subsequent indices will keep your publish fees lower, but they will
have less strength in future search results.
{t("index.multiple_description")}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onCancel}>Cancel</Button>
<Button variant="contained" onClick={onCancel}>{t("actions.cancel")}</Button>
<Button variant="contained" onClick={onOk}>
Continue
{t("actions.continue")}
</Button>
</DialogActions>
</Dialog>
@@ -762,6 +773,8 @@ const YourIndices = ({
category,
rootName,
}: PropsCreateIndex) => {
const { t } = useLibTranslation();
const [terms, setTerms] = useState<string[]>([]);
const publish = usePublish();
const [size, setSize] = useState(0);

View File

@@ -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
>
<DialogTitle>Publishing Status</DialogTitle>
<DialogTitle>{t("multi_publish.title")}</DialogTitle>
<DialogContent>
{publishError && (
<Stack spacing={3}>
@@ -215,7 +219,7 @@ export const MultiPublishDialogComponent = () => {
gap: '10px'
}}>
<ErrorIcon color="error" />
<Typography variant="body2">Publish failed</Typography>
<Typography variant="body2">{t("multi_publish.publish_failed")}</Typography>
</Box>
)}
@@ -229,7 +233,7 @@ export const MultiPublishDialogComponent = () => {
reject(new Error('Canceled Publish'));
reset();
}}>
Close
{t("actions.close")}
</Button>
{failedResources?.length > 0 && (
<Button
@@ -238,7 +242,7 @@ export const MultiPublishDialogComponent = () => {
variant="contained"
onClick={publishMultipleResources}
>
Retry
{t("actions.retry")}
</Button>
)}
</Box>
@@ -306,14 +310,14 @@ useEffect(() => {
<Box mt={2}>
<Typography variant="body2" gutterBottom>
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)}%)
</Typography>
<LinearProgress variant="determinate" value={chunkPercent} />
</Box>
<Box mt={2}>
<Typography variant="body2" gutterBottom>
File Processing ({publishStatus?.processed ? 100 : processingStart ? processingPercent.toFixed(0) : '0'}%)
{t("multi_publish.file_processing")} ({publishStatus?.processed ? 100 : processingStart ? processingPercent.toFixed(0) : '0'}%)
</Typography>
<LinearProgress variant="determinate" value={publishStatus?.processed ? 100 : processingPercent} />
</Box>
@@ -321,14 +325,14 @@ useEffect(() => {
{publishStatus?.processed && (
<Box mt={2} display="flex" gap={1} alignItems="center">
<CheckCircleIcon color="success" />
<Typography variant="body2">Published successfully</Typography>
<Typography variant="body2">{t("multi_publish.success")}</Typography>
</Box>
)}
{publishStatus?.retry && !publishStatus?.error && !publishStatus?.processed && (
<Box mt={2} display="flex" gap={1} alignItems="center">
<ErrorIcon color="error" />
<Typography variant="body2">Publish failed. Attempting retry...</Typography>
<Typography variant="body2">{t("multi_publish.attempt_retry")}</Typography>
</Box>
)}
@@ -336,7 +340,7 @@ useEffect(() => {
<Box mt={2} display="flex" gap={1} alignItems="center">
<ErrorIcon color="error" />
<Typography variant="body2">
Publish failed. {publishStatus?.error?.reason || 'Unknown error'}
{t("multi_publish.publish_failed")} - {publishStatus?.error?.reason || 'Unknown error'}
</Typography>
</Box>
)}

View File

@@ -373,7 +373,6 @@ removeFromList(listName, displayLimit)
getResourceMoreList(displayLimit)
}, [getResourceMoreList])
console.log('listToDisplay', listToDisplay)
return (
<div ref={elementRef} style={{

View File

@@ -292,7 +292,6 @@ removeFromList(listName, displayLimit)
getResourceMoreList(displayLimit)
}, [getResourceMoreList])
console.log('isLoading', isLoading, listToDisplay?.length)
return (

View File

@@ -61,6 +61,7 @@ import {
showSuccess,
} from "../../utils/toast";
import { RequestQueueWithPromise } from "../../utils/queue";
import { useLibTranslation } from "../../hooks/useLibTranslation";
export const requestQueueGetStatus = new RequestQueueWithPromise(1);
@@ -114,6 +115,8 @@ const SubtitleManagerComponent = ({
isFromDrawer = false,
exitFullscreen,
}: SubtitleManagerProps) => {
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")}
</Typography>
</ButtonBase>
<ButtonBase
@@ -365,7 +368,7 @@ const SubtitleManagerComponent = ({
marginTop: "20px",
}}
>
No subtitles
{t("subtitle.no_subtitles")}
</Typography>
)}
@@ -393,7 +396,7 @@ const SubtitleManagerComponent = ({
disabled={showAll}
onClick={() => setShowAll(true)}
>
Load community subs
{t("subtitle.load_community_subs")}
</Button>
</Box>
</Box>
@@ -424,6 +427,8 @@ const PublisherSubtitles = ({
onBack,
currentSubTrack,
}: PublisherSubtitlesProps) => {
const { t } = useLibTranslation();
return (
<>
<ButtonBase
@@ -439,7 +444,7 @@ const PublisherSubtitles = ({
justifyContent: "space-between",
}}
>
<Typography>Off</Typography>
<Typography>{t("subtitle.off")}</Typography>
{!currentSubTrack ? <CheckIcon /> : <ArrowForwardIosIcon />}
</ButtonBase>
@@ -470,6 +475,8 @@ const PublishSubtitles = ({
setIsOpen,
mySubtitles,
}: PublishSubtitlesProps) => {
const { t } = useLibTranslation();
const [language, setLanguage] = useState<null | string>(null);
const [subtitles, setSubtitles] = useState<Subtitle[]>([]);
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 = ({
},
}}
>
<DialogTitle>My Subtitles</DialogTitle>
<DialogTitle>{t("subtitle.my_subtitles")}</DialogTitle>
<IconButton
aria-label="close"
onClick={handleClose}
@@ -626,8 +637,8 @@ const PublishSubtitles = ({
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab label="New" {...a11yProps(0)} />
<Tab label="Existing" {...a11yProps(1)} />
<Tab label={t("subtitle.new")} {...a11yProps(0)} />
<Tab label={t("subtitle.existing")} {...a11yProps(1)} />
</Tabs>
</Box>
</Box>
@@ -651,7 +662,7 @@ const PublishSubtitles = ({
variant="contained"
>
<input {...getInputProps()} />
Import subtitles
{t("subtitle.import_subtitles")}
</Button>
</Box>
{subtitles?.map((sub, i) => {
@@ -697,7 +708,7 @@ const PublishSubtitles = ({
size="small"
color="secondary"
>
remove
{t("actions.remove")}
</Button>
</Box>
</Card>
@@ -739,7 +750,7 @@ const PublishSubtitles = ({
disabled={disableButton}
variant="contained"
>
Publish
{t("actions.publish")}
</Button>
)}
</DialogActions>
@@ -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 (
<Card
@@ -887,7 +900,7 @@ const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => {
size="small"
color="secondary"
>
delete
{t("actions.delete")}
</Button>
</Box>
</Card>

View File

@@ -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 (
<CustomFontTooltip title="Pause/Play (Spacebar)" placement="bottom" arrow>
<CustomFontTooltip title={t("video.play_pause")} placement="bottom" arrow>
<IconButton
sx={{
color: "white",
@@ -50,8 +49,10 @@ export const PlayButton = ({ togglePlay, isPlaying, isScreenSmall }: any) => {
};
export const ReloadButton = ({ reloadVideo, isScreenSmall }: any) => {
const { t } = useLibTranslation();
return (
<CustomFontTooltip title="Reload Video (R)" placement="bottom" arrow>
<CustomFontTooltip title={t("video.reload_video")} placement="bottom" arrow>
<IconButton
sx={{
color: "white",
@@ -72,7 +73,7 @@ export const ProgressSlider = ({
playerRef,
resetHideTimeout,
isVideoPlayerSmall,
isOnTimeline
isOnTimeline,
}: any) => {
const sliderRef = useRef(null);
const [isDragging, setIsDragging] = useState(false);
@@ -117,26 +118,25 @@ export const ProgressSlider = ({
const debounceTimeoutRef = useRef<any>(null);
const previousBlobUrlRef = useRef<string | null>(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 (
<Box
@@ -183,17 +183,17 @@ const handleMouseMove = (e: React.MouseEvent) => {
padding: isVideoPlayerSmall ? "0px" : "0px 10px",
}}
>
<Box
ref={hoverAnchorRef}
sx={{
position: "absolute",
top: 0,
width: "1px",
height: "1px",
pointerEvents: "none",
transform: "translateX(-50%)", // center popper on the anchor
}}
/>
<Box
ref={hoverAnchorRef}
sx={{
position: "absolute",
top: 0,
width: "1px",
height: "1px",
pointerEvents: "none",
transform: "translateX(-50%)", // center popper on the anchor
}}
/>
<Slider
ref={sliderRef}
onMouseMove={handleMouseMove}
@@ -268,9 +268,11 @@ const handleMouseMove = (e: React.MouseEvent) => {
};
export const VideoTime = ({ progress, isScreenSmall, duration }: any) => {
const { t } = useLibTranslation();
return (
<CustomFontTooltip
title="Seek video in 10% increments (0-9)"
title={t("video.seek_video")}
placement="bottom"
arrow
disableHoverListener={isScreenSmall}
@@ -295,12 +297,10 @@ export const VideoTime = ({ progress, isScreenSmall, duration }: any) => {
};
const VolumeButton = ({ isMuted, toggleMute }: any) => {
const { t } = useLibTranslation();
return (
<CustomFontTooltip
title="Toggle Mute (M), Raise (UP), Lower (DOWN)"
placement="bottom"
arrow
>
<CustomFontTooltip title={t("video.toggle_mute")} placement="bottom" arrow>
<IconButton
sx={{
color: "white",
@@ -373,6 +373,8 @@ export const PlaybackRate = ({
onSelect,
openPlaybackMenu,
}: any) => {
const { t } = useLibTranslation();
const [isOpen, setIsOpen] = useState(false);
const btnRef = useRef(null);
const theme = useTheme();
@@ -383,7 +385,7 @@ export const PlaybackRate = ({
return (
<>
<CustomFontTooltip
title="Video Speed. Increase (+ or >), Decrease (- or <)"
title={t("video.video_speed")}
placement="bottom"
arrow
>
@@ -403,55 +405,15 @@ export const PlaybackRate = ({
);
};
export const ObjectFitButton = ({ toggleObjectFit, isScreenSmall }: any) => {
return (
<CustomFontTooltip title="Toggle Aspect Ratio (O)" placement="bottom" arrow>
<IconButton
sx={{
color: "white",
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
}}
onClick={() => toggleObjectFit()}
>
<AspectRatioIcon />
</IconButton>
</CustomFontTooltip>
);
};
export const PictureInPictureButton = ({
isFullscreen,
toggleRef,
togglePictureInPicture,
isScreenSmall,
}: any) => {
return (
<>
{!isFullscreen && (
<CustomFontTooltip
title="Picture in Picture (P)"
placement="bottom"
arrow
>
<IconButton
sx={{
color: "white",
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
}}
ref={toggleRef}
onClick={togglePictureInPicture}
>
<PictureInPicture />
</IconButton>
</CustomFontTooltip>
)}
</>
);
};
export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => {
const { t } = useLibTranslation();
return (
<CustomFontTooltip title="Toggle Fullscreen (F)" placement="bottom" arrow>
<CustomFontTooltip
title={t("video.toggle_fullscreen")}
placement="bottom"
arrow
>
<IconButton
sx={{
color: "white",
@@ -479,6 +441,8 @@ export const PlayBackMenu = ({
playbackRate,
isFromDrawer,
}: PlayBackMenuProps) => {
const { t } = useLibTranslation();
const theme = useTheme();
const ref = useRef<any>(null);
@@ -539,7 +503,7 @@ export const PlayBackMenu = ({
fontSize: "0.85rem",
}}
>
Playback speed
{t("video.playback_speed")}
</Typography>
</ButtonBase>
</Box>

View File

@@ -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}
/>
{/* <ObjectFitButton /> */}
<CustomFontTooltip title="Subtitles" placement="bottom" arrow>
<CustomFontTooltip
title={t("subtitle.subtitles")}
placement="bottom"
arrow
>
<IconButton
ref={subtitleBtnRef}
onClick={openSubtitleManager}
@@ -158,7 +165,7 @@ export const VideoControlsBar = ({
/>
</IconButton>
</CustomFontTooltip>
{/* <PictureInPictureButton togglePictureInPicture={togglePictureInPicture} /> */}
<FullscreenButton toggleFullscreen={toggleFullscreen} />
</Box>
</Box>

View File

@@ -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);
}

View File

@@ -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 (
<GlobalContext.Provider value={contextValue}>
{config?.enableGlobalVideoFeature && <GlobalPipPlayer />}
{isPublishing && <MultiPublishDialog />}
@@ -108,6 +112,7 @@ export const GlobalProvider = ({
<IndexManager username={auth?.name} />
{children}
</GlobalContext.Provider>
);
};

44
src/hooks/useIframe.tsx Normal file
View File

@@ -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;
};

View File

@@ -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 });
}

View File

@@ -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"
]
}

27
src/i18n/i18n.ts Normal file
View File

@@ -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;

View File

@@ -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..."
}
}

View File

@@ -0,0 +1,5 @@
{
"subtitle": {
"subtitles": "subtitles es"
}
}

53
src/i18n/processors.ts Normal file
View File

@@ -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;
},
};

View File

@@ -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);