- Supabase not configured
+ {t("title")}
- Copy .env.example{" "}
- to .env.local and set{" "}
- NEXT_PUBLIC_SUPABASE_URL{" "}
- and{" "}
- NEXT_PUBLIC_SUPABASE_ANON_KEY
- , then restart the dev server.
+ {t.rich("instructions", {
+ envExample: () => (
+ .env.example
+ ),
+ envLocal: () => (
+ .env.local
+ ),
+ supabaseUrl: () => (
+ NEXT_PUBLIC_SUPABASE_URL
+ ),
+ supabaseAnonKey: () => (
+ NEXT_PUBLIC_SUPABASE_ANON_KEY
+ ),
+ })}
{isDev ? (
) : (
)}
diff --git a/src/components/dashboard/DashboardEmptyState.tsx b/src/components/dashboard/DashboardEmptyState.tsx
index fb223e9..eba0a7b 100644
--- a/src/components/dashboard/DashboardEmptyState.tsx
+++ b/src/components/dashboard/DashboardEmptyState.tsx
@@ -1,24 +1,25 @@
"use client";
import { FolderOpen } from "lucide-react";
+import { useTranslations } from "next-intl";
import { NewProjectMenu } from "@/components/dashboard/NewProjectMenu";
export function DashboardEmptyState() {
+ const t = useTranslations("auto.componentsDashboardDashboardEmptyState");
return (
- No projects yet
+ {t("title")}
- Create a video, image, or trim project to see it here. Everything you
- save appears in this workspace.
+ {t("description")}
diff --git a/src/components/dashboard/DashboardPlanBadge.tsx b/src/components/dashboard/DashboardPlanBadge.tsx
index f53b528..1568091 100644
--- a/src/components/dashboard/DashboardPlanBadge.tsx
+++ b/src/components/dashboard/DashboardPlanBadge.tsx
@@ -1,4 +1,5 @@
import Link from "next/link";
+import { getTranslations } from "next-intl/server";
import { Button } from "@/components/ui/button";
import { getUserProfile } from "@/lib/profiles";
@@ -16,6 +17,7 @@ interface DashboardPlanBadgeProps {
}
export async function DashboardPlanBadge({ userId }: DashboardPlanBadgeProps) {
+ const t = await getTranslations("auto.componentsDashboardDashboardPlanBadge");
const profile = await getUserProfile(userId);
return (
@@ -30,7 +32,7 @@ export async function DashboardPlanBadge({ userId }: DashboardPlanBadgeProps) {
{profile.plan !== "business" ? (
) : null}
>
diff --git a/src/components/dashboard/DashboardProjectsSection.tsx b/src/components/dashboard/DashboardProjectsSection.tsx
index 6765ab2..934adcd 100644
--- a/src/components/dashboard/DashboardProjectsSection.tsx
+++ b/src/components/dashboard/DashboardProjectsSection.tsx
@@ -1,6 +1,7 @@
"use client";
import { useMemo, useState } from "react";
+import { useTranslations } from "next-intl";
import { DashboardEmptyState } from "@/components/dashboard/DashboardEmptyState";
import { DashboardTopBar } from "@/components/dashboard/DashboardTopBar";
@@ -19,6 +20,7 @@ export function DashboardProjectsSection({
projects = [],
isLoading = false,
}: DashboardProjectsSectionProps) {
+ const t = useTranslations("auto.componentsDashboardDashboardProjectsSection");
const [searchQuery, setSearchQuery] = useState("");
const filteredProjects = useMemo(() => {
@@ -42,7 +44,7 @@ export function DashboardProjectsSection({
- Recent Projects
+ {t("recentProjects")}
{showEmpty && (
@@ -62,10 +64,10 @@ export function DashboardProjectsSection({
{showNoResults && (
- No projects match your search
+ {t("noResultsTitle")}
- Try a different keyword or clear the search bar.
+ {t("noResultsDescription")}
)}
diff --git a/src/components/dashboard/DashboardSidebar.tsx b/src/components/dashboard/DashboardSidebar.tsx
index addb099..29ef593 100644
--- a/src/components/dashboard/DashboardSidebar.tsx
+++ b/src/components/dashboard/DashboardSidebar.tsx
@@ -1,5 +1,6 @@
import Link from "next/link";
import { Suspense } from "react";
+import { getTranslations } from "next-intl/server";
import { LogoMark } from "@/components/ui/LogoMark";
import {
@@ -26,11 +27,12 @@ function getInitials(email: string, name?: string | null): string {
return email.slice(0, 2).toUpperCase();
}
-export function DashboardSidebar({
+export async function DashboardSidebar({
userEmail,
userName,
userId,
}: DashboardSidebarProps) {
+ const t = await getTranslations("auto.componentsDashboardDashboardSidebar");
const initials = getInitials(userEmail, userName);
return (
@@ -52,7 +54,7 @@ export function DashboardSidebar({
- Current plan
+ {t("currentPlan")}
}>
@@ -78,7 +80,7 @@ export function DashboardSidebar({
type="submit"
className="w-full rounded-lg px-3 py-2 text-left text-sm text-neutral-600 transition-colors hover:bg-neutral-50 hover:text-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-600 focus-visible:ring-offset-2"
>
- Sign out
+ {t("signOut")}
diff --git a/src/components/dashboard/DashboardSidebarNav.tsx b/src/components/dashboard/DashboardSidebarNav.tsx
index a35e7cb..08f6c8b 100644
--- a/src/components/dashboard/DashboardSidebarNav.tsx
+++ b/src/components/dashboard/DashboardSidebarNav.tsx
@@ -2,6 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
+import { useTranslations } from "next-intl";
import {
FolderOpen,
LayoutTemplate,
@@ -12,17 +13,18 @@ import {
import { cn } from "@/lib/utils";
const navItems = [
- { label: "My Projects", href: "/dashboard", icon: FolderOpen },
- { label: "Templates", href: "/templates", icon: LayoutTemplate },
- { label: "Upgrade", href: "/#pricing", icon: Zap },
- { label: "Settings", href: "/dashboard/settings", icon: Settings },
+ { labelKey: "myProjects", href: "/dashboard", icon: FolderOpen },
+ { labelKey: "templates", href: "/templates", icon: LayoutTemplate },
+ { labelKey: "upgrade", href: "/#pricing", icon: Zap },
+ { labelKey: "settings", href: "/dashboard/settings", icon: Settings },
] as const;
export function DashboardSidebarNav() {
const pathname = usePathname();
+ const t = useTranslations("auto.componentsDashboardDashboardSidebarNav");
return (
-
)}
@@ -127,13 +129,13 @@ export function SettingsBilling({ plan }: SettingsBillingProps) {
{cancelled && (
- Your plan has been cancelled. You'll keep access until the end of your billing period.
+ {t("cancelledNotice")}
)}
{!isPaid && (
- Upgrade to unlock unlimited projects, 4K export, and premium templates.
+ {t("upgradeHint")}
)}
diff --git a/src/components/dashboard/settings/SettingsNotifications.tsx b/src/components/dashboard/settings/SettingsNotifications.tsx
index 27427d2..4c52f75 100644
--- a/src/components/dashboard/settings/SettingsNotifications.tsx
+++ b/src/components/dashboard/settings/SettingsNotifications.tsx
@@ -1,42 +1,44 @@
"use client";
import { useState } from "react";
+import { useTranslations } from "next-intl";
interface Toggle {
id: string;
- label: string;
- description: string;
+ labelKey: string;
+ descriptionKey: string;
defaultOn: boolean;
}
const TOGGLES: Toggle[] = [
{
id: "render-complete",
- label: "Render complete",
- description: "Get notified when your video export finishes.",
+ labelKey: "renderCompleteLabel",
+ descriptionKey: "renderCompleteDescription",
defaultOn: true,
},
{
id: "project-shared",
- label: "Project shared with you",
- description: "When a team member shares a project.",
+ labelKey: "projectSharedLabel",
+ descriptionKey: "projectSharedDescription",
defaultOn: true,
},
{
id: "weekly-digest",
- label: "Weekly digest",
- description: "Summary of new templates and platform updates.",
+ labelKey: "weeklyDigestLabel",
+ descriptionKey: "weeklyDigestDescription",
defaultOn: false,
},
{
id: "product-news",
- label: "Product news",
- description: "New features, tips, and announcements.",
+ labelKey: "productNewsLabel",
+ descriptionKey: "productNewsDescription",
defaultOn: false,
},
];
export function SettingsNotifications() {
+ const t = useTranslations("auto.componentsDashboardSettingsSettingsNotifications");
const [prefs, setPrefs] = useState
>(
Object.fromEntries(TOGGLES.map((t) => [t.id, t.defaultOn]))
);
@@ -55,15 +57,15 @@ export function SettingsNotifications() {
return (
-
Notifications
-
Choose which emails you receive from FlatRender.
+
{t("title")}
+
{t("subtitle")}
{TOGGLES.map((item) => (
-
{item.label}
-
{item.description}
+
{t(item.labelKey)}
+
{t(item.descriptionKey)}
- {saved &&
Saved!}
+ {saved &&
{t("saved")}}
);
diff --git a/src/components/dashboard/settings/SettingsProfile.tsx b/src/components/dashboard/settings/SettingsProfile.tsx
index 0daabec..54628e0 100644
--- a/src/components/dashboard/settings/SettingsProfile.tsx
+++ b/src/components/dashboard/settings/SettingsProfile.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
+import { useTranslations } from "next-intl";
import { User } from "lucide-react";
interface SettingsProfileProps {
@@ -9,6 +10,7 @@ interface SettingsProfileProps {
}
export function SettingsProfile({ email, displayName }: SettingsProfileProps) {
+ const t = useTranslations("auto.componentsDashboardSettingsSettingsProfile");
const [name, setName] = useState(displayName ?? "");
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
@@ -27,12 +29,12 @@ export function SettingsProfile({ email, displayName }: SettingsProfileProps) {
});
const data = (await res.json().catch(() => null)) as { error?: string } | null;
if (!res.ok) {
- setMessage({ type: "error", text: data?.error ?? "Could not update profile." });
+ setMessage({ type: "error", text: data?.error ?? t("updateFailed") });
} else {
- setMessage({ type: "success", text: "Profile updated successfully." });
+ setMessage({ type: "success", text: t("updateSuccess") });
}
} catch {
- setMessage({ type: "error", text: "Network error. Please try again." });
+ setMessage({ type: "error", text: t("networkError") });
} finally {
setSaving(false);
}
@@ -40,8 +42,8 @@ export function SettingsProfile({ email, displayName }: SettingsProfileProps) {
return (
-
Profile
-
Your public name and account email.
+
{t("title")}
+
{t("subtitle")}
@@ -56,25 +58,25 @@ export function SettingsProfile({ email, displayName }: SettingsProfileProps) {
diff --git a/src/components/dashboard/settings/SettingsSecurity.tsx b/src/components/dashboard/settings/SettingsSecurity.tsx
index c4ec7f3..c1cce47 100644
--- a/src/components/dashboard/settings/SettingsSecurity.tsx
+++ b/src/components/dashboard/settings/SettingsSecurity.tsx
@@ -1,9 +1,11 @@
"use client";
import { useState } from "react";
+import { useTranslations } from "next-intl";
import { Eye, EyeOff } from "lucide-react";
export function SettingsSecurity() {
+ const t = useTranslations("auto.componentsDashboardSettingsSettingsSecurity");
const [current, setCurrent] = useState("");
const [next, setNext] = useState("");
const [confirm, setConfirm] = useState("");
@@ -16,11 +18,11 @@ export function SettingsSecurity() {
setMessage(null);
if (next.length < 8) {
- setMessage({ type: "error", text: "New password must be at least 8 characters." });
+ setMessage({ type: "error", text: t("errorMinLength") });
return;
}
if (next !== confirm) {
- setMessage({ type: "error", text: "Passwords do not match." });
+ setMessage({ type: "error", text: t("errorMismatch") });
return;
}
@@ -35,13 +37,13 @@ export function SettingsSecurity() {
});
const data = (await res.json().catch(() => null)) as { error?: string } | null;
if (!res.ok) {
- setMessage({ type: "error", text: data?.error ?? "Could not change password." });
+ setMessage({ type: "error", text: data?.error ?? t("errorChangeFailed") });
} else {
- setMessage({ type: "success", text: "Password changed successfully." });
+ setMessage({ type: "success", text: t("changeSuccess") });
setCurrent(""); setNext(""); setConfirm("");
}
} catch {
- setMessage({ type: "error", text: "Network error. Please try again." });
+ setMessage({ type: "error", text: t("networkError") });
} finally {
setSaving(false);
}
@@ -64,7 +66,7 @@ export function SettingsSecurity() {
type="button"
onClick={() => setShowPw((v) => !v)}
className="absolute inset-y-0 right-2 flex items-center text-neutral-400 hover:text-neutral-600"
- aria-label={showPw ? "Hide password" : "Show password"}
+ aria-label={showPw ? t("hidePassword") : t("showPassword")}
>
{showPw ?
:
}
@@ -75,13 +77,13 @@ export function SettingsSecurity() {
return (
-
Security
-
Change your account password.
+
{t("title")}
+
{t("subtitle")}
diff --git a/src/components/image-editor/AiRemoveBgModal.tsx b/src/components/image-editor/AiRemoveBgModal.tsx
index ebd91ce..2b90c21 100644
--- a/src/components/image-editor/AiRemoveBgModal.tsx
+++ b/src/components/image-editor/AiRemoveBgModal.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
+import { useTranslations } from "next-intl";
import { Loader2, Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -19,6 +20,7 @@ import {
} from "@/lib/image-editor-store";
export function AiRemoveBgModal() {
+ const t = useTranslations("auto.componentsImageEditorAiRemoveBgModal");
const isOpen = useImageEditorStore((s) => s.isAiModalOpen);
const setAiModalOpen = useImageEditorStore((s) => s.setAiModalOpen);
const replaceBaseImage = useImageEditorStore((s) => s.replaceBaseImage);
@@ -29,7 +31,7 @@ export function AiRemoveBgModal() {
const stage = getImageEditorStage();
const base = getBaseImageLayer({ layers });
if (!stage || !base) {
- toast({ title: "Open an image first." });
+ toast({ title: t("openImageFirst") });
return;
}
@@ -48,15 +50,15 @@ export function AiRemoveBgModal() {
};
if (!response.ok || !payload.image) {
- toast({ title: payload.error ?? "Background removal failed." });
+ toast({ title: payload.error ?? t("removalFailed") });
return;
}
replaceBaseImage(payload.image);
- toast({ title: "Background removed!" });
+ toast({ title: t("backgroundRemoved") });
setAiModalOpen(false);
} catch {
- toast({ title: "Could not reach background removal service." });
+ toast({ title: t("serviceUnreachable") });
} finally {
setIsLoading(false);
}
@@ -68,12 +70,9 @@ export function AiRemoveBgModal() {
- AI Background Removal
+ {t("title")}
-
- Remove the background from your base image. The result replaces the
- background layer with a transparent PNG.
-
+ {t("description")}
diff --git a/src/components/image-editor/ImageCropControls.tsx b/src/components/image-editor/ImageCropControls.tsx
index 02e68f6..7dc5c3b 100644
--- a/src/components/image-editor/ImageCropControls.tsx
+++ b/src/components/image-editor/ImageCropControls.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
+import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import type { ImageCropAspectRatio } from "@/lib/image-editor-types";
@@ -16,6 +17,7 @@ const ASPECT_OPTIONS: { id: ImageCropAspectRatio; label: string }[] = [
];
export function ImageCropControls() {
+ const t = useTranslations("auto.componentsImageEditorImageCropControls");
const [applying, setApplying] = useState(false);
const activeTool = useImageEditorStore((s) => s.activeTool);
const cropAspectRatio = useImageEditorStore((s) => s.cropAspectRatio);
@@ -49,7 +51,7 @@ export function ImageCropControls() {
: "border-gray-700 bg-gray-800 text-gray-200 hover:border-gray-600"
)}
>
- {option.label}
+ {option.id === "free" ? t("aspectFree") : option.label}
))}
@@ -62,7 +64,7 @@ export function ImageCropControls() {
onClick={cancelCrop}
disabled={applying}
>
- Cancel
+ {t("cancel")}
diff --git a/src/components/image-editor/ImageEditorRightPanel.tsx b/src/components/image-editor/ImageEditorRightPanel.tsx
index 4bc27e5..b452ebd 100644
--- a/src/components/image-editor/ImageEditorRightPanel.tsx
+++ b/src/components/image-editor/ImageEditorRightPanel.tsx
@@ -1,5 +1,7 @@
"use client";
+import { useTranslations } from "next-intl";
+
import { AdjustPanel } from "@/components/image-editor/panels/AdjustPanel";
import { FiltersPanel } from "@/components/image-editor/panels/FiltersPanel";
import { LayersPanel } from "@/components/image-editor/panels/LayersPanel";
@@ -7,20 +9,21 @@ import type { ImagePanelTab } from "@/lib/image-editor-types";
import { useImageEditorStore } from "@/lib/image-editor-store";
import { cn } from "@/lib/utils";
-const TABS: { id: ImagePanelTab; label: string }[] = [
- { id: "adjust", label: "Adjust" },
- { id: "filters", label: "Filters" },
- { id: "layers", label: "Layers" },
+const TAB_IDS: { id: ImagePanelTab; labelKey: string }[] = [
+ { id: "adjust", labelKey: "tabAdjust" },
+ { id: "filters", labelKey: "tabFilters" },
+ { id: "layers", labelKey: "tabLayers" },
];
export function ImageEditorRightPanel() {
+ const t = useTranslations("auto.componentsImageEditorImageEditorRightPanel");
const activePanelTab = useImageEditorStore((s) => s.activePanelTab);
const setActivePanelTab = useImageEditorStore((s) => s.setActivePanelTab);
return (
@@ -108,11 +110,13 @@ export function ImageEditorTopBar({
onClick={() => setExportOpen((v) => !v)}
>
- Export
+ {t("export")}
{exportOpen ? (
-
Format
+
+ {t("format")}
+
{(["png", "jpg", "webp"] as ExportImageFormat[]).map((fmt) => (
) : null}
diff --git a/src/components/image-editor/panels/AdjustPanel.tsx b/src/components/image-editor/panels/AdjustPanel.tsx
index f99a65f..be255c3 100644
--- a/src/components/image-editor/panels/AdjustPanel.tsx
+++ b/src/components/image-editor/panels/AdjustPanel.tsx
@@ -1,35 +1,38 @@
"use client";
+import { useTranslations } from "next-intl";
+
import { Slider } from "@/components/ui/slider";
import { useImageEditorStore } from "@/lib/image-editor-store";
const SLIDERS = [
- { key: "brightness" as const, label: "Brightness", min: -100, max: 100 },
- { key: "contrast" as const, label: "Contrast", min: -100, max: 100 },
- { key: "saturation" as const, label: "Saturation", min: -100, max: 100 },
- { key: "hue" as const, label: "Hue", min: -180, max: 180 },
- { key: "blur" as const, label: "Blur", min: 0, max: 20 },
- { key: "sharpen" as const, label: "Sharpen", min: 0, max: 10 },
- { key: "vignette" as const, label: "Vignette", min: 0, max: 100 },
+ { key: "brightness" as const, labelKey: "brightness", min: -100, max: 100 },
+ { key: "contrast" as const, labelKey: "contrast", min: -100, max: 100 },
+ { key: "saturation" as const, labelKey: "saturation", min: -100, max: 100 },
+ { key: "hue" as const, labelKey: "hue", min: -180, max: 180 },
+ { key: "blur" as const, labelKey: "blur", min: 0, max: 20 },
+ { key: "sharpen" as const, labelKey: "sharpen", min: 0, max: 10 },
+ { key: "vignette" as const, labelKey: "vignette", min: 0, max: 100 },
];
export function AdjustPanel() {
+ const t = useTranslations("auto.componentsImageEditorPanelsAdjustPanel");
const adjustments = useImageEditorStore((s) => s.adjustments);
const setAdjustments = useImageEditorStore((s) => s.setAdjustments);
const hasBase = useImageEditorStore((s) => s.layers.some((l) => l.type === "image"));
if (!hasBase) {
return (
-
Open an image to use adjustments.
+
{t("emptyState")}
);
}
return (
- {SLIDERS.map(({ key, label, min, max }) => (
+ {SLIDERS.map(({ key, labelKey, min, max }) => (
-
{label}
+
{t(labelKey)}
{adjustments[key]}
diff --git a/src/components/image-editor/panels/FiltersPanel.tsx b/src/components/image-editor/panels/FiltersPanel.tsx
index 2af6d0f..b02349d 100644
--- a/src/components/image-editor/panels/FiltersPanel.tsx
+++ b/src/components/image-editor/panels/FiltersPanel.tsx
@@ -1,16 +1,19 @@
"use client";
+import { useTranslations } from "next-intl";
+
import { FILTER_PRESETS } from "@/lib/image-editor-filters";
import { useImageEditorStore } from "@/lib/image-editor-store";
import { cn } from "@/lib/utils";
export function FiltersPanel() {
+ const t = useTranslations("auto.componentsImageEditorPanelsFiltersPanel");
const activeFilterPreset = useImageEditorStore((s) => s.activeFilterPreset);
const applyFilterPreset = useImageEditorStore((s) => s.applyFilterPreset);
const hasBase = useImageEditorStore((s) => s.layers.some((l) => l.type === "image"));
if (!hasBase) {
- return
Open an image to apply filters.
;
+ return
{t("emptyState")}
;
}
return (
diff --git a/src/components/image-editor/panels/LayersPanel.tsx b/src/components/image-editor/panels/LayersPanel.tsx
index 9d3cc14..40e543d 100644
--- a/src/components/image-editor/panels/LayersPanel.tsx
+++ b/src/components/image-editor/panels/LayersPanel.tsx
@@ -17,6 +17,7 @@ import {
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Eye, EyeOff, GripVertical, Trash2 } from "lucide-react";
+import { useTranslations } from "next-intl";
import type { ImageLayer } from "@/lib/image-editor-types";
import { useImageEditorStore } from "@/lib/image-editor-store";
@@ -38,6 +39,7 @@ function layerIcon(type: ImageLayer["type"]): string {
}
function SortableLayerRow({ layer }: { layer: ImageLayer }) {
+ const t = useTranslations("auto.componentsImageEditorPanelsLayersPanel");
const selectedLayerId = useImageEditorStore((s) => s.selectedLayerId);
const setSelectedLayer = useImageEditorStore((s) => s.setSelectedLayer);
const toggleLayerVisibility = useImageEditorStore((s) => s.toggleLayerVisibility);
@@ -63,7 +65,7 @@ function SortableLayerRow({ layer }: { layer: ImageLayer }) {
@@ -106,6 +108,7 @@ function SortableLayerRow({ layer }: { layer: ImageLayer }) {
}
export function LayersPanel() {
+ const t = useTranslations("auto.componentsImageEditorPanelsLayersPanel");
const layers = useImageEditorStore((s) => s.layers);
const reorderLayers = useImageEditorStore((s) => s.reorderLayers);
@@ -133,7 +136,7 @@ export function LayersPanel() {
};
if (layers.length === 0) {
- return
No layers yet.
;
+ return
{t("emptyState")}
;
}
return (
diff --git a/src/components/image-maker/ImageMakerBeforeAfter.tsx b/src/components/image-maker/ImageMakerBeforeAfter.tsx
index fac2f56..7fe3720 100644
--- a/src/components/image-maker/ImageMakerBeforeAfter.tsx
+++ b/src/components/image-maker/ImageMakerBeforeAfter.tsx
@@ -1,14 +1,20 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+
import { OptimizedImage } from "@/components/ui/optimized-image";
+import { placeholderSrc } from "@/lib/placeholder";
export function ImageMakerBeforeAfter() {
+ const t = useTranslations("auto.componentsImageMakerImageMakerBeforeAfter");
return (
- Before
+ {t("beforeLabel")}
- After
+ {t("afterLabel")}
- AI-enhanced color, layout, and brand styling applied in one click
+ {t("caption")}
);
diff --git a/src/components/image-maker/ImageMakerGallery.tsx b/src/components/image-maker/ImageMakerGallery.tsx
index 0be1bda..3c3000a 100644
--- a/src/components/image-maker/ImageMakerGallery.tsx
+++ b/src/components/image-maker/ImageMakerGallery.tsx
@@ -1,19 +1,23 @@
+import { getTranslations } from "next-intl/server";
+
import { OptimizedImage } from "@/components/ui/optimized-image";
import { SectionReveal } from "@/components/sections/SectionReveal";
+import { placeholderSrc } from "@/lib/placeholder";
import { GALLERY_ITEMS } from "./image-maker-gallery-data";
-export function ImageMakerGallery() {
+export async function ImageMakerGallery() {
+ const t = await getTranslations("auto.componentsImageMakerImageMakerGallery");
+
return (
- Example outputs from creators
+ {t("title")}
- Real-world layouts and styles you can recreate—or use as inspiration
- for your next project.
+ {t("subtitle")}
@@ -25,7 +29,7 @@ export function ImageMakerGallery() {
>
{children};
+}
diff --git a/src/components/layout/NavbarMenuDropdown.tsx b/src/components/layout/NavbarMenuDropdown.tsx
index f36d96c..82deb3a 100644
--- a/src/components/layout/NavbarMenuDropdown.tsx
+++ b/src/components/layout/NavbarMenuDropdown.tsx
@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
+import { useTranslations } from "next-intl";
import { ChevronDown, LayoutGrid } from "lucide-react";
import {
@@ -68,11 +69,14 @@ interface NavbarLearnDropdownProps {
label?: string;
}
-export function NavbarLearnDropdown({ items, label = "Learn" }: NavbarLearnDropdownProps) {
+export function NavbarLearnDropdown({ items, label }: NavbarLearnDropdownProps) {
+ const t = useTranslations("auto.componentsLayoutNavbarMenuDropdown");
+ const resolvedLabel = label ?? t("learn");
+
return (
- {label}
+ {resolvedLabel}
diff --git a/src/components/layout/NavbarMobileMenu.tsx b/src/components/layout/NavbarMobileMenu.tsx
index cb54aac..9064b83 100644
--- a/src/components/layout/NavbarMobileMenu.tsx
+++ b/src/components/layout/NavbarMobileMenu.tsx
@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
+import { useTranslations } from "next-intl";
import { LayoutGrid } from "lucide-react";
import {
@@ -17,11 +18,13 @@ const linkClass =
"flex min-h-11 items-center rounded-lg px-3 text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-900";
export function NavbarMobileMenu({ onNavigate }: NavbarMobileMenuProps) {
+ const t = useTranslations("auto.componentsLayoutNavbarMobileMenu");
+
return (
- Video Maker
+ {t("videoMaker")}
- Image Maker
+ {t("imageMaker")}
- Pricing
+ {t("pricing")}
- Learn
+ {t("learn")}
{LEARN_NAV_ITEMS.map((item) => (
diff --git a/src/components/sections/HeroPreviewCards.tsx b/src/components/sections/HeroPreviewCards.tsx
index c04175c..73bdf6d 100644
--- a/src/components/sections/HeroPreviewCards.tsx
+++ b/src/components/sections/HeroPreviewCards.tsx
@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
+import { useTranslations } from "next-intl";
import { motion, type Variants } from "framer-motion";
import { VideoPlayOverlay } from "@/components/sections/VideoPlayOverlay";
@@ -8,10 +9,10 @@ import { getHeroPreviewVideoSrc } from "@/lib/template-preview-media";
import { cn } from "@/lib/utils";
const previewTemplates = [
- { id: "hero-3d", title: "Factory of 3D Animations" },
- { id: "hero-whiteboard", title: "Whiteboard Animation Toolkit" },
- { id: "hero-explainer", title: "3D Explainer Video Toolkit" },
- { id: "hero-trendy", title: "Trendy Explainer Toolkit" },
+ { id: "hero-3d", titleKey: "template3dTitle" },
+ { id: "hero-whiteboard", titleKey: "templateWhiteboardTitle" },
+ { id: "hero-explainer", titleKey: "templateExplainerTitle" },
+ { id: "hero-trendy", titleKey: "templateTrendyTitle" },
] as const;
const containerVariants: Variants = {
@@ -37,6 +38,8 @@ interface HeroVideoThumbProps {
}
function HeroVideoThumb({ videoSrc, label }: HeroVideoThumbProps) {
+ const t = useTranslations("auto.componentsSectionsHeroPreviewCards");
+
return (
- Made by world-class motion designers
+ {t("heading")}
- {previewTemplates.map((template, index) => (
-
-
-
-
- {template.title}
-
-
-
- ))}
+ {previewTemplates.map((template, index) => {
+ const title = t(template.titleKey);
+ return (
+
+
+
+
+ {title}
+
+
+
+ );
+ })}
);
diff --git a/src/components/sections/PricingAnimatedPrice.tsx b/src/components/sections/PricingAnimatedPrice.tsx
index ac6081a..cf1bf8c 100644
--- a/src/components/sections/PricingAnimatedPrice.tsx
+++ b/src/components/sections/PricingAnimatedPrice.tsx
@@ -1,6 +1,7 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
+import { useTranslations } from "next-intl";
import { cn } from "@/lib/utils";
@@ -22,6 +23,7 @@ export function PricingAnimatedPrice({
size = "default",
}: PricingAnimatedPriceProps) {
const isCompact = size === "compact";
+ const t = useTranslations("auto.componentsSectionsPricingAnimatedPrice");
return (
@@ -53,7 +55,7 @@ export function PricingAnimatedPrice({
${formatPrice(price)}
- / month
+ {t("perMonth")}
);
diff --git a/src/components/sections/PricingBillingToggle.tsx b/src/components/sections/PricingBillingToggle.tsx
index 3b50fce..c432e20 100644
--- a/src/components/sections/PricingBillingToggle.tsx
+++ b/src/components/sections/PricingBillingToggle.tsx
@@ -1,6 +1,7 @@
"use client";
import { motion } from "framer-motion";
+import { useTranslations } from "next-intl";
import type { BillingPeriod } from "@/components/sections/pricing-data";
import { ANNUAL_SAVINGS_PERCENT } from "@/components/sections/pricing-data";
@@ -17,12 +18,13 @@ export function PricingBillingToggle({
onChange,
layoutId = "pricing-billing-pill",
}: PricingBillingToggleProps) {
+ const t = useTranslations("auto.componentsSectionsPricingBillingToggle");
return (
{(["monthly", "annual"] as const).map((period) => {
const isActive = billing === period;
- const label = period === "monthly" ? "Monthly" : "Yearly";
+ const label = period === "monthly" ? t("monthly") : t("yearly");
return (
);
diff --git a/src/components/sections/PricingCard.tsx b/src/components/sections/PricingCard.tsx
index 5f6ebdd..6660094 100644
--- a/src/components/sections/PricingCard.tsx
+++ b/src/components/sections/PricingCard.tsx
@@ -2,6 +2,7 @@
import Link from "next/link";
import { Tag } from "lucide-react";
+import { useTranslations } from "next-intl";
import { PricingAnimatedPrice } from "@/components/sections/PricingAnimatedPrice";
import { PricingCheckoutButton } from "@/components/sections/PricingCheckoutButton";
@@ -22,6 +23,7 @@ export interface PricingCardProps {
}
export function PricingCard({ tier, billing }: PricingCardProps) {
+ const t = useTranslations("auto.componentsSectionsPricingCard");
const price = getDisplayPrice(tier, billing);
const compareAt = getCompareAtPrice(tier, billing);
const highlighted = tier.highlighted ?? false;
@@ -38,7 +40,7 @@ export function PricingCard({ tier, billing }: PricingCardProps) {
>
{highlighted ? (
- Most Popular
+ {t("mostPopular")}
) : null}
diff --git a/src/components/sections/PricingCheckoutButton.tsx b/src/components/sections/PricingCheckoutButton.tsx
index 2fd79ea..0ce920d 100644
--- a/src/components/sections/PricingCheckoutButton.tsx
+++ b/src/components/sections/PricingCheckoutButton.tsx
@@ -2,6 +2,7 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
import { Loader2 } from "lucide-react";
import type { BillingPeriod } from "@/components/sections/pricing-data";
@@ -25,6 +26,7 @@ export function PricingCheckoutButton({
variant = "default",
}: PricingCheckoutButtonProps) {
const router = useRouter();
+ const t = useTranslations("auto.componentsSectionsPricingCheckoutButton");
const [loading, setLoading] = useState(false);
const [error, setError] = useState
(null);
@@ -46,7 +48,7 @@ export function PricingCheckoutButton({
router.push(`/auth?tab=sign-up&plan=${plan}`);
return;
}
- throw new Error(data.error ?? "Checkout failed.");
+ throw new Error(data.error ?? t("checkoutFailed"));
}
if (data.url) {
@@ -54,12 +56,12 @@ export function PricingCheckoutButton({
return;
}
- throw new Error("No checkout URL returned.");
+ throw new Error(t("noCheckoutUrl"));
} catch (checkoutError) {
setError(
checkoutError instanceof Error
? checkoutError.message
- : "Checkout failed."
+ : t("checkoutFailed")
);
} finally {
setLoading(false);
diff --git a/src/components/sections/PricingCompareTable.tsx b/src/components/sections/PricingCompareTable.tsx
index 0168e0b..7439321 100644
--- a/src/components/sections/PricingCompareTable.tsx
+++ b/src/components/sections/PricingCompareTable.tsx
@@ -2,6 +2,7 @@
import { Fragment } from "react";
import Link from "next/link";
+import { useTranslations } from "next-intl";
import { PricingAnimatedPrice } from "@/components/sections/PricingAnimatedPrice";
import { PricingBillingToggle } from "@/components/sections/PricingBillingToggle";
@@ -62,6 +63,7 @@ function PlanHeaderCell({
tier: PricingTier;
billing: BillingPeriod;
}) {
+ const t = useTranslations("auto.componentsSectionsPricingCompareTable");
const highlighted = tier.highlighted ?? false;
const isStripePlan = tier.id === "pro" || tier.id === "business";
@@ -74,7 +76,7 @@ function PlanHeaderCell({
>
{highlighted ? (
- Most Popular
+ {t("mostPopular")}
) : (
@@ -118,6 +120,7 @@ export function PricingCompareTable({
billing,
onBillingChange,
}: PricingCompareTableProps) {
+ const t = useTranslations("auto.componentsSectionsPricingCompareTable");
const lite = PRICING_TIERS.find((t) => t.id === "lite");
const pro = PRICING_TIERS.find((t) => t.id === "pro");
const business = PRICING_TIERS.find((t) => t.id === "business");
@@ -131,7 +134,7 @@ export function PricingCompareTable({
- Compare Plans & Features
+ {t("compareHeading")}
- Save up to {COMPARE_ANNUAL_SAVINGS_BADGE}%
+ {t("saveUpTo", { percent: COMPARE_ANNUAL_SAVINGS_BADGE })}
|
diff --git a/src/components/sections/PricingCreditsBanner.tsx b/src/components/sections/PricingCreditsBanner.tsx
index 5dd143d..ab7e7a0 100644
--- a/src/components/sections/PricingCreditsBanner.tsx
+++ b/src/components/sections/PricingCreditsBanner.tsx
@@ -1,11 +1,16 @@
+"use client";
+
import { Zap } from "lucide-react";
+import { useTranslations } from "next-intl";
export function PricingCreditsBanner() {
+ const t = useTranslations("auto.componentsSectionsPricingCreditsBanner");
+
return (
- You can refill AI credits anytime with an active plan
+ {t("refillCredits")}
);
diff --git a/src/components/sections/PricingFeatureList.tsx b/src/components/sections/PricingFeatureList.tsx
index b793bf6..4acad46 100644
--- a/src/components/sections/PricingFeatureList.tsx
+++ b/src/components/sections/PricingFeatureList.tsx
@@ -1,4 +1,7 @@
+"use client";
+
import { Check, Info } from "lucide-react";
+import { useTranslations } from "next-intl";
import type { PricingFeature } from "@/components/sections/pricing-data";
@@ -11,6 +14,8 @@ export function PricingFeatureList({
heading,
features,
}: PricingFeatureListProps) {
+ const t = useTranslations("auto.componentsSectionsPricingFeatureList");
+
return (
{heading ? (
@@ -36,7 +41,7 @@ export function PricingFeatureList({
{feature.info ? (
) : null}
diff --git a/src/components/sections/PricingFreeBanner.tsx b/src/components/sections/PricingFreeBanner.tsx
index 45f0acd..22868d5 100644
--- a/src/components/sections/PricingFreeBanner.tsx
+++ b/src/components/sections/PricingFreeBanner.tsx
@@ -1,17 +1,21 @@
+"use client";
+
import Link from "next/link";
+import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
export function PricingFreeBanner() {
+ const t = useTranslations("auto.componentsSectionsPricingFreeBanner");
+
return (
- Always Free to Try
+ {t("title")}
- Explore CreatorStudio with a Free plan — create HD videos with a
- watermark, try basic features, and experiment before you subscribe.
+ {t("description")}
);
diff --git a/src/components/sections/TemplateCard.tsx b/src/components/sections/TemplateCard.tsx
index 53ce202..df4526e 100644
--- a/src/components/sections/TemplateCard.tsx
+++ b/src/components/sections/TemplateCard.tsx
@@ -3,6 +3,7 @@
import { useCallback, useState } from "react";
import Link from "next/link";
import { AnimatePresence, motion } from "framer-motion";
+import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { OptimizedImage } from "@/components/ui/optimized-image";
@@ -40,9 +41,12 @@ export function TemplateCard({
priority = false,
onUseTemplate,
isUsingTemplate = false,
- useTemplateLabel = "Use Template",
- openingLabel = "Opening…",
+ useTemplateLabel,
+ openingLabel,
}: TemplateCardProps) {
+ const t = useTranslations("auto.componentsSectionsTemplateCard");
+ const resolvedUseTemplateLabel = useTemplateLabel ?? t("useTemplateLabel");
+ const resolvedOpeningLabel = openingLabel ?? t("openingLabel");
const [isHovered, setIsHovered] = useState(false);
const seed = previewSeed ?? name;
const videoSrc = previewVideoUrl ?? getTemplatePreviewVideoSrc(seed);
@@ -64,7 +68,7 @@ export function TemplateCard({
- {isUsingTemplate ? openingLabel : useTemplateLabel}
+ {isUsingTemplate ? resolvedOpeningLabel : resolvedUseTemplateLabel}
) : null}
diff --git a/src/components/sections/TestimonialCard.tsx b/src/components/sections/TestimonialCard.tsx
index 7f9a612..aa5ce46 100644
--- a/src/components/sections/TestimonialCard.tsx
+++ b/src/components/sections/TestimonialCard.tsx
@@ -1,4 +1,7 @@
+"use client";
+
import { Star } from "lucide-react";
+import { useTranslations } from "next-intl";
import { cn } from "@/lib/utils";
@@ -13,6 +16,7 @@ export function TestimonialCard({
testimonial,
className,
}: TestimonialCardProps) {
+ const t = useTranslations("auto.componentsSectionsTestimonialCard");
const { name, role, company, quote, initials } = testimonial;
return (
@@ -22,7 +26,7 @@ export function TestimonialCard({
className
)}
>
- Rated 5 out of 5 stars
+ {t("ratingLabel")}
{Array.from({ length: 5 }).map((_, index) => (
@@ -37,7 +39,7 @@ export function AddSceneMenu({ onAddBlank, variant = "footer" }: AddSceneMenuPro
className="flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-[#2a2d3e] bg-[#1a1d2e]/50 px-3 py-2 text-xs font-medium text-gray-300 transition-colors hover:border-[#3d4260] hover:bg-[#1f2234] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4c6ef5]"
>
- Add Scene
+ {t("addScene")}
)}
@@ -51,14 +53,14 @@ export function AddSceneMenu({ onAddBlank, variant = "footer" }: AddSceneMenuPro
className="flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm text-gray-200 hover:bg-[#252938] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4c6ef5]"
>
- Blank Scene
+ {t("blankScene")}
- From Template
+ {t("fromTemplate")}
diff --git a/src/components/studio/DraggableSceneItem.tsx b/src/components/studio/DraggableSceneItem.tsx
index 906ecc6..6e3402d 100644
--- a/src/components/studio/DraggableSceneItem.tsx
+++ b/src/components/studio/DraggableSceneItem.tsx
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
+import { useTranslations } from "next-intl";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { GripVertical } from "lucide-react";
@@ -28,6 +29,7 @@ export function DraggableSceneItem({
onDuplicate,
onRename,
}: DraggableSceneItemProps) {
+ const t = useTranslations("auto.componentsStudioDraggableSceneItem");
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(scene.name);
const inputRef = useRef(null);
@@ -80,7 +82,7 @@ export function DraggableSceneItem({
type="button"
ref={setActivatorNodeRef}
className="flex w-6 shrink-0 cursor-grab items-center justify-center text-gray-500 hover:text-gray-300 active:cursor-grabbing"
- aria-label={`Drag scene ${scene.name}`}
+ aria-label={t("dragScene", { name: scene.name })}
{...attributes}
{...listeners}
>
@@ -129,7 +131,7 @@ export function DraggableSceneItem({
}
}}
className="mt-1.5 w-full rounded border border-[#2a2d3e] bg-[#1a1d2e] px-1.5 py-0.5 text-xs text-white focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#4c6ef5]"
- aria-label="Scene name"
+ aria-label={t("sceneNameLabel")}
/>
) : (
- Saving…
+ {t("saving")}
);
}
@@ -44,7 +47,7 @@ export function ProjectSaveIndicator({
aria-live="polite"
>
- Saved
+ {t("saved")}
);
}
@@ -55,14 +58,14 @@ export function ProjectSaveIndicator({
className={cn("text-xs font-medium text-gray-400", className)}
aria-live="polite"
>
- Local save
+ {t("localSave")}
);
}
return (
- Save failed
+ {t("saveFailed")}
{onRetry ? (
) : null}
diff --git a/src/components/studio/PropertiesPanel.tsx b/src/components/studio/PropertiesPanel.tsx
index 408c411..7c01be6 100644
--- a/src/components/studio/PropertiesPanel.tsx
+++ b/src/components/studio/PropertiesPanel.tsx
@@ -1,6 +1,7 @@
"use client";
import { MousePointer2, SlidersHorizontal } from "lucide-react";
+import { useTranslations } from "next-intl";
import { CommonLayerControls } from "@/components/studio/properties/CommonLayerControls";
import { ImageLayerProperties } from "@/components/studio/properties/ImageLayerProperties";
@@ -14,6 +15,7 @@ export interface PropertiesPanelProps {
}
export function PropertiesPanel({ className }: PropertiesPanelProps) {
+ const t = useTranslations("auto.componentsStudioPropertiesPanel");
const scenes = useStudioStore((state) => state.scenes);
const activeSceneId = useStudioStore((state) => state.activeSceneId);
const selectedLayerId = useStudioStore((state) => state.selectedLayerId);
@@ -33,7 +35,7 @@ export function PropertiesPanel({ className }: PropertiesPanelProps) {
>
- Properties
+ {t("title")}
@@ -44,14 +46,14 @@ export function PropertiesPanel({ className }: PropertiesPanelProps) {
- Select a layer to edit properties
+ {t("emptyState")}
) : (
- {layer.type} layer
+ {t("layerLabel", { type: layer.type })}
{layer.type === "text" ?
: null}
{layer.type === "image" ? (
diff --git a/src/components/studio/RenderModal.tsx b/src/components/studio/RenderModal.tsx
index 687e7f0..0a8f6a6 100644
--- a/src/components/studio/RenderModal.tsx
+++ b/src/components/studio/RenderModal.tsx
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
+import { useTranslations } from "next-intl";
import { Download, Link2, Loader2, RefreshCw } from "lucide-react";
import { apiFetch } from "@/lib/api/fetch";
@@ -49,6 +50,7 @@ export function RenderModal({
scenes,
preset = null,
}: RenderModalProps) {
+ const t = useTranslations("auto.componentsStudioRenderModal");
const [resolution, setResolution] =
useState
("1080p");
const [fps, setFps] = useState(30);
@@ -93,13 +95,13 @@ export function RenderModal({
if (!response.ok) {
setJobStatus("failed");
- setErrorMessage("Could not fetch render status.");
+ setErrorMessage(t("errorFetchStatus"));
return;
}
setProgress(data.progress ?? 0);
setProgressMessage(
- data.progressMessage ?? `Rendering… ${data.progress}%`
+ data.progressMessage ?? t("renderingProgress", { progress: data.progress })
);
if (data.previewB64) setPreviewB64(data.previewB64);
@@ -112,18 +114,18 @@ export function RenderModal({
if (data.status === "failed") {
setJobStatus("failed");
- setErrorMessage(data.errorMessage ?? "Render failed.");
+ setErrorMessage(data.errorMessage ?? t("errorRenderFailed"));
}
} catch {
setJobStatus("failed");
- setErrorMessage("Network error while polling status.");
+ setErrorMessage(t("errorNetworkPolling"));
}
};
poll();
const intervalId = window.setInterval(poll, 3000);
return () => window.clearInterval(intervalId);
- }, [jobStatus, jobId]);
+ }, [jobStatus, jobId, t]);
const startRender = async () => {
setJobStatus("submitting");
@@ -150,17 +152,17 @@ export function RenderModal({
if (!response.ok || !data.jobId) {
setJobStatus("failed");
- setErrorMessage(data.error ?? "Failed to start render.");
+ setErrorMessage(data.error ?? t("errorStartRender"));
return;
}
setJobId(data.jobId);
setJobStatus("polling");
setProgress(0);
- setProgressMessage("Queued for rendering…");
+ setProgressMessage(t("queued"));
} catch {
setJobStatus("failed");
- setErrorMessage("Could not reach render API.");
+ setErrorMessage(t("errorReachApi"));
}
};
@@ -170,17 +172,17 @@ export function RenderModal({
- Progress
+ {t("progress")}
{progress}%
@@ -261,7 +263,7 @@ export function RenderModal({
- Resolution
+ {t("resolution")}
{RESOLUTIONS.map((item) => (
@@ -282,13 +284,13 @@ export function RenderModal({
-
Format
+
{t("format")}
MP4
-
FPS
+
{t("fps")}
{FPS_OPTIONS.map((item) => (
)}
diff --git a/src/components/studio/SceneBrowserCard.tsx b/src/components/studio/SceneBrowserCard.tsx
index 138586b..ee07584 100644
--- a/src/components/studio/SceneBrowserCard.tsx
+++ b/src/components/studio/SceneBrowserCard.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
+import { useTranslations } from "next-intl";
import { Check, Clock, ImageIcon, User } from "lucide-react";
import { getScenePreviewVideoSrc } from "@/lib/template-preview-media";
@@ -52,6 +53,7 @@ export function SceneBrowserCard({
isSelected,
onToggle,
}: SceneBrowserCardProps) {
+ const t = useTranslations("auto.componentsStudioSceneBrowserCard");
const [hovered, setHovered] = useState(false);
const videoSrc = getScenePreviewVideoSrc(scene.category, scene.id);
@@ -141,7 +143,7 @@ export function SceneBrowserCard({
{!isSelected && hovered && (
- Select
+ {t("selectCta")}
)}
diff --git a/src/components/studio/SceneBrowserModal.tsx b/src/components/studio/SceneBrowserModal.tsx
index 8ba267a..a6c52cb 100644
--- a/src/components/studio/SceneBrowserModal.tsx
+++ b/src/components/studio/SceneBrowserModal.tsx
@@ -1,6 +1,7 @@
"use client";
import { useMemo, useState } from "react";
+import { useTranslations } from "next-intl";
import { LayoutGrid, Search, X } from "lucide-react";
import { SceneBrowserCard } from "@/components/studio/SceneBrowserCard";
@@ -34,6 +35,7 @@ export function SceneBrowserModal({
onOpenChange,
onScenesAdd,
}: SceneBrowserModalProps) {
+ const t = useTranslations("auto.componentsStudioSceneBrowserModal");
const [categoryId, setCategoryId] = useState
("all");
const [mediaFilter, setMediaFilter] = useState("all");
const [search, setSearch] = useState("");
@@ -92,13 +94,13 @@ export function SceneBrowserModal({
- Select Scenes
+ {t("title")}
@@ -111,9 +113,9 @@ export function SceneBrowserModal({
onValueChange={(v) => setMediaFilter(v as SceneBrowserMediaFilter)}
>
- All
- Video
- Photo
+ {t("filterAll")}
+ {t("filterVideo")}
+ {t("filterPhoto")}
@@ -121,7 +123,7 @@ export function SceneBrowserModal({
setSearch(e.target.value)}
className="h-9 w-full rounded-lg border border-gray-200 bg-white pl-9 pr-3 text-sm placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600"
@@ -160,7 +162,7 @@ export function SceneBrowserModal({
{filteredScenes.length === 0 ? (
- No scenes match your filters.
+ {t("emptyState")}
) : (
@@ -184,7 +186,7 @@ export function SceneBrowserModal({
{selectedCount > 0 && (
{selectedCount}{" "}
- scene{selectedCount !== 1 ? "s" : ""} selected
+ {t("selectedSuffix", { count: selectedCount })}
)}
{selectedCount > 0 && (
@@ -193,7 +195,7 @@ export function SceneBrowserModal({
onClick={deselectAll}
className="text-sm text-gray-500 underline hover:text-gray-700 focus-visible:outline-none"
>
- Deselect All
+ {t("deselectAll")}
)}
@@ -205,7 +207,7 @@ export function SceneBrowserModal({
onClick={handleClose}
className="border-gray-300 text-gray-700 hover:bg-gray-50"
>
- Cancel
+ {t("cancel")}
diff --git a/src/components/studio/SceneItemActions.tsx b/src/components/studio/SceneItemActions.tsx
index 222d864..a7cab4b 100644
--- a/src/components/studio/SceneItemActions.tsx
+++ b/src/components/studio/SceneItemActions.tsx
@@ -1,6 +1,7 @@
"use client";
import { Copy, Trash2 } from "lucide-react";
+import { useTranslations } from "next-intl";
interface SceneItemActionsProps {
sceneName: string;
@@ -15,6 +16,7 @@ export function SceneItemActions({
onDuplicate,
onDelete,
}: SceneItemActionsProps) {
+ const t = useTranslations("auto.componentsStudioSceneItemActions");
return (
@@ -39,7 +41,7 @@ export function SceneItemActions({
onDelete();
}}
className="flex h-6 w-6 items-center justify-center rounded bg-[#0f111a]/90 text-gray-300 hover:bg-red-600/90 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4c6ef5]"
- aria-label={`Delete ${sceneName}`}
+ aria-label={t("delete", { sceneName })}
>
diff --git a/src/components/studio/SceneTransitionPicker.tsx b/src/components/studio/SceneTransitionPicker.tsx
index c111f5d..c194d61 100644
--- a/src/components/studio/SceneTransitionPicker.tsx
+++ b/src/components/studio/SceneTransitionPicker.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
+import { useTranslations } from "next-intl";
import {
Popover,
PopoverContent,
@@ -19,6 +20,7 @@ export function SceneTransitionPicker({
transitionType,
onChange,
}: SceneTransitionPickerProps) {
+ const t = useTranslations("auto.componentsStudioSceneTransitionPicker");
const [open, setOpen] = useState(false);
const activeOption =
SCENE_TRANSITION_OPTIONS.find((option) => option.id === transitionType) ??
@@ -30,8 +32,8 @@ export function SceneTransitionPicker({
- {SHAPE_OPTIONS.map(({ kind, label, icon: Icon, config }) => (
+ {SHAPE_OPTIONS.map(({ kind, labelKey, icon: Icon, config }) => (
))}
diff --git a/src/components/studio/canvas/VideoLayerNode.tsx b/src/components/studio/canvas/VideoLayerNode.tsx
index a889501..9899315 100644
--- a/src/components/studio/canvas/VideoLayerNode.tsx
+++ b/src/components/studio/canvas/VideoLayerNode.tsx
@@ -1,6 +1,7 @@
"use client";
import { Rect, Text } from "react-konva";
+import { useTranslations } from "next-intl";
import type Konva from "konva";
import type { Layer } from "@/lib/studio-types";
@@ -26,9 +27,12 @@ export function VideoLayerNode({
onTransformEnd,
registerNode,
}: VideoLayerNodeProps) {
+ const t = useTranslations("auto.componentsStudioCanvasVideoLayerNode");
const hasVideo = Boolean(getVideoSrc(layer.props));
const fileName =
- typeof layer.props.fileName === "string" ? layer.props.fileName : "Video";
+ typeof layer.props.fileName === "string"
+ ? layer.props.fileName
+ : t("defaultFileName");
return (
<>
@@ -62,7 +66,7 @@ export function VideoLayerNode({
width={layer.width}
height={20}
rotation={layer.rotation}
- text={hasVideo ? fileName : "Video clip"}
+ text={hasVideo ? fileName : t("placeholder")}
fontSize={14}
fill="#E5E7EB"
align="center"
diff --git a/src/components/studio/properties/CommonLayerControls.tsx b/src/components/studio/properties/CommonLayerControls.tsx
index a6ca141..c887d8a 100644
--- a/src/components/studio/properties/CommonLayerControls.tsx
+++ b/src/components/studio/properties/CommonLayerControls.tsx
@@ -1,6 +1,7 @@
"use client";
import { useRef, useState } from "react";
+import { useTranslations } from "next-intl";
import { ArrowDown, ArrowUp, Trash2 } from "lucide-react";
import {
@@ -17,6 +18,7 @@ interface CommonLayerControlsProps {
}
export function CommonLayerControls({ layer }: CommonLayerControlsProps) {
+ const t = useTranslations("auto.componentsStudioPropertiesCommonLayerControls");
const [aspectLocked, setAspectLocked] = useState(false);
const aspectRatioRef = useRef(layer.width / layer.height || 1);
const { update } = useLayerUpdater(layer);
@@ -44,7 +46,7 @@ export function CommonLayerControls({ layer }: CommonLayerControlsProps) {
return (
<>
-
+
update({ rotation })}
/>
-
+
@@ -118,7 +120,7 @@ export function CommonLayerControls({ layer }: CommonLayerControlsProps) {
className="flex w-full items-center justify-center gap-2 rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-xs font-medium text-red-500 transition-colors hover:border-red-300 hover:bg-red-100 hover:text-red-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500"
>
- Delete layer
+ {t("deleteLayer")}
>
);
diff --git a/src/components/studio/properties/ImageLayerProperties.tsx b/src/components/studio/properties/ImageLayerProperties.tsx
index 9a612de..8e9a406 100644
--- a/src/components/studio/properties/ImageLayerProperties.tsx
+++ b/src/components/studio/properties/ImageLayerProperties.tsx
@@ -1,6 +1,7 @@
"use client";
import { useRef, type ChangeEvent } from "react";
+import { useTranslations } from "next-intl";
import { FlipHorizontal, FlipVertical, ImagePlus } from "lucide-react";
import {
@@ -16,6 +17,7 @@ interface ImageLayerPropertiesProps {
}
export function ImageLayerProperties({ layer }: ImageLayerPropertiesProps) {
+ const t = useTranslations("auto.componentsStudioPropertiesImageLayerProperties");
const fileInputRef = useRef(null);
const { update, updateProps } = useLayerUpdater(layer);
const image = getImageProps(layer.props);
@@ -35,9 +37,9 @@ export function ImageLayerProperties({ layer }: ImageLayerPropertiesProps) {
};
return (
-
+
- Flip H
+ {t("flipHorizontal")}
- Replace image
+ {t("replaceImage")}
void;
}) {
+ const t = useTranslations("auto.componentsStudioPropertiesPropertyControls");
return (
diff --git a/src/components/studio/sidebar/ColorsTemplatePreviewCard.tsx b/src/components/studio/sidebar/ColorsTemplatePreviewCard.tsx
index 86c4288..2555c33 100644
--- a/src/components/studio/sidebar/ColorsTemplatePreviewCard.tsx
+++ b/src/components/studio/sidebar/ColorsTemplatePreviewCard.tsx
@@ -1,3 +1,7 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+
import { PALETTE_NAMES } from "@/lib/studio-color-palettes";
import { contrastTextColor } from "@/lib/studio-color-palettes";
@@ -10,8 +14,13 @@ export function ColorsTemplatePreviewCard({
palette,
paletteIndex,
}: ColorsTemplatePreviewCardProps) {
+ const t = useTranslations(
+ "auto.componentsStudioSidebarColorsTemplatePreviewCard"
+ );
const [mainColor, accentColor] = palette;
- const paletteName = PALETTE_NAMES[paletteIndex] ?? `Palette ${paletteIndex + 1}`;
+ const paletteName =
+ PALETTE_NAMES[paletteIndex] ??
+ t("paletteFallback", { number: paletteIndex + 1 });
return (
- Main Color
+ {t("mainColor")}
- Additional
+ {t("additional")}
state.applyFontFamilyToAllTextLayers
);
@@ -23,14 +25,14 @@ export function FontSidebarContent() {
className="w-full border-gray-200 bg-white text-gray-700 hover:bg-gray-50 hover:text-gray-900"
onClick={() => applyFontFamilyToAllTextLayers(fontFamily)}
>
- Apply to all text layers
+ {t("applyToAll")}
);
return (
-
+
({
label: item.label,
diff --git a/src/components/studio/sidebar/SceneEditSidebarContent.tsx b/src/components/studio/sidebar/SceneEditSidebarContent.tsx
index 372c7b4..4e7b915 100644
--- a/src/components/studio/sidebar/SceneEditSidebarContent.tsx
+++ b/src/components/studio/sidebar/SceneEditSidebarContent.tsx
@@ -2,11 +2,13 @@
import { useRef } from "react";
import { ImagePlus, Plus, Type } from "lucide-react";
+import { useTranslations } from "next-intl";
import { getTextProps, mergeLayerProps } from "@/lib/studio-layer-props";
import { useStudioStore } from "@/lib/studio-store";
export function SceneEditSidebarContent() {
+ const t = useTranslations("auto.componentsStudioSidebarSceneEditSidebarContent");
const scenes = useStudioStore((state) => state.scenes);
const activeSceneId = useStudioStore((state) => state.activeSceneId);
const updateLayer = useStudioStore((state) => state.updateLayer);
@@ -24,7 +26,7 @@ export function SceneEditSidebarContent() {
width: 800,
height: 80,
props: {
- text: "Your text here",
+ text: t("defaultText"),
fontSize: 48,
fill: "#111827",
fontFamily: "Inter, sans-serif",
@@ -42,7 +44,7 @@ export function SceneEditSidebarContent() {
{/* Panel header */}
- Edit Scene
+ {t("panelTitle")}
{activeScene && (
@@ -67,7 +69,11 @@ export function SceneEditSidebarContent() {
@@ -105,7 +111,7 @@ export function SceneEditSidebarContent() {
{imageLayers.map((layer, idx) => (
updateLayer(layer.id, {
@@ -122,10 +128,10 @@ export function SceneEditSidebarContent() {
- This scene has no content yet.
+ {t("emptyStateTitle")}
- Add a text layer to start editing.
+ {t("emptyStateHint")}
)}
@@ -139,7 +145,7 @@ export function SceneEditSidebarContent() {
className="flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-blue-300 bg-blue-50 px-3 py-2 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
>
- Add Text Layer
+ {t("addTextLayer")}
@@ -156,6 +162,7 @@ function ImageLayerInput({
layerProps: Record
;
onReplace: (src: string) => void;
}) {
+ const t = useTranslations("auto.componentsStudioSidebarSceneEditSidebarContent");
const inputRef = useRef(null);
const src = typeof layerProps.src === "string" ? layerProps.src : null;
@@ -195,7 +202,7 @@ function ImageLayerInput({
)}
- {src ? "Replace image" : "Upload image"}
+ {src ? t("replaceImage") : t("uploadImage")}
diff --git a/src/components/studio/sidebar/TransitionsSidebarContent.tsx b/src/components/studio/sidebar/TransitionsSidebarContent.tsx
index d23b786..94952c5 100644
--- a/src/components/studio/sidebar/TransitionsSidebarContent.tsx
+++ b/src/components/studio/sidebar/TransitionsSidebarContent.tsx
@@ -2,6 +2,7 @@
import { useEffect, useState } from "react";
import { Info } from "lucide-react";
+import { useTranslations } from "next-intl";
import { useStudioStore } from "@/lib/studio-store";
import type { SceneTransition } from "@/lib/studio-types";
@@ -36,10 +37,12 @@ function optionToTransitionType(id: TransitionOptionId): SceneTransition {
function TransitionOptionCard({
option,
+ label,
selected,
onSelect,
}: {
option: (typeof TRANSITION_OPTIONS)[number];
+ label: string;
selected: boolean;
onSelect: () => void;
}) {
@@ -63,13 +66,14 @@ function TransitionOptionCard({
- {option.label}
+ {label}
);
}
export function TransitionsSidebarContent() {
+ const t = useTranslations("auto.componentsStudioSidebarTransitionsSidebarContent");
const scenes = useStudioStore((state) => state.scenes);
const applyTransitionToAllScenes = useStudioStore(
(state) => state.applyTransitionToAllScenes
@@ -91,7 +95,7 @@ export function TransitionsSidebarContent() {
return (