diff --git a/src/app/(dashboard)/tools/audio-compress/page.tsx b/src/app/(dashboard)/tools/audio-compress/page.tsx
index d6c13c7..1f0d5c4 100644
--- a/src/app/(dashboard)/tools/audio-compress/page.tsx
+++ b/src/app/(dashboard)/tools/audio-compress/page.tsx
@@ -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 (
@@ -199,9 +205,9 @@ export default function AudioCompressPage() {
-
{t("tools.audioCompression.title")}
+
{getT("tools.audioCompression.title")}
- {t("tools.audioCompression.description")}
+ {getT("tools.audioCompression.description")}
@@ -220,8 +226,8 @@ export default function AudioCompressPage() {
/>
({
...opt,
value: config[opt.id as keyof AudioCompressConfig],
@@ -233,7 +239,7 @@ export default function AudioCompressPage() {
{canProcess && (
- {t("tools.audioCompression.compressAudio")}
+ {getT("tools.audioCompression.compressAudio")}
)}
@@ -248,15 +254,15 @@ export default function AudioCompressPage() {
)}
-
{t("tools.audioCompression.supportedFormats")}
+
{getT("tools.audioCompression.supportedFormats")}
-
{t("tools.audioCompression.input")}
-
{t("tools.audioCompression.inputFormats")}
+
{getT("tools.audioCompression.input")}
+
{getT("tools.audioCompression.inputFormats")}
-
{t("tools.audioCompression.output")}
-
{t("tools.audioCompression.outputFormats")}
+
{getT("tools.audioCompression.output")}
+
{getT("tools.audioCompression.outputFormats")}
diff --git a/src/app/(dashboard)/tools/image-compress/page.tsx b/src/app/(dashboard)/tools/image-compress/page.tsx
index 4232fd6..206c6a7 100644
--- a/src/app/(dashboard)/tools/image-compress/page.tsx
+++ b/src/app/(dashboard)/tools/image-compress/page.tsx
@@ -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 (
@@ -172,9 +178,9 @@ export default function ImageCompressPage() {
-
{t("tools.imageCompression.title")}
+
{getT("tools.imageCompression.title")}
- {t("tools.imageCompression.description")}
+ {getT("tools.imageCompression.description")}
@@ -193,8 +199,8 @@ export default function ImageCompressPage() {
/>
({
...opt,
value: config[opt.id as keyof ImageCompressConfig],
@@ -206,7 +212,7 @@ export default function ImageCompressPage() {
{canProcess && (
- {t("tools.imageCompression.compressImages")}
+ {getT("tools.imageCompression.compressImages")}
)}
@@ -221,9 +227,9 @@ export default function ImageCompressPage() {
)}
-
{t("tools.imageCompression.features")}
+
{getT("tools.imageCompression.features")}
- {(t("tools.imageCompression.featureList") as unknown as string[]).map((feature, index) => (
+ {(getT("tools.imageCompression.featureList") as unknown as string[]).map((feature, index) => (
• {feature}
))}
diff --git a/src/app/(dashboard)/tools/video-frames/page.tsx b/src/app/(dashboard)/tools/video-frames/page.tsx
index a9c6361..cc58728 100644
--- a/src/app/(dashboard)/tools/video-frames/page.tsx
+++ b/src/app/(dashboard)/tools/video-frames/page.tsx
@@ -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 (
@@ -187,9 +193,9 @@ export default function VideoFramesPage() {
-
{t("tools.videoFrames.title")}
+
{getT("tools.videoFrames.title")}
- {t("tools.videoFrames.description")}
+ {getT("tools.videoFrames.description")}
@@ -209,8 +215,8 @@ export default function VideoFramesPage() {
/>
({
...opt,
value: config[opt.id as keyof VideoFramesConfig],
@@ -222,7 +228,7 @@ export default function VideoFramesPage() {
{canProcess && (
- {t("tools.videoFrames.processVideo")}
+ {getT("tools.videoFrames.processVideo")}
)}
@@ -239,9 +245,9 @@ export default function VideoFramesPage() {
{/* Info Card */}
-
{t("tools.videoFrames.howItWorks")}
+
{getT("tools.videoFrames.howItWorks")}
- {(t("tools.videoFrames.steps") as unknown as string[]).map((step, index) => (
+ {(getT("tools.videoFrames.steps") as unknown as string[]).map((step, index) => (
{index + 1}. {step}
))}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index b1ea52d..227686e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -16,94 +16,126 @@ import {
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
-import { useTranslation } from "@/lib/i18n";
+import { useTranslation, getServerTranslations } from "@/lib/i18n";
+import { useState, useEffect } from "react";
function useFeatures() {
+ 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);
+ };
+
return [
{
icon: Video,
- title: t("home.tools.videoToFrames.title"),
- description: t("home.tools.videoToFrames.description"),
+ title: getT("home.tools.videoToFrames.title"),
+ description: getT("home.tools.videoToFrames.description"),
href: "/tools/video-frames",
},
{
icon: Image,
- title: t("home.tools.imageCompression.title"),
- description: t("home.tools.imageCompression.description"),
+ title: getT("home.tools.imageCompression.title"),
+ description: getT("home.tools.imageCompression.description"),
href: "/tools/image-compress",
},
{
icon: Music,
- title: t("home.tools.audioCompression.title"),
- description: t("home.tools.audioCompression.description"),
+ title: getT("home.tools.audioCompression.title"),
+ description: getT("home.tools.audioCompression.description"),
href: "/tools/audio-compress",
},
{
icon: Sparkles,
- title: t("home.tools.aiTools.title"),
- description: t("home.tools.aiTools.description"),
+ title: getT("home.tools.aiTools.title"),
+ description: getT("home.tools.aiTools.description"),
href: "/tools/ai-tools",
},
];
}
function useBenefits() {
+ 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);
+ };
+
return [
{
icon: Zap,
- title: t("home.benefits.lightningFast.title"),
- description: t("home.benefits.lightningFast.description"),
+ title: getT("home.benefits.lightningFast.title"),
+ description: getT("home.benefits.lightningFast.description"),
},
{
icon: Shield,
- title: t("home.benefits.secure.title"),
- description: t("home.benefits.secure.description"),
+ title: getT("home.benefits.secure.title"),
+ description: getT("home.benefits.secure.description"),
},
{
icon: Users,
- title: t("home.benefits.forDevelopers.title"),
- description: t("home.benefits.forDevelopers.description"),
+ title: getT("home.benefits.forDevelopers.title"),
+ description: getT("home.benefits.forDevelopers.description"),
},
];
}
function usePricingPlans() {
+ 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);
+ };
+
return [
{
- name: t("home.pricing.plans.free.name"),
- price: t("home.pricing.plans.free.price"),
- description: t("home.pricing.plans.free.description"),
- features: t("home.pricing.plans.free.features") as unknown as string[],
- cta: t("home.pricing.plans.free.cta"),
+ name: getT("home.pricing.plans.free.name"),
+ price: getT("home.pricing.plans.free.price"),
+ description: getT("home.pricing.plans.free.description"),
+ features: (mounted ? t("home.pricing.plans.free.features") : getServerTranslations("en").t("home.pricing.plans.free.features")) as unknown as string[],
+ cta: getT("home.pricing.plans.free.cta"),
href: "/register",
},
{
- name: t("home.pricing.plans.pro.name"),
- price: t("home.pricing.plans.pro.price"),
- period: t("home.pricing.plans.pro.period"),
- description: t("home.pricing.plans.pro.description"),
- features: t("home.pricing.plans.pro.features") as unknown as string[],
- cta: t("home.pricing.plans.pro.cta"),
+ name: getT("home.pricing.plans.pro.name"),
+ price: getT("home.pricing.plans.pro.price"),
+ period: getT("home.pricing.plans.pro.period"),
+ description: getT("home.pricing.plans.pro.description"),
+ features: (mounted ? t("home.pricing.plans.pro.features") : getServerTranslations("en").t("home.pricing.plans.pro.features")) as unknown as string[],
+ cta: getT("home.pricing.plans.pro.cta"),
href: "/pricing",
- popular: t("home.pricing.plans.pro.popular") as string,
+ popular: getT("home.pricing.plans.pro.popular"),
},
{
- name: t("home.pricing.plans.enterprise.name"),
- price: t("home.pricing.plans.enterprise.price"),
- description: t("home.pricing.plans.enterprise.description"),
- features: t("home.pricing.plans.enterprise.features") as unknown as string[],
- cta: t("home.pricing.plans.enterprise.cta"),
+ name: getT("home.pricing.plans.enterprise.name"),
+ price: getT("home.pricing.plans.enterprise.price"),
+ description: getT("home.pricing.plans.enterprise.description"),
+ features: (mounted ? t("home.pricing.plans.enterprise.features") : getServerTranslations("en").t("home.pricing.plans.enterprise.features")) as unknown as string[],
+ cta: getT("home.pricing.plans.enterprise.cta"),
href: "/contact",
},
];
}
function HeroSection() {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
const { t } = useTranslation();
+ const getT = (key: string, params?: any) => {
+ if (!mounted) return getServerTranslations("en").t(key, params);
+ return t(key, params);
+ };
+
return (
{/* Background gradient */}
@@ -121,22 +153,22 @@ function HeroSection() {
>
- {t("home.hero.badge")}
+ {getT("home.hero.badge")}
- {t("home.hero.title", { speed: t("home.hero.speed") })}
+ {getT("home.hero.title")}
- {t("home.hero.description")}
+ {getT("home.hero.description")}
- {t("home.hero.startBuilding")}
+ {getT("home.hero.startBuilding")}
- {t("common.viewPricing")}
+ {getT("common.viewPricing")}
@@ -149,15 +181,15 @@ function HeroSection() {
>
10K+
-
{t("home.hero.stats.developers")}
+
{getT("home.hero.stats.developers")}
1M+
-
{t("home.hero.stats.filesProcessed")}
+
{getT("home.hero.stats.filesProcessed")}
99.9%
-
{t("home.hero.stats.uptime")}
+
{getT("home.hero.stats.uptime")}
@@ -167,9 +199,16 @@ function HeroSection() {
}
function FeaturesSection() {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
const { t } = useTranslation();
const features = useFeatures();
+ const getT = (key: string) => {
+ if (!mounted) return getServerTranslations("en").t(key);
+ return t(key);
+ };
+
return (
@@ -181,10 +220,10 @@ function FeaturesSection() {
className="mb-16 text-center xl:mb-20 2xl:mb-24"
>
- {t("home.featuresSection.title")}
+ {getT("home.featuresSection.title")}
- {t("home.featuresSection.description")}
+ {getT("home.featuresSection.description")}
@@ -208,7 +247,7 @@ function FeaturesSection() {
- {t("common.tryNow")}
+ {getT("common.tryNow")}
@@ -222,9 +261,16 @@ function FeaturesSection() {
}
function BenefitsSection() {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
const { t } = useTranslation();
const benefits = useBenefits();
+ const getT = (key: string) => {
+ if (!mounted) return getServerTranslations("en").t(key);
+ return t(key);
+ };
+
return (
@@ -236,13 +282,13 @@ function BenefitsSection() {
transition={{ duration: 0.5 }}
>
- {t("home.benefits.title")}
+ {getT("home.benefits.title")}
- {t("home.benefits.description")}
+ {getT("home.benefits.description")}
- {t("common.learnMore")}
+ {getT("common.learnMore")}
@@ -276,9 +322,16 @@ function BenefitsSection() {
}
function PricingSection() {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
const { t } = useTranslation();
const pricingPlans = usePricingPlans();
+ const getT = (key: string) => {
+ if (!mounted) return getServerTranslations("en").t(key);
+ return t(key);
+ };
+
return (
@@ -290,10 +343,10 @@ function PricingSection() {
className="mb-16 text-center xl:mb-20 2xl:mb-24"
>
- {t("home.pricing.title")}
+ {getT("home.pricing.title")}
- {t("home.pricing.description")}
+ {getT("home.pricing.description")}
@@ -345,8 +398,15 @@ function PricingSection() {
}
function CTASection() {
+ 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);
+ };
+
return (
@@ -359,17 +419,17 @@ function CTASection() {
>
- {t("home.cta.title")}
+ {getT("home.cta.title")}
- {t("home.cta.description")}
+ {getT("home.cta.description")}
- {t("home.cta.getStarted")}
+ {getT("home.cta.getStarted")}
- {t("common.contactSales")}
+ {getT("common.contactSales")}
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
index 6c36cdd..9b29c90 100644
--- a/src/components/layout/Footer.tsx
+++ b/src/components/layout/Footer.tsx
@@ -2,26 +2,34 @@
import Link from "next/link";
import { Sparkles, Github, Twitter } from "lucide-react";
-import { useTranslation } from "@/lib/i18n";
+import { useTranslation, getServerTranslations } from "@/lib/i18n";
+import { useState, useEffect } from "react";
function useFooterLinks() {
+ 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);
+ };
+
return {
product: [
- { name: t("common.features"), href: "/features" },
- { name: t("nav.pricing"), href: "/pricing" },
+ { name: getT("common.features"), href: "/features" },
+ { name: getT("nav.pricing"), href: "/pricing" },
{ name: "API", href: "/api" },
- { name: t("nav.docs"), href: "/docs" },
+ { name: getT("nav.docs"), href: "/docs" },
],
tools: [
- { name: t("sidebar.videoToFrames"), href: "/tools/video-frames" },
- { name: t("sidebar.imageCompression"), href: "/tools/image-compress" },
- { name: t("sidebar.audioCompression"), href: "/tools/audio-compress" },
- { name: t("home.tools.aiTools.title"), href: "/tools/ai-tools" },
+ { name: getT("sidebar.videoToFrames"), href: "/tools/video-frames" },
+ { name: getT("sidebar.imageCompression"), href: "/tools/image-compress" },
+ { name: getT("sidebar.audioCompression"), href: "/tools/audio-compress" },
+ { name: getT("home.tools.aiTools.title"), href: "/tools/ai-tools" },
],
company: [
- { name: t("nav.about"), href: "/about" },
+ { name: getT("nav.about"), href: "/about" },
{ name: "Blog", href: "/blog" },
{ name: "Careers", href: "/careers" },
{ name: "Contact", href: "/contact" },
@@ -47,9 +55,16 @@ const sectionTitles: Record
= {
};
export function Footer() {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
const { t } = useTranslation();
const footerLinks = useFooterLinks();
+ const getT = (key: string) => {
+ if (!mounted) return getServerTranslations("en").t(key);
+ return t(key);
+ };
+
return (
@@ -60,10 +75,10 @@ export function Footer() {
-
{t("common.appName")}
+
{getT("common.appName")}
- {t("footer.tagline")}
+ {getT("footer.tagline")}
@@ -139,7 +154,7 @@ export function Footer() {
{/* Bottom section */}
- © {new Date().getFullYear()} {t("common.appName")}. All rights reserved.
+ © {new Date().getFullYear()} {getT("common.appName")}. All rights reserved.
{socialLinks.map((link) => {
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index e672aaa..5c362d7 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -2,12 +2,12 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X, Sparkles } from "lucide-react";
// import { Button } from "@/components/ui/button"; // TODO: Uncomment when adding login/register buttons
import { LanguageSwitcher } from "./LanguageSwitcher";
-import { useTranslation } from "@/lib/i18n";
+import { useTranslation, getServerTranslations } from "@/lib/i18n";
import { cn } from "@/lib/utils";
function useNavItems() {
@@ -24,8 +24,40 @@ function useNavItems() {
export function Header() {
const pathname = usePathname();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const [mounted, setMounted] = useState(false);
const navItems = useNavItems();
- const { t } = useTranslation();
+ const { t, locale, setLocale } = useTranslation();
+
+ useEffect(() => {
+ setMounted(true);
+
+ // Auto detect language on first mount if not manually set
+ const stored = localStorage.getItem("locale-storage");
+ if (!stored) {
+ const lang = navigator.language.toLowerCase();
+ if (lang.includes("zh")) {
+ setLocale("zh");
+ }
+ }
+ }, [setLocale]);
+
+ useEffect(() => {
+ if (mounted) {
+ document.documentElement.lang = locale;
+ }
+ }, [locale, mounted]);
+
+ // Prevent hydration mismatch by rendering a stable version initially
+ const displayT = (key: string, params?: any) => {
+ if (!mounted) return getServerTranslations("en").t(key, params);
+ return t(key, params);
+ };
+
+ const currentNavItems = mounted ? navItems : [
+ { name: "Tools", href: "/tools/image-compress" },
+ { name: "Pricing", href: "/tools/video-frames" },
+ { name: "Docs", href: "/tools/audio-compress" },
+ ];
return (