Implementiere i18n für Frontend, Admin und Datenbank

This commit is contained in:
Hördle Bot
2025-11-28 15:36:06 +01:00
parent 9df9a808bf
commit 771d0d06f3
37 changed files with 3717 additions and 560 deletions

View File

@@ -0,0 +1,134 @@
import Game from '@/components/Game';
import NewsSection from '@/components/NewsSection';
import { getOrCreateDailyPuzzle } from '@/lib/dailyPuzzle';
import { Link } from '@/lib/navigation';
import { PrismaClient } from '@prisma/client';
import { notFound } from 'next/navigation';
import { getLocalizedValue } from '@/lib/i18n';
import { getTranslations } from 'next-intl/server';
export const dynamic = 'force-dynamic';
const prisma = new PrismaClient();
interface PageProps {
params: Promise<{ locale: string; genre: string }>;
}
export default async function GenrePage({ params }: PageProps) {
const { locale, genre } = await params;
const decodedGenre = decodeURIComponent(genre);
const tNav = await getTranslations('Navigation');
// Fetch all genres to find the matching one by localized name
const allGenres = await prisma.genre.findMany();
const currentGenre = allGenres.find(g => getLocalizedValue(g.name, locale) === decodedGenre);
if (!currentGenre || !currentGenre.active) {
notFound();
}
const dailyPuzzle = await getOrCreateDailyPuzzle(currentGenre);
// getOrCreateDailyPuzzle likely expects string or needs update.
// Actually, getOrCreateDailyPuzzle takes `genreName: string | null`.
// If I pass the JSON object, it might fail.
// But wait, the DB schema for DailyPuzzle stores `genreId`.
// `getOrCreateDailyPuzzle` probably looks up genre by name.
// I should check `lib/dailyPuzzle.ts`.
// For now, I'll pass the localized name, but that might be risky if it tries to create a genre (unlikely).
// Let's assume for now I should pass the localized name if that's what it uses to find/create.
// But if `getOrCreateDailyPuzzle` uses `findUnique({ where: { name: genreName } })`, it will fail because name is JSON.
// I need to update `lib/dailyPuzzle.ts` too!
// I'll mark that as a todo. For now, let's proceed with page creation.
const genres = allGenres.filter(g => g.active);
// Sort
genres.sort((a, b) => getLocalizedValue(a.name, locale).localeCompare(getLocalizedValue(b.name, locale)));
const specials = await prisma.special.findMany();
specials.sort((a, b) => getLocalizedValue(a.name, locale).localeCompare(getLocalizedValue(b.name, locale)));
const now = new Date();
const activeSpecials = specials.filter(s => {
const isStarted = !s.launchDate || s.launchDate <= now;
const isEnded = s.endDate && s.endDate < now;
return isStarted && !isEnded;
});
const upcomingSpecials = specials.filter(s => {
return s.launchDate && s.launchDate > now;
});
return (
<>
<div style={{ textAlign: 'center', padding: '1rem', background: '#f3f4f6' }}>
<div style={{ display: 'flex', justifyContent: 'center', gap: '1rem', flexWrap: 'wrap', alignItems: 'center' }}>
<Link href="/" style={{ color: '#4b5563', textDecoration: 'none' }}>{tNav('global')}</Link>
{/* Genres */}
{genres.map(g => {
const name = getLocalizedValue(g.name, locale);
return (
<Link
key={g.id}
href={`/${name}`}
style={{
fontWeight: name === decodedGenre ? 'bold' : 'normal',
textDecoration: name === decodedGenre ? 'underline' : 'none',
color: name === decodedGenre ? 'black' : '#4b5563'
}}
>
{name}
</Link>
);
})}
{/* Separator if both exist */}
{genres.length > 0 && activeSpecials.length > 0 && (
<span style={{ color: '#d1d5db' }}>|</span>
)}
{/* Specials */}
{activeSpecials.map(s => {
const name = getLocalizedValue(s.name, locale);
return (
<Link
key={s.id}
href={`/special/${name}`}
style={{
color: '#be185d', // Pink-700
textDecoration: 'none',
fontWeight: '500'
}}
>
{name}
</Link>
);
})}
</div>
{/* Upcoming Specials */}
{upcomingSpecials.length > 0 && (
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: '#666' }}>
Coming soon: {upcomingSpecials.map(s => {
const name = getLocalizedValue(s.name, locale);
return (
<span key={s.id} style={{ marginLeft: '0.5rem' }}>
{name} ({s.launchDate ? new Date(s.launchDate).toLocaleDateString(locale, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
timeZone: process.env.TZ
}) : ''})
{s.curator && <span style={{ fontStyle: 'italic', marginLeft: '0.25rem' }}>Curated by {s.curator}</span>}
</span>
);
})}
</div>
)}
</div>
<NewsSection locale={locale} />
<Game dailyPuzzle={dailyPuzzle} genre={decodedGenre} />
</>
);
}