mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
feat: add internationalization support
This commit is contained in:
@@ -9,6 +9,7 @@ import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { fetchFile } from '@ffmpeg/util';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
newSpeed: 2
|
||||
@@ -18,6 +19,7 @@ export default function ChangeSpeed({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -128,13 +130,13 @@ export default function ChangeSpeed({
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'New Video Speed',
|
||||
title: t('video.changeSpeed.newVideoSpeed'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.newSpeed.toString()}
|
||||
onOwnChange={(val) => updateField('newSpeed', Number(val))}
|
||||
description="Default multiplier: 2 means 2x faster"
|
||||
description={t('video.changeSpeed.defaultMultiplier')}
|
||||
type="number"
|
||||
/>
|
||||
</Box>
|
||||
@@ -149,21 +151,32 @@ export default function ChangeSpeed({
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('video.changeSpeed.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult title="Setting Speed" value={null} loading={true} />
|
||||
<ToolFileResult
|
||||
title={t('video.changeSpeed.settingSpeed')}
|
||||
value={null}
|
||||
loading={true}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult title="Edited Video" value={result} extension="mp4" />
|
||||
<ToolFileResult
|
||||
title={t('video.changeSpeed.resultTitle')}
|
||||
value={result}
|
||||
extension="mp4"
|
||||
/>
|
||||
)
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
|
||||
toolInfo={{
|
||||
title: t('video.changeSpeed.toolInfo.title', { title }),
|
||||
description: longDescription
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,17 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Change speed',
|
||||
name: 'Change Video Speed',
|
||||
path: 'change-speed',
|
||||
icon: 'material-symbols-light:speed-outline',
|
||||
icon: 'material-symbols:speed',
|
||||
description:
|
||||
'This online utility lets you change the speed of a video. You can speed it up or slow it down.',
|
||||
shortDescription: 'Quickly change video speed',
|
||||
keywords: ['change', 'speed'],
|
||||
component: lazy(() => import('./index'))
|
||||
'Change the playback speed of video files. Speed up or slow down videos while maintaining audio synchronization. Supports various speed multipliers and common video formats.',
|
||||
shortDescription: 'Change video playback speed',
|
||||
keywords: ['video', 'speed', 'playback', 'fast', 'slow'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video.changeSpeed.name',
|
||||
description: 'video.changeSpeed.description',
|
||||
shortDescription: 'video.changeSpeed.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { compressVideo, VideoResolution } from './service';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import Slider from 'rc-slider';
|
||||
import 'rc-slider/assets/index.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const initialValues = {
|
||||
width: 480 as VideoResolution,
|
||||
@@ -68,6 +69,7 @@ const presetOptions = [
|
||||
];
|
||||
|
||||
export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -100,7 +102,7 @@ export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Resolution',
|
||||
title: t('video.compress.resolution'),
|
||||
component: (
|
||||
<Box>
|
||||
{resolutionOptions.map((option) => (
|
||||
@@ -117,7 +119,7 @@ export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Quality (CRF)',
|
||||
title: t('video.compress.quality'),
|
||||
component: (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Slider
|
||||
@@ -129,9 +131,9 @@ export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
updateField('crf', typeof value === 'number' ? value : value[0]);
|
||||
}}
|
||||
marks={{
|
||||
0: 'Lossless',
|
||||
23: 'Default',
|
||||
51: 'Worst'
|
||||
0: t('video.compress.lossless'),
|
||||
23: t('video.compress.default'),
|
||||
51: t('video.compress.worst')
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -160,16 +162,16 @@ export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('video.compress.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Compressed Video'}
|
||||
title={t('video.compress.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
loading={loading}
|
||||
loadingText={'Compressing video...'}
|
||||
loadingText={t('video.compress.loadingText')}
|
||||
/>
|
||||
}
|
||||
initialValues={initialValues}
|
||||
|
||||
@@ -16,5 +16,10 @@ export const tool = defineTool('video', {
|
||||
'resolution',
|
||||
'reduce size'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video.compress.name',
|
||||
description: 'video.compress.description',
|
||||
shortDescription: 'video.compress.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Box, TextField, Typography, Alert } from '@mui/material';
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import { Box, Typography, TextField, Alert } from '@mui/material';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { debounce } from 'lodash';
|
||||
import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { cropVideo, getVideoDimensions } from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import { debounce } from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
x: 0,
|
||||
@@ -17,6 +18,7 @@ const initialValues: InitialValuesType = {
|
||||
};
|
||||
|
||||
export default function CropVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -30,19 +32,23 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
if (!videoDimensions) return '';
|
||||
|
||||
if (values.x < 0 || values.y < 0) {
|
||||
return 'X and Y coordinates must be non-negative';
|
||||
return t('video.cropVideo.errorNonNegativeCoordinates');
|
||||
}
|
||||
|
||||
if (values.width <= 0 || values.height <= 0) {
|
||||
return 'Width and height must be positive';
|
||||
return t('video.cropVideo.errorPositiveDimensions');
|
||||
}
|
||||
|
||||
if (values.x + values.width > videoDimensions.width) {
|
||||
return `Crop area extends beyond video width (${videoDimensions.width}px)`;
|
||||
return t('video.cropVideo.errorBeyondWidth', {
|
||||
width: videoDimensions.width
|
||||
});
|
||||
}
|
||||
|
||||
if (values.y + values.height > videoDimensions.height) {
|
||||
return `Crop area extends beyond video height (${videoDimensions.height}px)`;
|
||||
return t('video.cropVideo.errorBeyondHeight', {
|
||||
height: videoDimensions.height
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -68,9 +74,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
setResult(croppedFile);
|
||||
} catch (error) {
|
||||
console.error('Error cropping video:', error);
|
||||
setProcessingError(
|
||||
'Error cropping video. Please check parameters and video file.'
|
||||
);
|
||||
setProcessingError(t('video.cropVideo.errorCroppingVideo'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -86,24 +90,26 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Video Information',
|
||||
title: t('video.cropVideo.videoInformation'),
|
||||
component: (
|
||||
<Box>
|
||||
{videoDimensions ? (
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
Video dimensions: {videoDimensions.width} ×{' '}
|
||||
{videoDimensions.height} pixels
|
||||
{t('video.cropVideo.videoDimensions', {
|
||||
width: videoDimensions.width,
|
||||
height: videoDimensions.height
|
||||
})}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
Load a video to see dimensions
|
||||
{t('video.cropVideo.loadVideoForDimensions')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Crop Coordinates',
|
||||
title: t('video.cropVideo.cropCoordinates'),
|
||||
component: (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{processingError && (
|
||||
@@ -113,7 +119,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<TextField
|
||||
label="X (left)"
|
||||
label={t('video.cropVideo.xCoordinate')}
|
||||
type="number"
|
||||
value={values.x}
|
||||
onChange={(e) => updateField('x', parseInt(e.target.value) || 0)}
|
||||
@@ -121,7 +127,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
inputProps={{ min: 0 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Y (top)"
|
||||
label={t('video.cropVideo.yCoordinate')}
|
||||
type="number"
|
||||
value={values.y}
|
||||
onChange={(e) => updateField('y', parseInt(e.target.value) || 0)}
|
||||
@@ -131,7 +137,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<TextField
|
||||
label="Width"
|
||||
label={t('video.cropVideo.width')}
|
||||
type="number"
|
||||
value={values.width}
|
||||
onChange={(e) =>
|
||||
@@ -141,7 +147,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
inputProps={{ min: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Height"
|
||||
label={t('video.cropVideo.height')}
|
||||
type="number"
|
||||
value={values.height}
|
||||
onChange={(e) =>
|
||||
@@ -183,7 +189,9 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error getting video dimensions:', error);
|
||||
setProcessingError('Failed to load video dimensions');
|
||||
setProcessingError(
|
||||
t('video.cropVideo.errorLoadingDimensions')
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setVideoDimensions(null);
|
||||
@@ -191,20 +199,20 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
}
|
||||
setInput(video);
|
||||
}}
|
||||
title={'Input Video'}
|
||||
title={t('video.cropVideo.inputTitle')}
|
||||
/>
|
||||
)}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
title={'Cropping Video'}
|
||||
title={t('video.cropVideo.croppingVideo')}
|
||||
value={null}
|
||||
loading={true}
|
||||
extension={''}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
title={'Cropped Video'}
|
||||
title={t('video.cropVideo.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
|
||||
@@ -2,13 +2,17 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Crop video',
|
||||
name: 'Crop Video',
|
||||
path: 'crop-video',
|
||||
icon: 'mdi:crop',
|
||||
description: 'Crop a video by specifying coordinates and dimensions',
|
||||
shortDescription: 'Crop video to specific area',
|
||||
keywords: ['crop', 'video', 'trim', 'cut', 'resize'],
|
||||
longDescription:
|
||||
'Remove unwanted parts from the edges of your video by cropping it to a specific rectangular area. Define the starting coordinates (X, Y) and the width and height of the area you want to keep.',
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:crop',
|
||||
description:
|
||||
'Crop video files to remove unwanted areas or focus on specific content. Specify crop dimensions and position to create custom video compositions.',
|
||||
shortDescription: 'Crop video to remove unwanted areas',
|
||||
keywords: ['video', 'crop', 'trim', 'edit', 'resize'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video.cropVideo.name',
|
||||
description: 'video.cropVideo.description',
|
||||
shortDescription: 'video.cropVideo.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { flipVideo } from './service';
|
||||
import { FlipOrientation, InitialValuesType } from './types';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const initialValues: InitialValuesType = {
|
||||
orientation: 'horizontal'
|
||||
@@ -30,6 +31,7 @@ const orientationOptions: { value: FlipOrientation; label: string }[] = [
|
||||
];
|
||||
|
||||
export default function FlipVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -58,13 +60,13 @@ export default function FlipVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Orientation',
|
||||
title: t('video.flip.orientation'),
|
||||
component: (
|
||||
<Box>
|
||||
{orientationOptions.map((orientationOption) => (
|
||||
<SimpleRadio
|
||||
key={orientationOption.value}
|
||||
title={orientationOption.label}
|
||||
title={t(`video.flip.${orientationOption.value}Label`)}
|
||||
checked={values.orientation === orientationOption.value}
|
||||
onClick={() => {
|
||||
updateField('orientation', orientationOption.value);
|
||||
@@ -84,20 +86,20 @@ export default function FlipVideo({ title }: ToolComponentProps) {
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('video.flip.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
title={'Flipping Video'}
|
||||
title={t('video.flip.flippingVideo')}
|
||||
value={null}
|
||||
loading={true}
|
||||
extension={''}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
title={'Flipped Video'}
|
||||
title={t('video.flip.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
|
||||
@@ -4,12 +4,15 @@ import { lazy } from 'react';
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Flip Video',
|
||||
path: 'flip',
|
||||
icon: 'mdi:flip-horizontal',
|
||||
icon: 'material-symbols:flip',
|
||||
description:
|
||||
'This online utility allows you to flip videos horizontally or vertically. You can preview the flipped video before processing. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Flip videos horizontally or vertically',
|
||||
keywords: ['flip', 'video', 'mirror', 'edit', 'horizontal', 'vertical'],
|
||||
longDescription:
|
||||
'Easily flip your videos horizontally (mirror) or vertically (upside down) with this simple online tool.',
|
||||
component: lazy(() => import('./index'))
|
||||
'Flip video files horizontally or vertically. Mirror videos for special effects or correct orientation issues.',
|
||||
shortDescription: 'Flip video horizontally or vertically',
|
||||
keywords: ['video', 'flip', 'mirror', 'horizontal', 'vertical'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video.flip.name',
|
||||
description: 'video.flip.description',
|
||||
shortDescription: 'video.flip.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,12 +3,17 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('gif', {
|
||||
name: 'Change speed',
|
||||
name: 'Change GIF Speed',
|
||||
path: 'change-speed',
|
||||
icon: 'material-symbols-light:speed-outline',
|
||||
icon: 'material-symbols:speed',
|
||||
description:
|
||||
'This online utility lets you change the speed of a GIF animation. You can speed it up or slow it down. You can set the same constant delay between all frames or change the delays of individual frames. You can also play both the input and output GIFs at the same time and compare their speeds',
|
||||
shortDescription: 'Quickly change GIF speed',
|
||||
keywords: ['change', 'speed'],
|
||||
component: lazy(() => import('./index'))
|
||||
'Change the playback speed of GIF animations. Speed up or slow down GIFs while maintaining smooth animation.',
|
||||
shortDescription: 'Change GIF animation speed',
|
||||
keywords: ['gif', 'speed', 'animation', 'fast', 'slow'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'gif.changeSpeed.name',
|
||||
description: 'gif.changeSpeed.description',
|
||||
shortDescription: 'gif.changeSpeed.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
79
src/pages/tools/video/i18n/en.json
Normal file
79
src/pages/tools/video/i18n/en.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"compress": {
|
||||
"title": "Compress Video",
|
||||
"description": "Reduce video file size while maintaining quality.",
|
||||
"inputTitle": "Input Video",
|
||||
"resultTitle": "Compressed Video",
|
||||
"compressionOptions": "Compression Options",
|
||||
"qualityDescription": "Video quality (1-100)",
|
||||
"qualityPlaceholder": "Quality",
|
||||
"toolInfo": {
|
||||
"title": "Video Compression",
|
||||
"description": "This tool allows you to compress video files to reduce their size while maintaining acceptable quality. You can adjust the compression level to balance between file size and video quality."
|
||||
}
|
||||
},
|
||||
"rotate": {
|
||||
"title": "Rotate Video",
|
||||
"description": "Rotate video by specified degrees.",
|
||||
"inputTitle": "Input Video",
|
||||
"resultTitle": "Rotated Video",
|
||||
"rotationOptions": "Rotation Options",
|
||||
"rotationAngleDescription": "Rotation angle in degrees",
|
||||
"anglePlaceholder": "Angle",
|
||||
"toolInfo": {
|
||||
"title": "Video Rotation",
|
||||
"description": "This tool allows you to rotate video files by a specified angle. You can rotate videos by 90, 180, or 270 degrees, or any custom angle."
|
||||
}
|
||||
},
|
||||
"flip": {
|
||||
"title": "Flip Video",
|
||||
"description": "Flip video horizontally or vertically.",
|
||||
"inputTitle": "Input Video",
|
||||
"resultTitle": "Flipped Video",
|
||||
"flipOptions": "Flip Options",
|
||||
"horizontalFlip": "Horizontal Flip",
|
||||
"verticalFlip": "Vertical Flip",
|
||||
"toolInfo": {
|
||||
"title": "Video Flip",
|
||||
"description": "This tool allows you to flip video files horizontally or vertically. Horizontal flip creates a mirror effect, while vertical flip turns the video upside down."
|
||||
}
|
||||
},
|
||||
"loop": {
|
||||
"title": "Loop Video",
|
||||
"description": "Create a looping video by repeating the original video multiple times.",
|
||||
"inputTitle": "Input Video",
|
||||
"resultTitle": "Looped Video",
|
||||
"loops": "Loops",
|
||||
"numberOfLoops": "Number of Loops",
|
||||
"loopingVideo": "Looping Video",
|
||||
"toolInfo": {
|
||||
"title": "What is a {{title}}?",
|
||||
"description": "This tool allows you to create a looping video by repeating the original video multiple times. You can specify how many times the video should loop."
|
||||
}
|
||||
},
|
||||
"cropVideo": {
|
||||
"title": "Crop Video",
|
||||
"description": "Crop video to remove unwanted areas.",
|
||||
"inputTitle": "Input Video",
|
||||
"resultTitle": "Cropped Video",
|
||||
"croppingVideo": "Cropping Video",
|
||||
"videoInformation": "Video Information",
|
||||
"videoDimensions": "Video dimensions: {{width}} × {{height}} pixels",
|
||||
"loadVideoForDimensions": "Load a video to see dimensions",
|
||||
"cropCoordinates": "Crop Coordinates",
|
||||
"xCoordinate": "X (left)",
|
||||
"yCoordinate": "Y (top)",
|
||||
"width": "Width",
|
||||
"height": "Height",
|
||||
"errorNonNegativeCoordinates": "X and Y coordinates must be non-negative",
|
||||
"errorPositiveDimensions": "Width and height must be positive",
|
||||
"errorBeyondWidth": "Crop area extends beyond video width ({{width}}px)",
|
||||
"errorBeyondHeight": "Crop area extends beyond video height ({{height}}px)",
|
||||
"errorCroppingVideo": "Error cropping video. Please check parameters and video file.",
|
||||
"errorLoadingDimensions": "Failed to load video dimensions",
|
||||
"toolInfo": {
|
||||
"title": "Crop Video",
|
||||
"description": "This tool allows you to crop video files to remove unwanted areas. You can specify the crop area by setting the X, Y coordinates and width, height dimensions."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { updateNumberField } from '@utils/string';
|
||||
import * as Yup from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
loops: 2
|
||||
@@ -21,6 +22,7 @@ const validationSchema = Yup.object({
|
||||
});
|
||||
|
||||
export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -43,7 +45,7 @@ export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Loops',
|
||||
title: t('video.loop.loops'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
@@ -51,7 +53,7 @@ export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
updateNumberField(value, 'loops', updateField)
|
||||
}
|
||||
value={values.loops}
|
||||
label={'Number of Loops'}
|
||||
label={t('video.loop.numberOfLoops')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -66,14 +68,14 @@ export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
value={null}
|
||||
title={'Looping Video'}
|
||||
title={t('video.loop.loopingVideo')}
|
||||
loading={true}
|
||||
extension={''}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
value={result}
|
||||
title={'Looped Video'}
|
||||
title={t('video.loop.resultTitle')}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
)
|
||||
@@ -83,7 +85,10 @@ export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
|
||||
toolInfo={{
|
||||
title: t('video.loop.toolInfo.title', { title }),
|
||||
description: longDescription
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,15 @@ import { lazy } from 'react';
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Loop Video',
|
||||
path: 'loop',
|
||||
icon: 'ic:baseline-loop',
|
||||
icon: 'material-symbols:loop',
|
||||
description:
|
||||
'This online utility lets you loop videos by specifying the number of repetitions. You can preview the looped video before processing. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Loop videos multiple times',
|
||||
keywords: ['loop', 'video', 'repeat', 'duplicate', 'sequence', 'playback'],
|
||||
component: lazy(() => import('./index'))
|
||||
'Create looping video files that repeat continuously. Perfect for background videos, presentations, or creating seamless loops.',
|
||||
shortDescription: 'Create looping video files',
|
||||
keywords: ['video', 'loop', 'repeat', 'continuous'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video.loop.name',
|
||||
description: 'video.loop.description',
|
||||
shortDescription: 'video.loop.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { rotateVideo } from './service';
|
||||
import { RotationAngle } from '../../pdf/rotate-pdf/types';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const initialValues = {
|
||||
rotation: 90
|
||||
@@ -27,6 +28,7 @@ const angleOptions: { value: RotationAngle; label: string }[] = [
|
||||
{ value: 270, label: '270° (90° Counter-clockwise)' }
|
||||
];
|
||||
export default function RotateVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -55,13 +57,13 @@ export default function RotateVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Rotation',
|
||||
title: t('video.rotate.rotation'),
|
||||
component: (
|
||||
<Box>
|
||||
{angleOptions.map((angleOption) => (
|
||||
<SimpleRadio
|
||||
key={angleOption.value}
|
||||
title={angleOption.label}
|
||||
title={t(`video.rotate.${angleOption.value}Degrees`)}
|
||||
checked={values.rotation === angleOption.value}
|
||||
onClick={() => {
|
||||
updateField('rotation', angleOption.value);
|
||||
@@ -81,20 +83,20 @@ export default function RotateVideo({ title }: ToolComponentProps) {
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('video.rotate.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
title={'Rotating Video'}
|
||||
title={t('video.rotate.rotatingVideo')}
|
||||
value={null}
|
||||
loading={true}
|
||||
extension={''}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
title={'Rotated Video'}
|
||||
title={t('video.rotate.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
|
||||
@@ -4,10 +4,15 @@ import { lazy } from 'react';
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Rotate Video',
|
||||
path: 'rotate',
|
||||
icon: 'mdi:rotate-right',
|
||||
icon: 'material-symbols:rotate-right',
|
||||
description:
|
||||
'This online utility lets you rotate videos by 90, 180, or 270 degrees. You can preview the rotated video before processing. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Rotate videos by 90, 180, or 270 degrees',
|
||||
keywords: ['rotate', 'video', 'flip', 'edit', 'adjust'],
|
||||
component: lazy(() => import('./index'))
|
||||
'Rotate video files by 90, 180, or 270 degrees. Correct video orientation or create special effects with precise rotation control.',
|
||||
shortDescription: 'Rotate video by specified degrees',
|
||||
keywords: ['video', 'rotate', 'orientation', 'degrees'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video.rotate.name',
|
||||
description: 'video.rotate.description',
|
||||
shortDescription: 'video.rotate.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { fetchFile } from '@ffmpeg/util';
|
||||
import { debounce } from 'lodash';
|
||||
import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ffmpeg = new FFmpeg();
|
||||
|
||||
@@ -28,6 +29,7 @@ const validationSchema = Yup.object({
|
||||
});
|
||||
|
||||
export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
@@ -85,7 +87,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Timestamps',
|
||||
title: t('video.trim.timestamps'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
@@ -93,7 +95,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
updateNumberField(value, 'trimStart', updateField)
|
||||
}
|
||||
value={values.trimStart}
|
||||
label={'Start Time'}
|
||||
label={t('video.trim.startTime')}
|
||||
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
@@ -101,7 +103,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
updateNumberField(value, 'trimEnd', updateField)
|
||||
}
|
||||
value={values.trimEnd}
|
||||
label={'End Time'}
|
||||
label={t('video.trim.endTime')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -116,7 +118,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('video.trim.inputTitle')}
|
||||
showTrimControls={true}
|
||||
onTrimChange={(trimStart, trimEnd) => {
|
||||
setFieldValue('trimStart', trimStart);
|
||||
@@ -129,7 +131,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
}}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Trimmed Video'}
|
||||
title={t('video.trim.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
|
||||
@@ -4,10 +4,15 @@ import { lazy } from 'react';
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Trim Video',
|
||||
path: 'trim',
|
||||
icon: 'mdi:scissors',
|
||||
icon: 'material-symbols:content-cut',
|
||||
description:
|
||||
'This online utility lets you trim videos by setting start and end points. You can preview the trimmed section before processing. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Trim videos by setting start and end points',
|
||||
keywords: ['trim', 'cut', 'video', 'clip', 'edit'],
|
||||
component: lazy(() => import('./index'))
|
||||
'Trim video files by specifying start and end times. Remove unwanted sections from the beginning or end of videos.',
|
||||
shortDescription: 'Trim video by removing unwanted sections',
|
||||
keywords: ['video', 'trim', 'cut', 'edit', 'time'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video.trim.name',
|
||||
description: 'video.trim.description',
|
||||
shortDescription: 'video.trim.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,11 +2,17 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Video to Gif',
|
||||
name: 'Video to GIF',
|
||||
path: 'video-to-gif',
|
||||
icon: 'fluent:gif-16-regular',
|
||||
description: 'This online utility lets you convert a short video to gif.',
|
||||
shortDescription: 'Quickly convert a short video to gif',
|
||||
keywords: ['video', 'to', 'gif', 'convert'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:gif',
|
||||
description:
|
||||
'Convert video files to animated GIF format. Extract specific time ranges and create shareable animated images.',
|
||||
shortDescription: 'Convert video to animated GIF',
|
||||
keywords: ['video', 'gif', 'convert', 'animated', 'image'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video.videoToGif.name',
|
||||
description: 'video.videoToGif.description',
|
||||
shortDescription: 'video.videoToGif.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user