[dyad] Adding a language switcher - wrote 9 file(s)

This commit is contained in:
[dyad]
2026-01-18 15:31:20 +01:00
parent 90cfdf0785
commit cad0921161
9 changed files with 142 additions and 25 deletions

View File

@@ -4,6 +4,7 @@ import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { Footer } from "@/components/footer"; import { Footer } from "@/components/footer";
import { I18nProvider } from "@/context/i18n-context";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -36,9 +37,11 @@ export default function RootLayout({
enableSystem enableSystem
disableTransitionOnChange disableTransitionOnChange
> >
<I18nProvider>
{children} {children}
<Footer /> <Footer />
<Toaster /> <Toaster />
</I18nProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

View File

@@ -1,19 +1,5 @@
import { ImageConverter } from "@/components/image-converter"; import { HomePage } from "@/components/home-page";
export default function Home() { export default function Home() {
return ( return <HomePage />;
<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
</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.
</p>
</div>
<ImageConverter />
</main>
</div>
);
} }

View File

@@ -2,6 +2,7 @@ import Link from "next/link";
import { Github, Twitter } from "lucide-react"; import { Github, Twitter } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { changelogData } from "@/lib/changelog-data"; import { changelogData } from "@/lib/changelog-data";
import { LanguageSwitcher } from "./language-switcher";
export function Footer() { export function Footer() {
const latestVersion = changelogData[0]?.version; const latestVersion = changelogData[0]?.version;
@@ -14,6 +15,7 @@ export function Footer() {
</div> </div>
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center gap-1"> <div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center gap-1">
<LanguageSwitcher />
<Button variant="ghost" size="icon" asChild> <Button variant="ghost" size="icon" asChild>
<Link href="https://github.com/" target="_blank" rel="noopener noreferrer" aria-label="GitHub"> <Link href="https://github.com/" target="_blank" rel="noopener noreferrer" aria-label="GitHub">
<Github className="h-4 w-4" /> <Github className="h-4 w-4" />

View File

@@ -0,0 +1,24 @@
"use client";
import { ImageConverter } from "@/components/image-converter";
import { useTranslation } from "@/context/i18n-context";
export function HomePage() {
const { t } = useTranslation();
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">
{t('appTitle')}
</h1>
<p className="mt-3 text-lg text-gray-600 dark:text-gray-400">
{t('appDescription')}
</p>
</div>
<ImageConverter />
</main>
</div>
);
}

View File

@@ -36,6 +36,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useTranslation } from "@/context/i18n-context";
const aspectRatios = [ const aspectRatios = [
{ name: "Custom", value: "custom" }, { name: "Custom", value: "custom" },
@@ -64,6 +65,7 @@ const initialSettings = {
}; };
export function ImageConverter() { export function ImageConverter() {
const { t } = useTranslation();
const [images, setImages] = useState<File[]>([]); const [images, setImages] = useState<File[]>([]);
const [previewUrls, setPreviewUrls] = useState<string[]>([]); const [previewUrls, setPreviewUrls] = useState<string[]>([]);
const [filenames, setFilenames] = useState<string[]>([]); const [filenames, setFilenames] = useState<string[]>([]);
@@ -427,7 +429,7 @@ export function ImageConverter() {
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium">Upload Images</h3> <h3 className="text-lg font-medium">{t('uploadTitle')}</h3>
<div <div
className={cn( 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", "w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
@@ -440,8 +442,8 @@ export function ImageConverter() {
> >
<div className="flex flex-col items-center justify-center text-center text-muted-foreground"> <div className="flex flex-col items-center justify-center text-center text-muted-foreground">
<Upload className="w-8 h-8 mb-2" /> <Upload className="w-8 h-8 mb-2" />
<p className="font-semibold">Click or drag and drop to upload</p> <p className="font-semibold">{t('uploadPrompt')}</p>
<p className="text-xs text-muted-foreground mt-1">PNG, JPG, WEBP supported</p> <p className="text-xs text-muted-foreground mt-1">{t('uploadFormats')}</p>
</div> </div>
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple /> <Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
</div> </div>
@@ -453,11 +455,11 @@ export function ImageConverter() {
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<CardTitle>Uploaded Images</CardTitle> <CardTitle>{t('uploadedImagesTitle')}</CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />Clear All</Button> <Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />{t('clearAll')}</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Remove all uploaded images.</p> <p>Remove all uploaded images.</p>
@@ -467,7 +469,7 @@ export function ImageConverter() {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button onClick={handleConvertAndDownloadAll} disabled={!hasImages || isConverting || convertingIndex !== null}> <Button onClick={handleConvertAndDownloadAll} disabled={!hasImages || isConverting || convertingIndex !== null}>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
{isConverting ? "Converting..." : `Download All (${images.length})`} {isConverting ? "Converting..." : t('downloadAll', { count: images.length })}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>

View File

@@ -0,0 +1,34 @@
"use client";
import { useTranslation } from "@/context/i18n-context";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Languages } from "lucide-react";
export function LanguageSwitcher() {
const { setLanguage } = useTranslation();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Languages className="h-4 w-4" />
<span className="sr-only">Change language</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setLanguage("en")}>
English
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("de")}>
Deutsch
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,46 @@
"use client";
import React, { createContext, useState, useContext, ReactNode } from 'react';
import en from '@/locales/en.json';
import de from '@/locales/de.json';
interface I18nContextType {
language: string;
setLanguage: (language: string) => void;
t: (key: string, params?: { [key: string]: string | number }) => string;
}
const I18nContext = createContext<I18nContextType | undefined>(undefined);
const translations: { [key: string]: { [key: string]: string } } = {
en,
de,
};
export function I18nProvider({ children }: { children: ReactNode }) {
const [language, setLanguage] = useState('en');
const t = (key: string, params?: { [key: string]: string | number }) => {
let translation = translations[language]?.[key] || key;
if (params) {
Object.keys(params).forEach(paramKey => {
translation = translation.replace(`{${paramKey}}`, String(params[paramKey]));
});
}
return translation;
};
return (
<I18nContext.Provider value={{ language, setLanguage, t }}>
{children}
</I18nContext.Provider>
);
}
export function useTranslation() {
const context = useContext(I18nContext);
if (context === undefined) {
throw new Error('useTranslation must be used within an I18nProvider');
}
return context;
}

10
src/locales/de.json Normal file
View File

@@ -0,0 +1,10 @@
{
"appTitle": "Bild Web Exporter",
"appDescription": "Laden Sie ein Bild hoch und exportieren Sie es in einer anderen Auflösung und einem anderen Format.",
"uploadTitle": "Bilder hochladen",
"uploadPrompt": "Klicken oder per Drag & Drop hochladen",
"uploadFormats": "PNG, JPG, WEBP werden unterstützt",
"uploadedImagesTitle": "Hochgeladene Bilder",
"clearAll": "Alle löschen",
"downloadAll": "Alle herunterladen ({count})"
}

10
src/locales/en.json Normal file
View File

@@ -0,0 +1,10 @@
{
"appTitle": "Image Web Exporter",
"appDescription": "Upload a picture, then export it in a different resolution and format.",
"uploadTitle": "Upload Images",
"uploadPrompt": "Click or drag and drop to upload",
"uploadFormats": "PNG, JPG, WEBP supported",
"uploadedImagesTitle": "Uploaded Images",
"clearAll": "Clear All",
"downloadAll": "Download All ({count})"
}