[dyad] Adding internationalization with next-intl - wrote 19 file(s), renamed 5 file(s), added next-intl package(s)
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
import { Changelog } from "@/components/changelog";
|
||||
import Link from "next/link";
|
||||
import Link from "next-intl/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function ChangelogPage() {
|
||||
const t = useTranslations("ChangelogPage");
|
||||
return (
|
||||
<div className="relative flex flex-col items-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background font-[family-name:var(--font-geist-sans)]">
|
||||
<div className="w-full max-w-4xl mx-auto">
|
||||
<Button asChild variant="ghost" className="mb-4 -ml-4">
|
||||
<Link href="/">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Converter
|
||||
{t('back')}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1,16 +1,18 @@
|
||||
import Link from "next/link";
|
||||
import Link from "next-intl/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function ImprintPage() {
|
||||
const t = useTranslations("ImprintPage");
|
||||
return (
|
||||
<div className="relative flex flex-col items-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background">
|
||||
<div className="w-full max-w-4xl mx-auto">
|
||||
<Button asChild variant="ghost" className="mb-4 -ml-4">
|
||||
<Link href="/">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Converter
|
||||
{t('back')}
|
||||
</Link>
|
||||
</Button>
|
||||
<main className="w-full">
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import "../globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -20,26 +22,32 @@ export const metadata: Metadata = {
|
||||
description: "Upload a picture, then export it in a different resolution and format.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params: { locale }
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}>) {
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<Footer />
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<Footer />
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
@@ -1,15 +1,17 @@
|
||||
import { ImageConverter } from "@/components/image-converter";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Home() {
|
||||
const t = useTranslations('HomePage');
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col items-center w-full max-w-6xl z-10">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl">
|
||||
Image Web Exporter
|
||||
{t('title')}
|
||||
</h1>
|
||||
<p className="mt-3 text-lg text-gray-600 dark:text-gray-400">
|
||||
Upload a picture, then export it in a different resolution and format.
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<ImageConverter />
|
||||
@@ -1,16 +1,18 @@
|
||||
import Link from "next/link";
|
||||
import Link from "next-intl/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function PrivacyPage() {
|
||||
const t = useTranslations("PrivacyPage");
|
||||
return (
|
||||
<div className="relative flex flex-col items-center min-h-screen p-4 sm:p-8 bg-gray-50 dark:bg-background">
|
||||
<div className="w-full max-w-4xl mx-auto">
|
||||
<Button asChild variant="ghost" className="mb-4 -ml-4">
|
||||
<Link href="/">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Converter
|
||||
{t('back')}
|
||||
</Link>
|
||||
</Button>
|
||||
<main className="w-full">
|
||||
@@ -4,12 +4,14 @@ import { Button } from "@/components/ui/button";
|
||||
import { Check, RotateCcw } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ActionButtonsProps {
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
export function ActionButtons({ onReset }: ActionButtonsProps) {
|
||||
const t = useTranslations("ActionButtons");
|
||||
const handleApply = () => {
|
||||
toast.info("Settings updated and will be used for all downloads.");
|
||||
};
|
||||
@@ -20,18 +22,18 @@ export function ActionButtons({ onReset }: ActionButtonsProps) {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={onReset} className="w-full" variant="outline">
|
||||
<RotateCcw className="mr-2 h-4 w-4" /> Reset
|
||||
<RotateCcw className="mr-2 h-4 w-4" /> {t('reset')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Reset all settings to their default values.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('resetTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={handleApply} className="w-full">
|
||||
<Check className="mr-2 h-4 w-4" /> Apply
|
||||
<Check className="mr-2 h-4 w-4" /> {t('apply')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Confirm and apply all the settings above. This does not download the images.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('applyTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import Link from "next/link";
|
||||
"use client";
|
||||
|
||||
import Link from "next-intl/link";
|
||||
import { Github, Twitter } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { changelogData } from "@/lib/changelog-data";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export function Footer() {
|
||||
const t = useTranslations("Footer");
|
||||
const latestVersion = changelogData[0]?.version;
|
||||
|
||||
return (
|
||||
@@ -27,8 +31,8 @@ export function Footer() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<Link href="/imprint" className="hover:text-primary transition-colors">Imprint</Link>
|
||||
<Link href="/privacy" className="hover:text-primary transition-colors">Privacy</Link>
|
||||
<Link href="/imprint" className="hover:text-primary transition-colors">{t('imprint')}</Link>
|
||||
<Link href="/privacy" className="hover:text-primary transition-colors">{t('privacy')}</Link>
|
||||
{latestVersion && (
|
||||
<Link
|
||||
href="/changelog"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Download, X } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { generateFinalFilename } from "@/lib/image-processor";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ImageListItemProps {
|
||||
image: ImageFile;
|
||||
@@ -27,22 +28,24 @@ export function ImageListItem({
|
||||
onDownload,
|
||||
isProcessing,
|
||||
}: ImageListItemProps) {
|
||||
const t = useTranslations("ImageListItem");
|
||||
const finalFilename = generateFinalFilename(image.filename, settings, index);
|
||||
const finalNameText = t('finalName', { finalFilename, format: settings.format });
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="p-4 border rounded-lg flex items-center gap-4">
|
||||
<img src={image.previewUrl} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Base Name</Label>
|
||||
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">{t('baseName')}</Label>
|
||||
<Input
|
||||
id={`filename-${index}`}
|
||||
value={image.filename}
|
||||
onChange={(e) => onFilenameChange(index, e.target.value)}
|
||||
className="text-sm font-medium h-8 mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${settings.format}`}>
|
||||
Final name: {finalFilename}.{settings.format}
|
||||
<p className="text-xs text-muted-foreground truncate mt-1" title={finalNameText}>
|
||||
{finalNameText}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center shrink-0">
|
||||
@@ -58,7 +61,7 @@ export function ImageListItem({
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Download this image</p></TooltipContent>
|
||||
<TooltipContent><p>{t('downloadTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -72,7 +75,7 @@ export function ImageListItem({
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Remove this image</p></TooltipContent>
|
||||
<TooltipContent><p>{t('removeTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Download, Trash2 } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { ImageListItem } from "./image-list-item";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ImageListProps {
|
||||
images: ImageFile[];
|
||||
@@ -30,6 +31,8 @@ export function ImageList({
|
||||
isConverting,
|
||||
convertingIndex,
|
||||
}: ImageListProps) {
|
||||
const t = useTranslations("ImageList");
|
||||
|
||||
if (images.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -41,24 +44,24 @@ export function ImageList({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle>Uploaded Images</CardTitle>
|
||||
<CardTitle>{t('title')}</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="sm" onClick={onClearAll} disabled={isProcessing}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />Clear All
|
||||
<Trash2 className="mr-2 h-4 w-4" />{t('clearAll')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Remove all uploaded images.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('clearAllTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={onDownloadAll} disabled={isProcessing}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{isConverting ? "Converting..." : `Download All (${images.length})`}
|
||||
{isConverting ? t('converting') : t('downloadAll', { count: images.length })}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Convert and download all images with the current settings.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('downloadAllTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,14 @@ import { Input } from "@/components/ui/input";
|
||||
import { Upload } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Card, CardContent } from "./ui/card";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ImageUploadAreaProps {
|
||||
onFilesSelected: (files: FileList | null) => void;
|
||||
}
|
||||
|
||||
export function ImageUploadArea({ onFilesSelected }: ImageUploadAreaProps) {
|
||||
const t = useTranslations("ImageUploadArea");
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -39,7 +41,7 @@ export function ImageUploadArea({ onFilesSelected }: ImageUploadAreaProps) {
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Upload Images</h3>
|
||||
<h3 className="text-lg font-medium">{t('title')}</h3>
|
||||
<div
|
||||
className={cn(
|
||||
"w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
|
||||
@@ -52,8 +54,8 @@ export function ImageUploadArea({ onFilesSelected }: ImageUploadAreaProps) {
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center text-center text-muted-foreground">
|
||||
<Upload className="w-8 h-8 mb-2" />
|
||||
<p className="font-semibold">Click or drag and drop to upload</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP supported</p>
|
||||
<p className="font-semibold">{t('prompt')}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t('supportedFormats')}</p>
|
||||
</div>
|
||||
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/
|
||||
import { ImageSettings } from "./settings/image-settings";
|
||||
import { FilenameSettings } from "./settings/filename-settings";
|
||||
import { QualitySettings } from "./settings/quality-settings";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface SettingsPanelProps {
|
||||
settings: ConversionSettings;
|
||||
@@ -23,13 +24,14 @@ export function SettingsPanel({
|
||||
onApplyDefaultBaseNameToAll,
|
||||
hasImages,
|
||||
}: SettingsPanelProps) {
|
||||
const t = useTranslations("SettingsPanel");
|
||||
return (
|
||||
<Accordion type="single" collapsible defaultValue="image-settings" className="w-full space-y-4">
|
||||
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
|
||||
<AccordionTrigger className="p-6 hover:no-underline">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">Adjust resolution and scaling for all images.</p>
|
||||
<h3 className="text-lg font-medium leading-none">{t('imageSettingsTitle')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('imageSettingsSubtitle')}</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6">
|
||||
@@ -45,8 +47,8 @@ export function SettingsPanel({
|
||||
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
|
||||
<AccordionTrigger className="p-6 hover:no-underline">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-medium leading-none">Filename Settings</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">Customize the output filenames.</p>
|
||||
<h3 className="text-lg font-medium leading-none">{t('filenameSettingsTitle')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('filenameSettingsSubtitle')}</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6">
|
||||
@@ -62,8 +64,8 @@ export function SettingsPanel({
|
||||
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
|
||||
<AccordionTrigger className="p-6 hover:no-underline">
|
||||
<div className="text-left">
|
||||
<h3 className="text-lg font-medium leading-none">Quality Settings</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">Choose format and compression level.</p>
|
||||
<h3 className="text-lg font-medium leading-none">{t('qualitySettingsTitle')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('qualitySettingsSubtitle')}</p>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-6 pb-6">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface FilenameSettingsProps {
|
||||
settings: ConversionSettings;
|
||||
@@ -21,21 +22,22 @@ export function FilenameSettings({
|
||||
onApplyDefaultBaseNameToAll,
|
||||
hasImages,
|
||||
}: FilenameSettingsProps) {
|
||||
const t = useTranslations("FilenameSettings");
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="use-default-base-name" checked={settings.useDefaultBaseName} onCheckedChange={(checked) => onSettingsChange({ useDefaultBaseName: checked })} />
|
||||
<Label htmlFor="use-default-base-name" className="flex items-center gap-1.5 cursor-pointer">
|
||||
Use default base name
|
||||
{t('useDefaultBaseName')}
|
||||
<Tooltip>
|
||||
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>When enabled, all newly uploaded images will use the specified default base name.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('useDefaultBaseNameTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
</div>
|
||||
{settings.useDefaultBaseName && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="default-base-name">Default base name</Label>
|
||||
<Label htmlFor="default-base-name">{t('defaultBaseName')}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="default-base-name"
|
||||
@@ -46,30 +48,30 @@ export function FilenameSettings({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm" onClick={onApplyDefaultBaseNameToAll} disabled={!settings.defaultBaseName || !hasImages}>
|
||||
Apply to all
|
||||
{t('applyToAll')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Apply this base name to all currently uploaded images.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('applyToAllTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="prefix">Prefix</Label>
|
||||
<Label htmlFor="prefix">{t('prefix')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Add text to the beginning of every filename.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('prefixTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input id="prefix" placeholder="e.g., travel-" value={settings.prefix} onChange={(e) => onSettingsChange({ prefix: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="suffix">Suffix</Label>
|
||||
<Label htmlFor="suffix">{t('suffix')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Add text to the end of every filename (before the number).</p></TooltipContent>
|
||||
<TooltipContent><p>{t('suffixTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input id="suffix" placeholder="e.g., -edit" value={settings.suffix} onChange={(e) => onSettingsChange({ suffix: e.target.value })} />
|
||||
@@ -77,10 +79,10 @@ export function FilenameSettings({
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Switch id="use-counter" checked={settings.useCounter} onCheckedChange={(checked) => onSettingsChange({ useCounter: checked })} />
|
||||
<Label htmlFor="use-counter" className="flex items-center gap-1.5 cursor-pointer">
|
||||
Add sequential number
|
||||
{t('addSequentialNumber')}
|
||||
<Tooltip>
|
||||
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Append a numbered sequence to each filename.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('addSequentialNumberTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -88,10 +90,10 @@ export function FilenameSettings({
|
||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="counter-start">Start number</Label>
|
||||
<Label htmlFor="counter-start">{t('startNumber')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>The first number to use in the sequence.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('startNumberTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input
|
||||
@@ -104,10 +106,10 @@ export function FilenameSettings({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="counter-digits">Padding digits</Label>
|
||||
<Label htmlFor="counter-digits">{t('paddingDigits')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Total number of digits for the counter, padded with leading zeros (e.g., 3 for 001).</p></TooltipContent>
|
||||
<TooltipContent><p>{t('paddingDigitsTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input
|
||||
|
||||
@@ -9,14 +9,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ArrowRightLeft, HelpCircle } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { ObjectPositionControl } from "@/components/object-position-control";
|
||||
|
||||
const aspectRatios = [
|
||||
{ name: "Custom", value: "custom" },
|
||||
{ name: "1:1 (Square)", value: "1/1" },
|
||||
{ name: "4:3 (Standard)", value: "4/3" },
|
||||
{ name: "3:2 (Photography)", value: "3/2" },
|
||||
{ name: "16:9 (Widescreen)", value: "16/9" },
|
||||
];
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ImageSettingsProps {
|
||||
settings: ConversionSettings;
|
||||
@@ -31,14 +24,24 @@ export function ImageSettings({
|
||||
onAspectRatioChange,
|
||||
onSwapDimensions,
|
||||
}: ImageSettingsProps) {
|
||||
const t = useTranslations("ImageSettings");
|
||||
|
||||
const aspectRatios = [
|
||||
{ name: t('custom'), value: "custom" },
|
||||
{ name: t('square'), value: "1/1" },
|
||||
{ name: t('standard'), value: "4/3" },
|
||||
{ name: t('photography'), value: "3/2" },
|
||||
{ name: t('widescreen'), value: "16/9" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="aspect-ratio">Aspect Ratio</Label>
|
||||
<Label htmlFor="aspect-ratio">{t('aspectRatio')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Choose a preset aspect ratio or select 'Custom' to enter dimensions manually.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('aspectRatioTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Select value={settings.aspectRatio} onValueChange={onAspectRatioChange}>
|
||||
@@ -53,10 +56,10 @@ export function ImageSettings({
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="width">Width (px)</Label>
|
||||
<Label htmlFor="width">{t('width')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Set the output width in pixels. Leave blank to use the original width.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('widthTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input id="width" type="number" placeholder="Auto" value={settings.width} onChange={(e) => { onSettingsChange({ width: e.target.value, aspectRatio: 'custom' }) }} />
|
||||
@@ -67,14 +70,14 @@ export function ImageSettings({
|
||||
<ArrowRightLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Swap the entered width and height values.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('swapTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="height">Height (px)</Label>
|
||||
<Label htmlFor="height">{t('height')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Set the output height in pixels. Leave blank to use the original height.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('heightTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input id="height" type="number" placeholder="Auto" value={settings.height} onChange={(e) => { onSettingsChange({ height: e.target.value, aspectRatio: 'custom' }) }} />
|
||||
@@ -83,37 +86,37 @@ export function ImageSettings({
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Checkbox id="keep-orientation" checked={settings.keepOrientation} onCheckedChange={(checked) => onSettingsChange({ keepOrientation: Boolean(checked) })} />
|
||||
<Label htmlFor="keep-orientation" className="cursor-pointer flex items-center gap-1.5">
|
||||
Keep original orientation
|
||||
{t('keepOrientation')}
|
||||
<Tooltip>
|
||||
<TooltipTrigger onClick={(e) => e.preventDefault()}><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Automatically swaps width and height to match the original image's orientation.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('keepOrientationTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="scale-mode">Scaling</Label>
|
||||
<Label htmlFor="scale-mode">{t('scaling')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Determines how the image fits into the new dimensions.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('scalingTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Select value={settings.scaleMode} onValueChange={(value) => onSettingsChange({ scaleMode: value as any })}>
|
||||
<SelectTrigger id="scale-mode"><SelectValue placeholder="Select scaling mode" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fill">Fill (stretch to fit)</SelectItem>
|
||||
<SelectItem value="cover">Cover (crop to fit)</SelectItem>
|
||||
<SelectItem value="contain">Contain (letterbox)</SelectItem>
|
||||
<SelectItem value="fill">{t('fill')}</SelectItem>
|
||||
<SelectItem value="cover">{t('cover')}</SelectItem>
|
||||
<SelectItem value="contain">{t('contain')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{settings.scaleMode !== 'fill' && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label>Position</Label>
|
||||
<Label>{t('position')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Sets the anchor point for 'Cover' or 'Contain' scaling.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('positionTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ObjectPositionControl value={settings.objectPosition} onChange={(pos) => onSettingsChange({ objectPosition: pos as ObjectPosition })} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface QualitySettingsProps {
|
||||
settings: ConversionSettings;
|
||||
@@ -13,14 +14,15 @@ interface QualitySettingsProps {
|
||||
}
|
||||
|
||||
export function QualitySettings({ settings, onSettingsChange }: QualitySettingsProps) {
|
||||
const t = useTranslations("QualitySettings");
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="format">Format</Label>
|
||||
<Label htmlFor="format">{t('format')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Choose the output file format for the images.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('formatTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Select value={settings.format} onValueChange={(value: ImageFormat) => onSettingsChange({ format: value })}>
|
||||
@@ -35,10 +37,10 @@ export function QualitySettings({ settings, onSettingsChange }: QualitySettingsP
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="quality">Quality</Label>
|
||||
<Label htmlFor="quality">{t('quality')}</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger><HelpCircle className="h-4 w-4 text-muted-foreground" /></TooltipTrigger>
|
||||
<TooltipContent><p>Set compression quality for JPEG/WEBP. Higher is better quality but larger file size.</p></TooltipContent>
|
||||
<TooltipContent><p>{t('qualityTooltip')}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">{settings.quality}%</span>
|
||||
@@ -53,7 +55,7 @@ export function QualitySettings({ settings, onSettingsChange }: QualitySettingsP
|
||||
disabled={settings.format === 'png'}
|
||||
/>
|
||||
{settings.format === 'png' && (
|
||||
<p className="text-xs text-muted-foreground pt-1">Quality slider is disabled for PNG (lossless format).</p>
|
||||
<p className="text-xs text-muted-foreground pt-1">{t('pngWarning')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
14
src/i18n.ts
Normal file
14
src/i18n.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
|
||||
export const locales = ['en'];
|
||||
export const defaultLocale = 'en';
|
||||
|
||||
export default getRequestConfig(async ({locale}) => {
|
||||
if (!locales.includes(locale as any)) {
|
||||
locale = defaultLocale;
|
||||
}
|
||||
|
||||
return {
|
||||
messages: (await import(`../messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
10
src/middleware.ts
Normal file
10
src/middleware.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
|
||||
export default createMiddleware({
|
||||
locales: ['en'],
|
||||
defaultLocale: 'en'
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
|
||||
};
|
||||
Reference in New Issue
Block a user