fix: 修复服务端渲染时的翻译不匹配问题
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Music, Volume2 } from "lucide-react";
|
||||
import { FileUploader } from "@/components/tools/FileUploader";
|
||||
@@ -10,7 +10,7 @@ import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUploadStore } from "@/store/uploadStore";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { useTranslation, getServerTranslations } from "@/lib/i18n";
|
||||
import type { UploadedFile, ProcessedFile, AudioCompressConfig } from "@/types";
|
||||
|
||||
const audioAccept = {
|
||||
@@ -24,15 +24,13 @@ const defaultConfig: AudioCompressConfig = {
|
||||
channels: 2,
|
||||
};
|
||||
|
||||
function useConfigOptions(config: AudioCompressConfig): ConfigOption[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function useConfigOptions(config: AudioCompressConfig, getT: (key: string) => string): ConfigOption[] {
|
||||
return [
|
||||
{
|
||||
id: "bitrate",
|
||||
type: "select",
|
||||
label: t("config.audioCompression.bitrate"),
|
||||
description: t("config.audioCompression.bitrateDescription"),
|
||||
label: getT("config.audioCompression.bitrate"),
|
||||
description: getT("config.audioCompression.bitrateDescription"),
|
||||
value: config.bitrate,
|
||||
options: [
|
||||
{ label: "64 kbps", value: 64 },
|
||||
@@ -45,8 +43,8 @@ function useConfigOptions(config: AudioCompressConfig): ConfigOption[] {
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: t("config.audioCompression.format"),
|
||||
description: t("config.audioCompression.formatDescription"),
|
||||
label: getT("config.audioCompression.format"),
|
||||
description: getT("config.audioCompression.formatDescription"),
|
||||
value: config.format,
|
||||
options: [
|
||||
{ label: "MP3", value: "mp3" },
|
||||
@@ -58,8 +56,8 @@ function useConfigOptions(config: AudioCompressConfig): ConfigOption[] {
|
||||
{
|
||||
id: "sampleRate",
|
||||
type: "select",
|
||||
label: t("config.audioCompression.sampleRate"),
|
||||
description: t("config.audioCompression.sampleRateDescription"),
|
||||
label: getT("config.audioCompression.sampleRate"),
|
||||
description: getT("config.audioCompression.sampleRateDescription"),
|
||||
value: config.sampleRate,
|
||||
options: [
|
||||
{ label: "44.1 kHz", value: 44100 },
|
||||
@@ -69,19 +67,27 @@ function useConfigOptions(config: AudioCompressConfig): ConfigOption[] {
|
||||
{
|
||||
id: "channels",
|
||||
type: "radio",
|
||||
label: t("config.audioCompression.channels"),
|
||||
description: t("config.audioCompression.channelsDescription"),
|
||||
label: getT("config.audioCompression.channels"),
|
||||
description: getT("config.audioCompression.channelsDescription"),
|
||||
value: config.channels,
|
||||
options: [
|
||||
{ label: t("config.audioCompression.stereo"), value: 2 },
|
||||
{ label: t("config.audioCompression.mono"), value: 1 },
|
||||
{ label: getT("config.audioCompression.stereo"), value: 2 },
|
||||
{ label: getT("config.audioCompression.mono"), value: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function AudioCompressPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getT = (key: string) => {
|
||||
if (!mounted) return getServerTranslations("en").t(key);
|
||||
return t(key);
|
||||
};
|
||||
|
||||
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
|
||||
useUploadStore();
|
||||
|
||||
@@ -118,7 +124,7 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
message: t("processing.uploadingAudio"),
|
||||
message: getT("processing.uploadingAudio"),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -128,14 +134,14 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: i,
|
||||
message: t("processing.uploadProgress", { progress: i }),
|
||||
message: getT("processing.uploadProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: 0,
|
||||
message: t("processing.compressingAudio"),
|
||||
message: getT("processing.compressingAudio"),
|
||||
});
|
||||
|
||||
// Simulate processing
|
||||
@@ -144,7 +150,7 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: i,
|
||||
message: t("processing.compressProgress", { progress: i }),
|
||||
message: getT("processing.compressProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,14 +174,14 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
message: t("processing.compressionComplete"),
|
||||
message: getT("processing.compressionComplete"),
|
||||
});
|
||||
} catch (error) {
|
||||
setProcessingStatus({
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
message: t("processing.compressionFailed"),
|
||||
error: error instanceof Error ? error.message : t("processing.unknownError"),
|
||||
message: getT("processing.compressionFailed"),
|
||||
error: error instanceof Error ? error.message : getT("processing.unknownError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -185,7 +191,7 @@ export default function AudioCompressPage() {
|
||||
};
|
||||
|
||||
const canProcess = files.length > 0 && processingStatus.status !== "processing";
|
||||
const configOptions = useConfigOptions(config);
|
||||
const configOptions = useConfigOptions(config, getT);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -199,9 +205,9 @@ export default function AudioCompressPage() {
|
||||
<Music className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{t("tools.audioCompression.title")}</h1>
|
||||
<h1 className="text-3xl font-bold">{getT("tools.audioCompression.title")}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t("tools.audioCompression.description")}
|
||||
{getT("tools.audioCompression.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,8 +226,8 @@ export default function AudioCompressPage() {
|
||||
/>
|
||||
|
||||
<ConfigPanel
|
||||
title={t("config.audioCompression.title")}
|
||||
description={t("config.audioCompression.description")}
|
||||
title={getT("config.audioCompression.title")}
|
||||
description={getT("config.audioCompression.description")}
|
||||
options={configOptions.map((opt) => ({
|
||||
...opt,
|
||||
value: config[opt.id as keyof AudioCompressConfig],
|
||||
@@ -233,7 +239,7 @@ export default function AudioCompressPage() {
|
||||
{canProcess && (
|
||||
<Button onClick={handleProcess} size="lg" className="w-full">
|
||||
<Volume2 className="mr-2 h-4 w-4" />
|
||||
{t("tools.audioCompression.compressAudio")}
|
||||
{getT("tools.audioCompression.compressAudio")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -248,15 +254,15 @@ export default function AudioCompressPage() {
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
|
||||
<h3 className="mb-3 font-semibold">{t("tools.audioCompression.supportedFormats")}</h3>
|
||||
<h3 className="mb-3 font-semibold">{getT("tools.audioCompression.supportedFormats")}</h3>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{t("tools.audioCompression.input")}</p>
|
||||
<p>{t("tools.audioCompression.inputFormats")}</p>
|
||||
<p className="font-medium text-foreground">{getT("tools.audioCompression.input")}</p>
|
||||
<p>{getT("tools.audioCompression.inputFormats")}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{t("tools.audioCompression.output")}</p>
|
||||
<p>{t("tools.audioCompression.outputFormats")}</p>
|
||||
<p className="font-medium text-foreground">{getT("tools.audioCompression.output")}</p>
|
||||
<p>{getT("tools.audioCompression.outputFormats")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Image as ImageIcon, Zap } from "lucide-react";
|
||||
import { FileUploader } from "@/components/tools/FileUploader";
|
||||
@@ -10,7 +10,7 @@ import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUploadStore } from "@/store/uploadStore";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { useTranslation, getServerTranslations } from "@/lib/i18n";
|
||||
import type { UploadedFile, ProcessedFile, ImageCompressConfig } from "@/types";
|
||||
|
||||
const imageAccept = {
|
||||
@@ -22,15 +22,13 @@ const defaultConfig: ImageCompressConfig = {
|
||||
format: "original",
|
||||
};
|
||||
|
||||
function useConfigOptions(config: ImageCompressConfig): ConfigOption[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function useConfigOptions(config: ImageCompressConfig, getT: (key: string) => string): ConfigOption[] {
|
||||
return [
|
||||
{
|
||||
id: "quality",
|
||||
type: "slider",
|
||||
label: t("config.imageCompression.quality"),
|
||||
description: t("config.imageCompression.qualityDescription"),
|
||||
label: getT("config.imageCompression.quality"),
|
||||
description: getT("config.imageCompression.qualityDescription"),
|
||||
value: config.quality,
|
||||
min: 1,
|
||||
max: 100,
|
||||
@@ -41,21 +39,29 @@ function useConfigOptions(config: ImageCompressConfig): ConfigOption[] {
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: t("config.imageCompression.format"),
|
||||
description: t("config.imageCompression.formatDescription"),
|
||||
label: getT("config.imageCompression.format"),
|
||||
description: getT("config.imageCompression.formatDescription"),
|
||||
value: config.format,
|
||||
options: [
|
||||
{ label: t("config.imageCompression.formatOriginal"), value: "original" },
|
||||
{ label: t("config.imageCompression.formatJpeg"), value: "jpeg" },
|
||||
{ label: t("config.imageCompression.formatPng"), value: "png" },
|
||||
{ label: t("config.imageCompression.formatWebp"), value: "webp" },
|
||||
{ label: getT("config.imageCompression.formatOriginal"), value: "original" },
|
||||
{ label: getT("config.imageCompression.formatJpeg"), value: "jpeg" },
|
||||
{ label: getT("config.imageCompression.formatPng"), value: "png" },
|
||||
{ label: getT("config.imageCompression.formatWebp"), value: "webp" },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function ImageCompressPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getT = (key: string) => {
|
||||
if (!mounted) return getServerTranslations("en").t(key);
|
||||
return t(key);
|
||||
};
|
||||
|
||||
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
|
||||
useUploadStore();
|
||||
|
||||
@@ -92,7 +98,7 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
message: t("processing.uploadingImages"),
|
||||
message: getT("processing.uploadingImages"),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -102,14 +108,14 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: i,
|
||||
message: t("processing.uploadProgress", { progress: i }),
|
||||
message: getT("processing.uploadProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: 0,
|
||||
message: t("processing.compressingImages"),
|
||||
message: getT("processing.compressingImages"),
|
||||
});
|
||||
|
||||
// Simulate processing
|
||||
@@ -118,7 +124,7 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: i,
|
||||
message: t("processing.compressProgress", { progress: i }),
|
||||
message: getT("processing.compressProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,14 +147,14 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
message: t("processing.compressionComplete"),
|
||||
message: getT("processing.compressionComplete"),
|
||||
});
|
||||
} catch (error) {
|
||||
setProcessingStatus({
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
message: t("processing.compressionFailed"),
|
||||
error: error instanceof Error ? error.message : t("processing.unknownError"),
|
||||
message: getT("processing.compressionFailed"),
|
||||
error: error instanceof Error ? error.message : getT("processing.unknownError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -158,7 +164,7 @@ export default function ImageCompressPage() {
|
||||
};
|
||||
|
||||
const canProcess = files.length > 0 && processingStatus.status !== "processing";
|
||||
const configOptions = useConfigOptions(config);
|
||||
const configOptions = useConfigOptions(config, getT);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -172,9 +178,9 @@ export default function ImageCompressPage() {
|
||||
<ImageIcon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{t("tools.imageCompression.title")}</h1>
|
||||
<h1 className="text-3xl font-bold">{getT("tools.imageCompression.title")}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t("tools.imageCompression.description")}
|
||||
{getT("tools.imageCompression.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,8 +199,8 @@ export default function ImageCompressPage() {
|
||||
/>
|
||||
|
||||
<ConfigPanel
|
||||
title={t("config.imageCompression.title")}
|
||||
description={t("config.imageCompression.description")}
|
||||
title={getT("config.imageCompression.title")}
|
||||
description={getT("config.imageCompression.description")}
|
||||
options={configOptions.map((opt) => ({
|
||||
...opt,
|
||||
value: config[opt.id as keyof ImageCompressConfig],
|
||||
@@ -206,7 +212,7 @@ export default function ImageCompressPage() {
|
||||
{canProcess && (
|
||||
<Button onClick={handleProcess} size="lg" className="w-full">
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
{t("tools.imageCompression.compressImages")}
|
||||
{getT("tools.imageCompression.compressImages")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -221,9 +227,9 @@ export default function ImageCompressPage() {
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
|
||||
<h3 className="mb-3 font-semibold">{t("tools.imageCompression.features")}</h3>
|
||||
<h3 className="mb-3 font-semibold">{getT("tools.imageCompression.features")}</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
{(t("tools.imageCompression.featureList") as unknown as string[]).map((feature, index) => (
|
||||
{(getT("tools.imageCompression.featureList") as unknown as string[]).map((feature, index) => (
|
||||
<li key={index}>• {feature}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Video, Settings } from "lucide-react";
|
||||
import { FileUploader } from "@/components/tools/FileUploader";
|
||||
@@ -10,7 +10,7 @@ import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUploadStore } from "@/store/uploadStore";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { useTranslation, getServerTranslations } from "@/lib/i18n";
|
||||
import type { UploadedFile, ProcessedFile, VideoFramesConfig } from "@/types";
|
||||
|
||||
const videoAccept = {
|
||||
@@ -25,15 +25,13 @@ const defaultConfig: VideoFramesConfig = {
|
||||
height: undefined,
|
||||
};
|
||||
|
||||
function useConfigOptions(config: VideoFramesConfig): ConfigOption[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function useConfigOptions(config: VideoFramesConfig, getT: (key: string) => string): ConfigOption[] {
|
||||
return [
|
||||
{
|
||||
id: "fps",
|
||||
type: "slider",
|
||||
label: t("config.videoFrames.fps"),
|
||||
description: t("config.videoFrames.fpsDescription"),
|
||||
label: getT("config.videoFrames.fps"),
|
||||
description: getT("config.videoFrames.fpsDescription"),
|
||||
value: config.fps,
|
||||
min: 1,
|
||||
max: 60,
|
||||
@@ -44,8 +42,8 @@ function useConfigOptions(config: VideoFramesConfig): ConfigOption[] {
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: t("config.videoFrames.format"),
|
||||
description: t("config.videoFrames.formatDescription"),
|
||||
label: getT("config.videoFrames.format"),
|
||||
description: getT("config.videoFrames.formatDescription"),
|
||||
value: config.format,
|
||||
options: [
|
||||
{ label: "PNG", value: "png" },
|
||||
@@ -56,8 +54,8 @@ function useConfigOptions(config: VideoFramesConfig): ConfigOption[] {
|
||||
{
|
||||
id: "quality",
|
||||
type: "slider",
|
||||
label: t("config.videoFrames.quality"),
|
||||
description: t("config.videoFrames.qualityDescription"),
|
||||
label: getT("config.videoFrames.quality"),
|
||||
description: getT("config.videoFrames.qualityDescription"),
|
||||
value: config.quality,
|
||||
min: 1,
|
||||
max: 100,
|
||||
@@ -68,7 +66,15 @@ function useConfigOptions(config: VideoFramesConfig): ConfigOption[] {
|
||||
}
|
||||
|
||||
export default function VideoFramesPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getT = (key: string) => {
|
||||
if (!mounted) return getServerTranslations("en").t(key);
|
||||
return t(key);
|
||||
};
|
||||
|
||||
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
|
||||
useUploadStore();
|
||||
|
||||
@@ -105,7 +111,7 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
message: t("processing.uploadingVideo"),
|
||||
message: getT("processing.uploadingVideo"),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -115,14 +121,14 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: i,
|
||||
message: t("processing.uploadProgress", { progress: i }),
|
||||
message: getT("processing.uploadProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: 0,
|
||||
message: t("processing.extractingFrames"),
|
||||
message: getT("processing.extractingFrames"),
|
||||
});
|
||||
|
||||
// Simulate processing
|
||||
@@ -131,7 +137,7 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: i,
|
||||
message: t("processing.processProgress", { progress: i }),
|
||||
message: getT("processing.processProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,14 +161,14 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
message: t("processing.processingComplete"),
|
||||
message: getT("processing.processingComplete"),
|
||||
});
|
||||
} catch (error) {
|
||||
setProcessingStatus({
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
message: t("processing.processingFailed"),
|
||||
error: error instanceof Error ? error.message : t("processing.unknownError"),
|
||||
message: getT("processing.processingFailed"),
|
||||
error: error instanceof Error ? error.message : getT("processing.unknownError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -172,7 +178,7 @@ export default function VideoFramesPage() {
|
||||
};
|
||||
|
||||
const canProcess = files.length > 0 && processingStatus.status !== "processing";
|
||||
const configOptions = useConfigOptions(config);
|
||||
const configOptions = useConfigOptions(config, getT);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -187,9 +193,9 @@ export default function VideoFramesPage() {
|
||||
<Video className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{t("tools.videoFrames.title")}</h1>
|
||||
<h1 className="text-3xl font-bold">{getT("tools.videoFrames.title")}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t("tools.videoFrames.description")}
|
||||
{getT("tools.videoFrames.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,8 +215,8 @@ export default function VideoFramesPage() {
|
||||
/>
|
||||
|
||||
<ConfigPanel
|
||||
title={t("config.videoFrames.title")}
|
||||
description={t("config.videoFrames.description")}
|
||||
title={getT("config.videoFrames.title")}
|
||||
description={getT("config.videoFrames.description")}
|
||||
options={configOptions.map((opt) => ({
|
||||
...opt,
|
||||
value: config[opt.id as keyof VideoFramesConfig],
|
||||
@@ -222,7 +228,7 @@ export default function VideoFramesPage() {
|
||||
{canProcess && (
|
||||
<Button onClick={handleProcess} size="lg" className="w-full">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
{t("tools.videoFrames.processVideo")}
|
||||
{getT("tools.videoFrames.processVideo")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -239,9 +245,9 @@ export default function VideoFramesPage() {
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
|
||||
<h3 className="mb-3 font-semibold">{t("tools.videoFrames.howItWorks")}</h3>
|
||||
<h3 className="mb-3 font-semibold">{getT("tools.videoFrames.howItWorks")}</h3>
|
||||
<ol className="space-y-2 text-sm text-muted-foreground">
|
||||
{(t("tools.videoFrames.steps") as unknown as string[]).map((step, index) => (
|
||||
{(getT("tools.videoFrames.steps") as unknown as string[]).map((step, index) => (
|
||||
<li key={index}>{index + 1}. {step}</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
Reference in New Issue
Block a user