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", "name": "qapp-core",
"version": "1.0.37", "version": "1.0.46",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "qapp-core", "name": "qapp-core",
"version": "1.0.37", "version": "1.0.46",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/react-virtual": "^3.13.2", "@tanstack/react-virtual": "^3.13.2",
@@ -16,10 +16,12 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dexie": "^4.0.11", "dexie": "^4.0.11",
"dompurify": "^3.2.4", "dompurify": "^3.2.4",
"i18next": "^25.3.2",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"iso-639-1": "^3.1.5", "iso-639-1": "^3.1.5",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-i18next": "^15.6.1",
"react-idle-timer": "^5.7.2", "react-idle-timer": "^5.7.2",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"react-rnd": "^10.5.2", "react-rnd": "^10.5.2",
@@ -2395,6 +2397,46 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/idb-keyval": {
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
@@ -3217,6 +3259,32 @@
"react-dom": ">=16" "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": { "node_modules/react-idle-timer": {
"version": "5.7.2", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz", "resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz",
@@ -3947,7 +4015,7 @@
"version": "5.8.3", "version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -4024,6 +4092,15 @@
"global": "^4.3.1" "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": { "node_modules/webidl-conversions": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",

View File

@@ -18,7 +18,8 @@
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "tsup", "generate-i18n": "./scripts/generate_locales.js",
"build": "npm run generate-i18n && tsup",
"prepare": "npm run build", "prepare": "npm run build",
"clean": "rm -rf dist" "clean": "rm -rf dist"
}, },
@@ -30,10 +31,12 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dexie": "^4.0.11", "dexie": "^4.0.11",
"dompurify": "^3.2.4", "dompurify": "^3.2.4",
"i18next": "^25.3.2",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"iso-639-1": "^3.1.5", "iso-639-1": "^3.1.5",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-i18next": "^15.6.1",
"react-idle-timer": "^5.7.2", "react-idle-timer": "^5.7.2",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"react-rnd": "^10.5.2", "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 { createAvatarLink } from "../../utils/qortal";
import { extractComponents } from "../../utils/text"; import { extractComponents } from "../../utils/text";
import NavigateNextIcon from "@mui/icons-material/NavigateNext"; import NavigateNextIcon from "@mui/icons-material/NavigateNext";
import { useLibTranslation } from "../../hooks/useLibTranslation";
import { t } from "i18next";
const uid = new ShortUniqueId({ length: 10, dictionary: "alphanum" }); 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(); const cleanString = (str: string) => str.replace(/\s{2,}/g, ' ').trim().toLocaleLowerCase();
export const IndexManager = ({ username }: PropsIndexManager) => { export const IndexManager = ({ username }: PropsIndexManager) => {
const { t } = useLibTranslation();
const open = useIndexStore((state) => state.open); const open = useIndexStore((state) => state.open);
const setOpen = useIndexStore((state) => state.setOpen); const setOpen = useIndexStore((state) => state.setOpen);
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
@@ -123,7 +127,7 @@ export const IndexManager = ({ username }: PropsIndexManager) => {
}, },
}} }}
> >
<DialogTitle>Index manager</DialogTitle> <DialogTitle>{t("index.title")}</DialogTitle>
<IconButton <IconButton
aria-label="close" aria-label="close"
onClick={handleClose} onClick={handleClose}
@@ -196,6 +200,8 @@ const EntryMode = ({
username, username,
hasMetadata, hasMetadata,
}: PropsEntryMode) => { }: PropsEntryMode) => {
const { t } = useLibTranslation();
return ( return (
<> <>
<DialogContent> <DialogContent>
@@ -222,7 +228,7 @@ const EntryMode = ({
width: "100%", width: "100%",
}} }}
> >
<Typography>Create new index</Typography> <Typography>{t("index.create_new_index")}</Typography>
</Box> </Box>
</ButtonBase> </ButtonBase>
{/* <ButtonBase {/* <ButtonBase
@@ -260,7 +266,7 @@ const EntryMode = ({
alignItems: "center", alignItems: "center",
}} }}
> >
<Typography>Add metadata</Typography> <Typography>{t("index.add_metadata")}</Typography>
{hasMetadata && <CheckCircleIcon color="success" />} {hasMetadata && <CheckCircleIcon color="success" />}
</Box> </Box>
</ButtonBase> </ButtonBase>
@@ -288,12 +294,14 @@ const AddMetadata = ({
setDescription, setDescription,
setTitle, setTitle,
}: PropsAddMetadata) => { }: PropsAddMetadata) => {
const { t } = useLibTranslation();
const publish = usePublish(); const publish = usePublish();
const disableButton = !title.trim() || !description.trim() || !name || !link; const disableButton = !title.trim() || !description.trim() || !name || !link;
const createMetadata = async () => { const createMetadata = async () => {
const loadId = showLoading("Publishing metadata..."); const loadId = showLoading(t("index.publishing_metadata"));
try { try {
const identifierWithoutHash = name + link; const identifierWithoutHash = name + link;
const identifier = await hashWordWithoutPublicSalt( const identifier = await hashWordWithoutPublicSalt(
@@ -313,7 +321,7 @@ const AddMetadata = ({
}); });
if (res?.signature) { if (res?.signature) {
showSuccess("Successfully published metadata"); showSuccess(t("index.published_metadata"));
publish.updatePublish( publish.updatePublish(
{ {
identifier, identifier,
@@ -325,7 +333,7 @@ const AddMetadata = ({
} }
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : "Failed to publish metadata"; error instanceof Error ? error.message : t("index.failed_metadata");
showError(message); showError(message);
} finally { } finally {
dismissToast(loadId); dismissToast(loadId);
@@ -350,7 +358,7 @@ const AddMetadata = ({
<IconButton disabled={mode === 1} onClick={() => setMode(1)}> <IconButton disabled={mode === 1} onClick={() => setMode(1)}>
<ArrowBackIosIcon /> <ArrowBackIosIcon />
</IconButton> </IconButton>
<Typography>Example of how it could look like:</Typography> <Typography>{t("index.example")}</Typography>
<Card sx={{ <Card sx={{
width: '100%', width: '100%',
padding: '5px' padding: '5px'
@@ -437,12 +445,12 @@ const AddMetadata = ({
</Box> </Box>
</Card> </Card>
<Box> <Box>
<Typography>Title</Typography> <Typography>{t("index.metadata.title")}</Typography>
<TextField <TextField
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
size="small" size="small"
placeholder="Add a title for the link" placeholder={t("index.metadata_title_placeholder")}
slotProps={{ slotProps={{
htmlInput: { maxLength: 50 }, htmlInput: { maxLength: 50 },
}} }}
@@ -452,19 +460,19 @@ const AddMetadata = ({
variant="caption" variant="caption"
color={title.length >= 50 ? "error" : "text.secondary"} color={title.length >= 50 ? "error" : "text.secondary"}
> >
{title.length}/{50} characters {title.length}/{50} {` ${t("index.characters")}`}
</Typography> </Typography>
} }
/> />
</Box> </Box>
<Box> <Box>
<Typography>Description</Typography> <Typography>{t("index.metadata.description")}</Typography>
<TextField <TextField
fullWidth fullWidth
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
size="small" size="small"
placeholder="Add a description for the link" placeholder={t("index.metadata_description_placeholder")}
slotProps={{ slotProps={{
htmlInput: { maxLength: 120 }, htmlInput: { maxLength: 120 },
}} }}
@@ -473,7 +481,7 @@ const AddMetadata = ({
variant="caption" variant="caption"
color={description.length >= 120 ? "error" : "text.secondary"} color={description.length >= 120 ? "error" : "text.secondary"}
> >
{description.length}/{120} characters {description.length}/{120} {` ${t("index.characters")}`}
</Typography> </Typography>
} }
/> />
@@ -486,7 +494,7 @@ const AddMetadata = ({
disabled={disableButton} disabled={disableButton}
variant="contained" variant="contained"
> >
Publish metadata {t("actions.publish_metadata")}
</Button> </Button>
</DialogActions> </DialogActions>
</> </>
@@ -507,6 +515,8 @@ const CreateIndex = ({
category, category,
rootName, rootName,
}: PropsCreateIndex) => { }: PropsCreateIndex) => {
const { t } = useLibTranslation();
const [terms, setTerms] = useState<string[]>([]); const [terms, setTerms] = useState<string[]>([]);
const publish = usePublish(); const publish = usePublish();
const [size, setSize] = useState(0); const [size, setSize] = useState(0);
@@ -588,7 +598,7 @@ const CreateIndex = ({
const disableButton = (terms.length === 0 && !recommendedSelection) || !name || !link; const disableButton = (terms.length === 0 && !recommendedSelection) || !name || !link;
const createIndex = async () => { const createIndex = async () => {
const loadId = showLoading("Publishing index..."); const loadId = showLoading(t("index.publishing_index"));
try { try {
const hashedRootName = await hashWordWithoutPublicSalt(rootName, 20); const hashedRootName = await hashWordWithoutPublicSalt(rootName, 20);
const hashedLink = await hashWordWithoutPublicSalt(link, 20); const hashedLink = await hashWordWithoutPublicSalt(link, 20);
@@ -603,7 +613,7 @@ const CreateIndex = ({
}); });
if (res?.signature) { if (res?.signature) {
showSuccess("Successfully published index"); showSuccess(t("index.published_index"));
publish.updatePublish( publish.updatePublish(
{ {
identifier, identifier,
@@ -616,7 +626,7 @@ const CreateIndex = ({
} }
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : "Failed to publish index"; error instanceof Error ? error.message : t("index.failed_index");
showError(message); showError(message);
} finally { } finally {
dismissToast(loadId); dismissToast(loadId);
@@ -645,7 +655,7 @@ const CreateIndex = ({
</IconButton> </IconButton>
{recommendedIndices?.length > 0 && ( {recommendedIndices?.length > 0 && (
<> <>
<Typography>Recommended Indices</Typography> <Typography>{t("index.recommended_indices")}</Typography>
<RadioGroup <RadioGroup
aria-labelledby="demo-controlled-radio-buttons-group" aria-labelledby="demo-controlled-radio-buttons-group"
name="controlled-radio-buttons-group" name="controlled-radio-buttons-group"
@@ -673,7 +683,7 @@ const CreateIndex = ({
value={recommendedSelection} value={recommendedSelection}
onChange={handleChange} onChange={handleChange}
> >
<FormControlLabel value="" control={<Radio />} label="Add search term" /> <FormControlLabel value="" control={<Radio />} label={t("index.add_search_term")} />
</RadioGroup> </RadioGroup>
<Spacer height="10px" /> <Spacer height="10px" />
{!recommendedSelection && ( {!recommendedSelection && (
@@ -681,7 +691,7 @@ const CreateIndex = ({
maxLength={17} maxLength={17}
items={terms} items={terms}
onlyStrings onlyStrings
label="search terms" label={t("index.search_terms")}
setItems={async (termsNew: string[]) => { setItems={async (termsNew: string[]) => {
try { try {
if (terms?.length === 1 && termsNew?.length === 2) { if (terms?.length === 1 && termsNew?.length === 2) {
@@ -704,8 +714,10 @@ const CreateIndex = ({
shouldRecommendMax && fullSize > 230 ? "visible" : "hidden", shouldRecommendMax && fullSize > 230 ? "visible" : "hidden",
}} }}
> >
It is recommended to keep your term character count below{" "}
{recommendedSize} characters {t("index.recommendation_size", {
recommendedSize
})}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -716,7 +728,7 @@ const CreateIndex = ({
disabled={disableButton} disabled={disableButton}
variant="contained" variant="contained"
> >
Publish index {t("actions.publish_index")}
</Button> </Button>
</DialogActions> </DialogActions>
<Dialog <Dialog
@@ -733,18 +745,17 @@ const CreateIndex = ({
}} }}
> >
<DialogTitle id="alert-dialog-title"> <DialogTitle id="alert-dialog-title">
Adding multiple indices {t("index.multiple_title")}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">
Subsequent indices will keep your publish fees lower, but they will {t("index.multiple_description")}
have less strength in future search results.
</DialogContentText> </DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button variant="contained" onClick={onCancel}>Cancel</Button> <Button variant="contained" onClick={onCancel}>{t("actions.cancel")}</Button>
<Button variant="contained" onClick={onOk}> <Button variant="contained" onClick={onOk}>
Continue {t("actions.continue")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@@ -762,6 +773,8 @@ const YourIndices = ({
category, category,
rootName, rootName,
}: PropsCreateIndex) => { }: PropsCreateIndex) => {
const { t } = useLibTranslation();
const [terms, setTerms] = useState<string[]>([]); const [terms, setTerms] = useState<string[]>([]);
const publish = usePublish(); const publish = usePublish();
const [size, setSize] = useState(0); const [size, setSize] = useState(0);

View File

@@ -22,6 +22,8 @@ import { ResourceToPublish } from "../../types/qortalRequests/types";
import { QortalGetMetadata } from "../../types/interfaces/resources"; import { QortalGetMetadata } from "../../types/interfaces/resources";
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { useLibTranslation } from "../../hooks/useLibTranslation";
import { t } from "i18next";
export interface MultiplePublishError { export interface MultiplePublishError {
error: { error: {
@@ -31,6 +33,8 @@ export interface MultiplePublishError {
export const MultiPublishDialogComponent = () => { export const MultiPublishDialogComponent = () => {
const { t } = useLibTranslation();
const { const {
resources, resources,
isPublishing, isPublishing,
@@ -177,7 +181,7 @@ export const MultiPublishDialogComponent = () => {
disableAutoFocus disableAutoFocus
disableRestoreFocus disableRestoreFocus
> >
<DialogTitle>Publishing Status</DialogTitle> <DialogTitle>{t("multi_publish.title")}</DialogTitle>
<DialogContent> <DialogContent>
{publishError && ( {publishError && (
<Stack spacing={3}> <Stack spacing={3}>
@@ -215,7 +219,7 @@ export const MultiPublishDialogComponent = () => {
gap: '10px' gap: '10px'
}}> }}>
<ErrorIcon color="error" /> <ErrorIcon color="error" />
<Typography variant="body2">Publish failed</Typography> <Typography variant="body2">{t("multi_publish.publish_failed")}</Typography>
</Box> </Box>
)} )}
@@ -229,7 +233,7 @@ export const MultiPublishDialogComponent = () => {
reject(new Error('Canceled Publish')); reject(new Error('Canceled Publish'));
reset(); reset();
}}> }}>
Close {t("actions.close")}
</Button> </Button>
{failedResources?.length > 0 && ( {failedResources?.length > 0 && (
<Button <Button
@@ -238,7 +242,7 @@ export const MultiPublishDialogComponent = () => {
variant="contained" variant="contained"
onClick={publishMultipleResources} onClick={publishMultipleResources}
> >
Retry {t("actions.retry")}
</Button> </Button>
)} )}
</Box> </Box>
@@ -306,14 +310,14 @@ useEffect(() => {
<Box mt={2}> <Box mt={2}>
<Typography variant="body2" gutterBottom> <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> </Typography>
<LinearProgress variant="determinate" value={chunkPercent} /> <LinearProgress variant="determinate" value={chunkPercent} />
</Box> </Box>
<Box mt={2}> <Box mt={2}>
<Typography variant="body2" gutterBottom> <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> </Typography>
<LinearProgress variant="determinate" value={publishStatus?.processed ? 100 : processingPercent} /> <LinearProgress variant="determinate" value={publishStatus?.processed ? 100 : processingPercent} />
</Box> </Box>
@@ -321,14 +325,14 @@ useEffect(() => {
{publishStatus?.processed && ( {publishStatus?.processed && (
<Box mt={2} display="flex" gap={1} alignItems="center"> <Box mt={2} display="flex" gap={1} alignItems="center">
<CheckCircleIcon color="success" /> <CheckCircleIcon color="success" />
<Typography variant="body2">Published successfully</Typography> <Typography variant="body2">{t("multi_publish.success")}</Typography>
</Box> </Box>
)} )}
{publishStatus?.retry && !publishStatus?.error && !publishStatus?.processed && ( {publishStatus?.retry && !publishStatus?.error && !publishStatus?.processed && (
<Box mt={2} display="flex" gap={1} alignItems="center"> <Box mt={2} display="flex" gap={1} alignItems="center">
<ErrorIcon color="error" /> <ErrorIcon color="error" />
<Typography variant="body2">Publish failed. Attempting retry...</Typography> <Typography variant="body2">{t("multi_publish.attempt_retry")}</Typography>
</Box> </Box>
)} )}
@@ -336,7 +340,7 @@ useEffect(() => {
<Box mt={2} display="flex" gap={1} alignItems="center"> <Box mt={2} display="flex" gap={1} alignItems="center">
<ErrorIcon color="error" /> <ErrorIcon color="error" />
<Typography variant="body2"> <Typography variant="body2">
Publish failed. {publishStatus?.error?.reason || 'Unknown error'} {t("multi_publish.publish_failed")} - {publishStatus?.error?.reason || 'Unknown error'}
</Typography> </Typography>
</Box> </Box>
)} )}

View File

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

View File

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

View File

@@ -61,6 +61,7 @@ import {
showSuccess, showSuccess,
} from "../../utils/toast"; } from "../../utils/toast";
import { RequestQueueWithPromise } from "../../utils/queue"; import { RequestQueueWithPromise } from "../../utils/queue";
import { useLibTranslation } from "../../hooks/useLibTranslation";
export const requestQueueGetStatus = new RequestQueueWithPromise(1); export const requestQueueGetStatus = new RequestQueueWithPromise(1);
@@ -114,6 +115,8 @@ const SubtitleManagerComponent = ({
isFromDrawer = false, isFromDrawer = false,
exitFullscreen, exitFullscreen,
}: SubtitleManagerProps) => { }: SubtitleManagerProps) => {
const { t } = useLibTranslation();
const [mode, setMode] = useState(1); const [mode, setMode] = useState(1);
const [isOpenPublish, setIsOpenPublish] = useState(false); const [isOpenPublish, setIsOpenPublish] = useState(false);
const { lists, identifierOperations, auth } = useGlobal(); const { lists, identifierOperations, auth } = useGlobal();
@@ -308,7 +311,7 @@ const SubtitleManagerComponent = ({
fontSize: "0.85rem", fontSize: "0.85rem",
}} }}
> >
Subtitles {t("subtitle.subtitles")}
</Typography> </Typography>
</ButtonBase> </ButtonBase>
<ButtonBase <ButtonBase
@@ -365,7 +368,7 @@ const SubtitleManagerComponent = ({
marginTop: "20px", marginTop: "20px",
}} }}
> >
No subtitles {t("subtitle.no_subtitles")}
</Typography> </Typography>
)} )}
@@ -393,7 +396,7 @@ const SubtitleManagerComponent = ({
disabled={showAll} disabled={showAll}
onClick={() => setShowAll(true)} onClick={() => setShowAll(true)}
> >
Load community subs {t("subtitle.load_community_subs")}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -424,6 +427,8 @@ const PublisherSubtitles = ({
onBack, onBack,
currentSubTrack, currentSubTrack,
}: PublisherSubtitlesProps) => { }: PublisherSubtitlesProps) => {
const { t } = useLibTranslation();
return ( return (
<> <>
<ButtonBase <ButtonBase
@@ -439,7 +444,7 @@ const PublisherSubtitles = ({
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
<Typography>Off</Typography> <Typography>{t("subtitle.off")}</Typography>
{!currentSubTrack ? <CheckIcon /> : <ArrowForwardIosIcon />} {!currentSubTrack ? <CheckIcon /> : <ArrowForwardIosIcon />}
</ButtonBase> </ButtonBase>
@@ -470,6 +475,8 @@ const PublishSubtitles = ({
setIsOpen, setIsOpen,
mySubtitles, mySubtitles,
}: PublishSubtitlesProps) => { }: PublishSubtitlesProps) => {
const { t } = useLibTranslation();
const [language, setLanguage] = useState<null | string>(null); const [language, setLanguage] = useState<null | string>(null);
const [subtitles, setSubtitles] = useState<Subtitle[]>([]); const [subtitles, setSubtitles] = useState<Subtitle[]>([]);
const [isPublishing, setIsPublishing] = useState(false); const [isPublishing, setIsPublishing] = useState(false);
@@ -488,7 +495,7 @@ const PublishSubtitles = ({
}; };
newSubtitles.push(newSubtitle); newSubtitles.push(newSubtitle);
} catch (error) { } catch (error) {
console.error("Failed to parse audio file:", error); console.error("Failed to convert to base64:", error);
} }
} }
setSubtitles((prev) => [...newSubtitles, ...prev]); setSubtitles((prev) => [...newSubtitles, ...prev]);
@@ -538,11 +545,13 @@ const PublishSubtitles = ({
let loadId; let loadId;
try { try {
setIsPublishing(true); setIsPublishing(true);
loadId = showLoading("Deleting subtitle..."); loadId = showLoading(t("subtitle.deleting_subtitle"));
await lists.deleteResource([sub]); await lists.deleteResource([sub]);
showSuccess("Deleted subtitle"); showSuccess(t("subtitle.deleted"));
} catch (error) { } catch (error) {
showError(error instanceof Error ? error.message : "Unable to delete"); showError(
error instanceof Error ? error.message : t("subtitle.unable_delete")
);
} finally { } finally {
setIsPublishing(false); setIsPublishing(false);
dismissToast(loadId); dismissToast(loadId);
@@ -553,12 +562,14 @@ const PublishSubtitles = ({
let loadId; let loadId;
try { try {
setIsPublishing(true); setIsPublishing(true);
loadId = showLoading("Publishing subtitles..."); loadId = showLoading(t("subtitle.publishing"));
await publishHandler(subtitles); await publishHandler(subtitles);
showSuccess("Subtitles published"); showSuccess(t("subtitle.published"));
setSubtitles([]); setSubtitles([]);
} catch (error) { } catch (error) {
showError(error instanceof Error ? error.message : "Unable to publish"); showError(
error instanceof Error ? error.message : t("subtitle.unable_publish")
);
} finally { } finally {
dismissToast(loadId); dismissToast(loadId);
setIsPublishing(false); setIsPublishing(false);
@@ -587,7 +598,7 @@ const PublishSubtitles = ({
}, },
}} }}
> >
<DialogTitle>My Subtitles</DialogTitle> <DialogTitle>{t("subtitle.my_subtitles")}</DialogTitle>
<IconButton <IconButton
aria-label="close" aria-label="close"
onClick={handleClose} onClick={handleClose}
@@ -626,8 +637,8 @@ const PublishSubtitles = ({
onChange={handleChange} onChange={handleChange}
aria-label="basic tabs example" aria-label="basic tabs example"
> >
<Tab label="New" {...a11yProps(0)} /> <Tab label={t("subtitle.new")} {...a11yProps(0)} />
<Tab label="Existing" {...a11yProps(1)} /> <Tab label={t("subtitle.existing")} {...a11yProps(1)} />
</Tabs> </Tabs>
</Box> </Box>
</Box> </Box>
@@ -651,7 +662,7 @@ const PublishSubtitles = ({
variant="contained" variant="contained"
> >
<input {...getInputProps()} /> <input {...getInputProps()} />
Import subtitles {t("subtitle.import_subtitles")}
</Button> </Button>
</Box> </Box>
{subtitles?.map((sub, i) => { {subtitles?.map((sub, i) => {
@@ -697,7 +708,7 @@ const PublishSubtitles = ({
size="small" size="small"
color="secondary" color="secondary"
> >
remove {t("actions.remove")}
</Button> </Button>
</Box> </Box>
</Card> </Card>
@@ -739,7 +750,7 @@ const PublishSubtitles = ({
disabled={disableButton} disabled={disableButton}
variant="contained" variant="contained"
> >
Publish {t("actions.publish")}
</Button> </Button>
)} )}
</DialogActions> </DialogActions>
@@ -849,6 +860,8 @@ interface MySubtitleProps {
onDelete: (subtitle: QortalGetMetadata) => void; onDelete: (subtitle: QortalGetMetadata) => void;
} }
const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => { const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => {
const { t } = useLibTranslation();
const { resource, isLoading, error } = usePublish(2, "JSON", sub); const { resource, isLoading, error } = usePublish(2, "JSON", sub);
return ( return (
<Card <Card
@@ -887,7 +900,7 @@ const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => {
size="small" size="small"
color="secondary" color="secondary"
> >
delete {t("actions.delete")}
</Button> </Button>
</Box> </Box>
</Card> </Card>

View File

@@ -3,9 +3,7 @@ import {
Box, Box,
ButtonBase, ButtonBase,
Divider, Divider,
Fade,
IconButton, IconButton,
Popover,
Popper, Popper,
Slider, Slider,
Typography, Typography,
@@ -13,11 +11,9 @@ import {
} from "@mui/material"; } from "@mui/material";
export const fontSizeExSmall = "60%"; export const fontSizeExSmall = "60%";
export const fontSizeSmall = "80%"; export const fontSizeSmall = "80%";
import AspectRatioIcon from "@mui/icons-material/AspectRatio";
import { import {
Fullscreen, Fullscreen,
Pause, Pause,
PictureInPicture,
PlayArrow, PlayArrow,
Refresh, Refresh,
VolumeOff, VolumeOff,
@@ -25,17 +21,20 @@ import {
} from "@mui/icons-material"; } from "@mui/icons-material";
import { formatTime } from "../../utils/time.js"; import { formatTime } from "../../utils/time.js";
import { CustomFontTooltip } from "./CustomFontTooltip.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"; import SlowMotionVideoIcon from "@mui/icons-material/SlowMotionVideo";
const buttonPaddingBig = "6px"; const buttonPaddingBig = "6px";
const buttonPaddingSmall = "4px"; const buttonPaddingSmall = "4px";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos"; import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
import CheckIcon from "@mui/icons-material/Check"; import CheckIcon from "@mui/icons-material/Check";
import { useLibTranslation } from "../../hooks/useLibTranslation.js";
export const PlayButton = ({ togglePlay, isPlaying, isScreenSmall }: any) => { export const PlayButton = ({ togglePlay, isPlaying, isScreenSmall }: any) => {
const { t } = useLibTranslation();
return ( return (
<CustomFontTooltip title="Pause/Play (Spacebar)" placement="bottom" arrow> <CustomFontTooltip title={t("video.play_pause")} placement="bottom" arrow>
<IconButton <IconButton
sx={{ sx={{
color: "white", color: "white",
@@ -50,8 +49,10 @@ export const PlayButton = ({ togglePlay, isPlaying, isScreenSmall }: any) => {
}; };
export const ReloadButton = ({ reloadVideo, isScreenSmall }: any) => { export const ReloadButton = ({ reloadVideo, isScreenSmall }: any) => {
const { t } = useLibTranslation();
return ( return (
<CustomFontTooltip title="Reload Video (R)" placement="bottom" arrow> <CustomFontTooltip title={t("video.reload_video")} placement="bottom" arrow>
<IconButton <IconButton
sx={{ sx={{
color: "white", color: "white",
@@ -72,7 +73,7 @@ export const ProgressSlider = ({
playerRef, playerRef,
resetHideTimeout, resetHideTimeout,
isVideoPlayerSmall, isVideoPlayerSmall,
isOnTimeline isOnTimeline,
}: any) => { }: any) => {
const sliderRef = useRef(null); const sliderRef = useRef(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@@ -117,26 +118,25 @@ export const ProgressSlider = ({
const debounceTimeoutRef = useRef<any>(null); const debounceTimeoutRef = useRef<any>(null);
const previousBlobUrlRef = useRef<string | null>(null); const previousBlobUrlRef = useRef<string | null>(null);
const handleMouseMove = (e: React.MouseEvent) => { const handleMouseMove = (e: React.MouseEvent) => {
const slider = sliderRef.current; const slider = sliderRef.current;
if (!slider) return; if (!slider) return;
const rect = slider.getBoundingClientRect(); const rect = slider.getBoundingClientRect();
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
const percent = x / rect.width; const percent = x / rect.width;
const time = Math.min(Math.max(0, percent * duration), duration); const time = Math.min(Math.max(0, percent * duration), duration);
// Position anchor element at the correct spot // Position anchor element at the correct spot
if (hoverAnchorRef.current) { if (hoverAnchorRef.current) {
hoverAnchorRef.current.style.left = `${x}px`; hoverAnchorRef.current.style.left = `${x}px`;
} }
setHoverX(e.clientX); // optional can be removed unless used elsewhere setHoverX(e.clientX); // optional can be removed unless used elsewhere
setShowDuration(time); setShowDuration(time);
if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current); if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
lastRequestedTimeRef.current = null; lastRequestedTimeRef.current = null;
setThumbnailUrl(null); setThumbnailUrl(null);
@@ -166,14 +166,14 @@ const handleMouseMove = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
}; };
useEffect(()=> { useEffect(() => {
if(!isOnTimeline) return if (!isOnTimeline) return;
if(hoverX){ if (hoverX) {
isOnTimeline.current = true isOnTimeline.current = true;
} else { } else {
isOnTimeline.current = false isOnTimeline.current = false;
} }
}, [hoverX]) }, [hoverX]);
return ( return (
<Box <Box
@@ -183,17 +183,17 @@ const handleMouseMove = (e: React.MouseEvent) => {
padding: isVideoPlayerSmall ? "0px" : "0px 10px", padding: isVideoPlayerSmall ? "0px" : "0px 10px",
}} }}
> >
<Box <Box
ref={hoverAnchorRef} ref={hoverAnchorRef}
sx={{ sx={{
position: "absolute", position: "absolute",
top: 0, top: 0,
width: "1px", width: "1px",
height: "1px", height: "1px",
pointerEvents: "none", pointerEvents: "none",
transform: "translateX(-50%)", // center popper on the anchor transform: "translateX(-50%)", // center popper on the anchor
}} }}
/> />
<Slider <Slider
ref={sliderRef} ref={sliderRef}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
@@ -268,9 +268,11 @@ const handleMouseMove = (e: React.MouseEvent) => {
}; };
export const VideoTime = ({ progress, isScreenSmall, duration }: any) => { export const VideoTime = ({ progress, isScreenSmall, duration }: any) => {
const { t } = useLibTranslation();
return ( return (
<CustomFontTooltip <CustomFontTooltip
title="Seek video in 10% increments (0-9)" title={t("video.seek_video")}
placement="bottom" placement="bottom"
arrow arrow
disableHoverListener={isScreenSmall} disableHoverListener={isScreenSmall}
@@ -295,12 +297,10 @@ export const VideoTime = ({ progress, isScreenSmall, duration }: any) => {
}; };
const VolumeButton = ({ isMuted, toggleMute }: any) => { const VolumeButton = ({ isMuted, toggleMute }: any) => {
const { t } = useLibTranslation();
return ( return (
<CustomFontTooltip <CustomFontTooltip title={t("video.toggle_mute")} placement="bottom" arrow>
title="Toggle Mute (M), Raise (UP), Lower (DOWN)"
placement="bottom"
arrow
>
<IconButton <IconButton
sx={{ sx={{
color: "white", color: "white",
@@ -373,6 +373,8 @@ export const PlaybackRate = ({
onSelect, onSelect,
openPlaybackMenu, openPlaybackMenu,
}: any) => { }: any) => {
const { t } = useLibTranslation();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const btnRef = useRef(null); const btnRef = useRef(null);
const theme = useTheme(); const theme = useTheme();
@@ -383,7 +385,7 @@ export const PlaybackRate = ({
return ( return (
<> <>
<CustomFontTooltip <CustomFontTooltip
title="Video Speed. Increase (+ or >), Decrease (- or <)" title={t("video.video_speed")}
placement="bottom" placement="bottom"
arrow 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) => { export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => {
const { t } = useLibTranslation();
return ( return (
<CustomFontTooltip title="Toggle Fullscreen (F)" placement="bottom" arrow> <CustomFontTooltip
title={t("video.toggle_fullscreen")}
placement="bottom"
arrow
>
<IconButton <IconButton
sx={{ sx={{
color: "white", color: "white",
@@ -479,6 +441,8 @@ export const PlayBackMenu = ({
playbackRate, playbackRate,
isFromDrawer, isFromDrawer,
}: PlayBackMenuProps) => { }: PlayBackMenuProps) => {
const { t } = useLibTranslation();
const theme = useTheme(); const theme = useTheme();
const ref = useRef<any>(null); const ref = useRef<any>(null);
@@ -539,7 +503,7 @@ export const PlayBackMenu = ({
fontSize: "0.85rem", fontSize: "0.85rem",
}} }}
> >
Playback speed {t("video.playback_speed")}
</Typography> </Typography>
</ButtonBase> </ButtonBase>
</Box> </Box>

View File

@@ -12,6 +12,8 @@ import {
import SubtitlesIcon from "@mui/icons-material/Subtitles"; import SubtitlesIcon from "@mui/icons-material/Subtitles";
import { CustomFontTooltip } from "./CustomFontTooltip"; import { CustomFontTooltip } from "./CustomFontTooltip";
import { RefObject } from "react"; import { RefObject } from "react";
import { useLibTranslation } from "../../hooks/useLibTranslation";
import i18n from "../../i18n/i18n";
interface VideoControlsBarProps { interface VideoControlsBarProps {
canPlay: boolean; canPlay: boolean;
isScreenSmall: boolean; isScreenSmall: boolean;
@@ -71,8 +73,10 @@ export const VideoControlsBar = ({
openPlaybackMenu, openPlaybackMenu,
togglePictureInPicture, togglePictureInPicture,
isVideoPlayerSmall, isVideoPlayerSmall,
isOnTimeline isOnTimeline,
}: VideoControlsBarProps) => { }: VideoControlsBarProps) => {
const { t } = useLibTranslation();
const showMobileControls = isScreenSmall && canPlay; const showMobileControls = isScreenSmall && canPlay;
const controlGroupSX = { const controlGroupSX = {
@@ -145,8 +149,11 @@ export const VideoControlsBar = ({
increaseSpeed={increaseSpeed} increaseSpeed={increaseSpeed}
decreaseSpeed={decreaseSpeed} decreaseSpeed={decreaseSpeed}
/> />
{/* <ObjectFitButton /> */} <CustomFontTooltip
<CustomFontTooltip title="Subtitles" placement="bottom" arrow> title={t("subtitle.subtitles")}
placement="bottom"
arrow
>
<IconButton <IconButton
ref={subtitleBtnRef} ref={subtitleBtnRef}
onClick={openSubtitleManager} onClick={openSubtitleManager}
@@ -158,7 +165,7 @@ export const VideoControlsBar = ({
/> />
</IconButton> </IconButton>
</CustomFontTooltip> </CustomFontTooltip>
{/* <PictureInPictureButton togglePictureInPicture={togglePictureInPicture} /> */}
<FullscreenButton toggleFullscreen={toggleFullscreen} /> <FullscreenButton toggleFullscreen={toggleFullscreen} />
</Box> </Box>
</Box> </Box>

View File

@@ -631,10 +631,8 @@ export const VideoPlayer = ({
playerRef.current?.playbackRate(playbackRate); playerRef.current?.playbackRate(playbackRate);
playerRef.current?.volume(volume); playerRef.current?.volume(volume);
const key = `${resource.service}-${resource.name}-${resource.identifier}` const key = `${resource.service}-${resource.name}-${resource.identifier}`
console.log('key', key)
if (key) { if (key) {
const savedProgress = getProgress(key); const savedProgress = getProgress(key);
console.log('savedProgress', savedProgress)
if (typeof savedProgress === "number") { if (typeof savedProgress === "number") {
playerRef.current?.currentTime(savedProgress); playerRef.current?.currentTime(savedProgress);
} }

View File

@@ -17,6 +17,7 @@ import { useProgressStore } from "../state/video";
import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer"; import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer";
import { MultiPublishDialog } from "../components/MultiPublish/MultiPublishDialog"; import { MultiPublishDialog } from "../components/MultiPublish/MultiPublishDialog";
import { useMultiplePublishStore } from "../state/multiplePublish"; import { useMultiplePublishStore } from "../state/multiplePublish";
import { useIframe } from "../hooks/useIframe";
// ✅ Define Global Context Type // ✅ Define Global Context Type
interface GlobalContextType { interface GlobalContextType {
@@ -52,6 +53,8 @@ export const GlobalProvider = ({
config, config,
toastStyle = {}, toastStyle = {},
}: GlobalProviderProps) => { }: GlobalProviderProps) => {
useIframe()
// ✅ Call hooks and pass in options dynamically // ✅ Call hooks and pass in options dynamically
const auth = useAuth(config?.auth || {}); const auth = useAuth(config?.auth || {});
const isPublishing = useMultiplePublishStore((s) => s.isPublishing); const isPublishing = useMultiplePublishStore((s) => s.isPublishing);
@@ -94,6 +97,7 @@ export const GlobalProvider = ({
return ( return (
<GlobalContext.Provider value={contextValue}> <GlobalContext.Provider value={contextValue}>
{config?.enableGlobalVideoFeature && <GlobalPipPlayer />} {config?.enableGlobalVideoFeature && <GlobalPipPlayer />}
{isPublishing && <MultiPublishDialog />} {isPublishing && <MultiPublishDialog />}
@@ -108,6 +112,7 @@ export const GlobalProvider = ({
<IndexManager username={auth?.name} /> <IndexManager username={auth?.name} />
{children} {children}
</GlobalContext.Provider> </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 let isPaused = false
const callFunction = async (build?: boolean, isRecalling?: boolean) => { const callFunction = async (build?: boolean, isRecalling?: boolean) => {
try { try {
console.log('retryAttempts', retryAttempts, tries)
if ((isCalling || isPaused) && !build) return; if ((isCalling || isPaused) && !build) return;
isCalling = true; isCalling = true;
statusMap[resourceId] = getResourceStatus(resourceId); statusMap[resourceId] = getResourceStatus(resourceId);