feat(i18n): centralize more UI texts in messages

Move hardcoded labels/strings in About, Projects, Contact form, Footer and Consent banner into next-intl message files (en/de) so content is maintained in one place.

Co-authored-by: dennis <dennis@konkol.net>
This commit is contained in:
Cursor Agent
2026-01-15 10:03:32 +00:00
parent faf41a511b
commit a617f6eb92
7 changed files with 200 additions and 56 deletions

View File

@@ -57,32 +57,32 @@ const About = () => {
const techStack = [
{
category: "Frontend & Mobile",
category: t("techStack.categories.frontendMobile"),
icon: Globe,
items: ["Next.js", "Tailwind CSS", "Flutter"],
},
{
category: "Backend & DevOps",
category: t("techStack.categories.backendDevops"),
icon: Server,
items: ["Docker Swarm", "Traefik", "Nginx Proxy Manager", "Redis"],
},
{
category: "Tools & Automation",
category: t("techStack.categories.toolsAutomation"),
icon: Wrench,
items: ["Git", "CI/CD", "n8n", "Self-hosted Services"],
items: ["Git", "CI/CD", "n8n", t("techStack.items.selfHostedServices")],
},
{
category: "Security & Admin",
category: t("techStack.categories.securityAdmin"),
icon: Shield,
items: ["CrowdSec", "Suricata", "Mailcow"],
},
];
const hobbies: Array<{ icon: typeof Code; text: string }> = [
{ icon: Code, text: "Self-Hosting & DevOps" },
{ icon: Gamepad2, text: "Gaming" },
{ icon: Server, text: "Setting up Game Servers" },
{ icon: Activity, text: "Jogging to clear my mind and stay active" },
{ icon: Code, text: t("hobbies.selfHosting") },
{ icon: Gamepad2, text: t("hobbies.gaming") },
{ icon: Server, text: t("hobbies.gameServers") },
{ icon: Activity, text: t("hobbies.jogging") },
];
return (
@@ -151,7 +151,7 @@ const About = () => {
variants={fadeInUp}
className="text-2xl font-bold text-stone-900 mb-6"
>
My Tech Stack
{t("techStackTitle")}
</motion.h3>
<div className="grid grid-cols-1 gap-4">
{techStack.map((stack, idx) => (
@@ -209,7 +209,7 @@ const About = () => {
variants={fadeInUp}
className="text-xl font-bold text-stone-900 mb-4"
>
When I&apos;m Not Coding
{t("hobbiesTitle")}
</motion.h3>
<div className="space-y-3">
{hobbies.map((hobby, idx) => (

View File

@@ -21,9 +21,11 @@ export default function ConsentBanner() {
essential: t("essential"),
analytics: t("analytics"),
chat: t("chat"),
alwaysOn: t("alwaysOn"),
acceptAll: t("acceptAll"),
acceptSelected: t("acceptSelected"),
rejectAll: t("rejectAll"),
hide: t("hide"),
};
if (minimized) {
@@ -56,14 +58,14 @@ export default function ConsentBanner() {
aria-label="Minimize privacy banner"
title="Minimize"
>
Hide
{s.hide}
</button>
</div>
<div className="mt-3 space-y-2">
<div className="flex items-center justify-between gap-3">
<div className="text-xs font-semibold text-stone-800">{s.essential}</div>
<div className="text-[11px] text-stone-500">Always on</div>
<div className="text-[11px] text-stone-500">{s.alwaysOn}</div>
</div>
<label className="flex items-center justify-between gap-3 py-1">

View File

@@ -12,6 +12,8 @@ const Contact = () => {
const { showEmailSent, showEmailError } = useToast();
const locale = useLocale();
const t = useTranslations("home.contact");
const tForm = useTranslations("home.contact.form");
const tInfo = useTranslations("home.contact.info");
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
useEffect(() => {
@@ -49,27 +51,27 @@ const Contact = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = "Name is required";
newErrors.name = tForm("errors.nameRequired");
} else if (formData.name.trim().length < 2) {
newErrors.name = "Name must be at least 2 characters";
newErrors.name = tForm("errors.nameMin");
}
if (!formData.email.trim()) {
newErrors.email = "Email is required";
newErrors.email = tForm("errors.emailRequired");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = "Please enter a valid email address";
newErrors.email = tForm("errors.emailInvalid");
}
if (!formData.subject.trim()) {
newErrors.subject = "Subject is required";
newErrors.subject = tForm("errors.subjectRequired");
} else if (formData.subject.trim().length < 3) {
newErrors.subject = "Subject must be at least 3 characters";
newErrors.subject = tForm("errors.subjectMin");
}
if (!formData.message.trim()) {
newErrors.message = "Message is required";
newErrors.message = tForm("errors.messageRequired");
} else if (formData.message.trim().length < 10) {
newErrors.message = "Message must be at least 10 characters";
newErrors.message = tForm("errors.messageMin");
}
setErrors(newErrors);
@@ -153,14 +155,14 @@ const Contact = () => {
const contactInfo = [
{
icon: Mail,
title: "Email",
title: tInfo("email"),
value: "contact@dk0.dev",
href: "mailto:contact@dk0.dev",
},
{
icon: MapPin,
title: "Location",
value: "Osnabrück, Germany",
title: tInfo("location"),
value: tInfo("locationValue"),
},
];
@@ -251,7 +253,7 @@ const Contact = () => {
className="glass-card p-8 rounded-3xl bg-white/50 border border-white/70"
>
<h3 className="text-2xl font-bold text-gray-800 mb-6">
Send Message
{tForm("title")}
</h3>
<form onSubmit={handleSubmit} className="space-y-6">
@@ -276,7 +278,7 @@ const Contact = () => {
? "border-red-400 focus:ring-red-400"
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
}`}
placeholder="Your name"
placeholder={tForm("placeholders.name")}
aria-invalid={
errors.name && touched.name ? "true" : "false"
}
@@ -311,7 +313,7 @@ const Contact = () => {
? "border-red-400 focus:ring-red-400"
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
}`}
placeholder="your@email.com"
placeholder={tForm("placeholders.email")}
aria-invalid={
errors.email && touched.email ? "true" : "false"
}
@@ -347,7 +349,7 @@ const Contact = () => {
? "border-red-400 focus:ring-red-400"
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
}`}
placeholder="What's this about?"
placeholder={tForm("placeholders.subject")}
aria-invalid={
errors.subject && touched.subject ? "true" : "false"
}
@@ -384,7 +386,7 @@ const Contact = () => {
? "border-red-400 focus:ring-red-400"
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
}`}
placeholder="Tell me more about your project or question..."
placeholder={tForm("placeholders.message")}
aria-invalid={
errors.message && touched.message ? "true" : "false"
}
@@ -403,7 +405,7 @@ const Contact = () => {
<span></span>
)}
<span className="text-xs text-stone-400">
{formData.message.length} characters
{tForm("characters", { count: formData.message.length })}
</span>
</div>
</div>
@@ -419,12 +421,12 @@ const Contact = () => {
{isSubmitting ? (
<>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
<span>Sending Message...</span>
<span>{tForm("sending")}</span>
</>
) : (
<>
<Send size={20} />
<span className="text-cream">Send Message</span>
<span className="text-cream">{tForm("send")}</span>
</>
)}
</motion.button>

View File

@@ -5,17 +5,15 @@ import { motion } from 'framer-motion';
import { Heart, Code } from 'lucide-react';
import { SiGithub, SiLinkedin } from 'react-icons/si';
import Link from 'next/link';
import { useLocale } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
import { useConsent } from "./ConsentProvider";
const Footer = () => {
const [currentYear, setCurrentYear] = useState(2024);
const locale = useLocale();
const t = useTranslations("footer");
const { resetConsent } = useConsent();
useEffect(() => {
setCurrentYear(new Date().getFullYear());
}, []);
const [currentYear] = useState(() => new Date().getFullYear());
const socialLinks = [
{ icon: SiGithub, href: 'https://github.com/Denshooter', label: 'GitHub' },
@@ -45,7 +43,7 @@ const Footer = () => {
<Link href={`/${locale}`} className="text-xl font-bold font-mono text-stone-800 hover:text-liquid-blue transition-colors">
dk<span className="text-liquid-rose">0</span>
</Link>
<p className="text-xs text-stone-500">Software Engineer</p>
<p className="text-xs text-stone-500">{t("role")}</p>
</div>
</motion.div>
@@ -88,7 +86,7 @@ const Footer = () => {
>
<Heart size={14} className="text-liquid-rose fill-liquid-rose" />
</motion.div>
<span>Made in Germany</span>
<span>{t("madeIn")}</span>
</motion.div>
</div>
@@ -105,21 +103,21 @@ const Footer = () => {
href={`/${locale}/legal-notice`}
className="text-stone-500 hover:text-stone-800 transition-colors duration-200"
>
Impressum
{t("legalNotice")}
</Link>
<Link
href={`/${locale}/privacy-policy`}
className="text-stone-500 hover:text-stone-800 transition-colors duration-200"
>
Privacy Policy
{t("privacyPolicy")}
</Link>
<button
type="button"
onClick={() => resetConsent()}
className="text-stone-500 hover:text-stone-800 transition-colors duration-200"
title="Show privacy settings banner again"
title={t("privacySettingsTitle")}
>
Privacy settings
{t("privacySettings")}
</button>
<Link
href="/404"
@@ -131,7 +129,7 @@ const Footer = () => {
</div>
<div className="text-xs text-stone-400 flex items-center space-x-1">
<span>Built with</span>
<span>{t("builtWith")}</span>
<span className="text-stone-600 font-semibold">Next.js</span>
<span className="text-stone-300"></span>
<span className="text-stone-600 font-semibold">TypeScript</span>

View File

@@ -5,7 +5,7 @@ import { motion, Variants } from "framer-motion";
import { ExternalLink, Github, ArrowRight, Calendar } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import { useLocale } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
const fadeInUp: Variants = {
hidden: { opacity: 0, y: 20 },
@@ -48,6 +48,7 @@ interface Project {
const Projects = () => {
const [projects, setProjects] = useState<Project[]>([]);
const locale = useLocale();
const t = useTranslations("home.projects");
useEffect(() => {
const loadProjects = async () => {
@@ -82,11 +83,10 @@ const Projects = () => {
className="text-center mb-20"
>
<h2 className="text-4xl md:text-6xl font-bold mb-6 text-stone-900">
Selected Works
{t("title")}
</h2>
<p className="text-lg text-stone-600 max-w-2xl mx-auto mt-4 font-light">
A collection of projects I&apos;ve worked on, ranging from web
applications to experiments.
{t("subtitle")}
</p>
</motion.div>
@@ -140,7 +140,7 @@ const Projects = () => {
{project.featured && (
<div className="absolute top-3 left-3 z-20">
<div className="px-3 py-1 bg-[#292524]/80 backdrop-blur-md text-[#fdfcf8] text-[10px] font-bold uppercase tracking-widest rounded-full shadow-sm border border-white/10">
Featured
{t("featured")}
</div>
</div>
)}
@@ -253,7 +253,7 @@ const Projects = () => {
href={`/${locale}/projects`}
className="inline-flex items-center gap-2 px-8 py-4 bg-white border border-stone-200 rounded-full text-stone-700 font-medium hover:bg-stone-50 hover:border-stone-300 hover:gap-3 transition-all duration-500 ease-out shadow-sm hover:shadow-md"
>
View All Projects <ArrowRight size={16} />
{t("viewAll")} <ArrowRight size={16} />
</Link>
</motion.div>
</div>

View File

@@ -17,9 +17,11 @@
"essential": "Essentiell",
"analytics": "Analytics",
"chat": "Chatbot",
"alwaysOn": "Immer aktiv",
"acceptAll": "Alles akzeptieren",
"acceptSelected": "Auswahl akzeptieren",
"rejectAll": "Alles ablehnen"
"rejectAll": "Alles ablehnen",
"hide": "Ausblenden"
}
,
"home": {
@@ -39,14 +41,83 @@
"p2": "Ich entwickle Full-Stack Web-Apps mit Next.js und Mobile-Apps mit Flutter. Besonders spannend finde ich DevOps: eigene Infrastruktur, Automatisierung und CI/CD Deployments.",
"p3": "Wenn ich nicht code oder an Servern schraube, findest du mich beim Gaming, Joggen oder beim Experimentieren mit Automationen.",
"funFactTitle": "Fun Fact",
"funFactBody": "Auch wenn ich viel automatisiere, nutze ich für Kalender & Notizen noch Stift und Papier das hilft mir beim Fokus."
"funFactBody": "Auch wenn ich viel automatisiere, nutze ich für Kalender & Notizen noch Stift und Papier das hilft mir beim Fokus.",
"techStackTitle": "Mein Tech Stack",
"hobbiesTitle": "Wenn ich nicht code",
"techStack": {
"categories": {
"frontendMobile": "Frontend & Mobile",
"backendDevops": "Backend & DevOps",
"toolsAutomation": "Tools & Automation",
"securityAdmin": "Security & Admin"
},
"items": {
"selfHostedServices": "Self-hosted Services"
}
},
"hobbies": {
"selfHosting": "Self-Hosting & DevOps",
"gaming": "Gaming",
"gameServers": "Game-Server einrichten",
"jogging": "Joggen zum Kopf freibekommen und aktiv bleiben"
}
},
"projects": {
"title": "Ausgewählte Projekte",
"subtitle": "Eine Auswahl an Projekten, an denen ich gearbeitet habe von Web-Apps bis zu Experimenten.",
"featured": "Featured",
"viewAll": "Alle Projekte ansehen"
},
"contact": {
"title": "Kontakt",
"subtitle": "Du willst zusammenarbeiten oder hast Fragen zu meinen Projekten? Schreib mir gerne!",
"getInTouch": "Melde dich",
"getInTouchBody": "Ich bin immer offen für neue Chancen, spannende Projekte oder einfach einen Tech-Talk."
"getInTouchBody": "Ich bin immer offen für neue Chancen, spannende Projekte oder einfach einen Tech-Talk.",
"info": {
"email": "E-Mail",
"location": "Ort",
"locationValue": "Osnabrück, Deutschland"
},
"form": {
"title": "Nachricht senden",
"sending": "Sende Nachricht…",
"send": "Nachricht senden",
"labels": {
"name": "Name",
"email": "E-Mail",
"subject": "Betreff",
"message": "Nachricht",
"requiredMarker": "*"
},
"placeholders": {
"name": "Dein Name",
"email": "dein@email.de",
"subject": "Worum gehts?",
"message": "Erzähl mir mehr über dein Projekt oder deine Frage…"
},
"errors": {
"nameRequired": "Name ist erforderlich",
"nameMin": "Name muss mindestens 2 Zeichen haben",
"emailRequired": "E-Mail ist erforderlich",
"emailInvalid": "Bitte eine gültige E-Mail-Adresse eingeben",
"subjectRequired": "Betreff ist erforderlich",
"subjectMin": "Betreff muss mindestens 3 Zeichen haben",
"messageRequired": "Nachricht ist erforderlich",
"messageMin": "Nachricht muss mindestens 10 Zeichen haben"
},
"characters": "{count} Zeichen"
}
}
}
,
"footer": {
"role": "Software Engineer",
"madeIn": "Made in Germany",
"legalNotice": "Impressum",
"privacyPolicy": "Datenschutz",
"privacySettings": "Datenschutz-Einstellungen",
"privacySettingsTitle": "Datenschutz-Banner wieder anzeigen",
"builtWith": "Built with"
}
}

View File

@@ -17,9 +17,11 @@
"essential": "Essential",
"analytics": "Analytics",
"chat": "Chatbot",
"alwaysOn": "Always on",
"acceptAll": "Accept all",
"acceptSelected": "Accept selected",
"rejectAll": "Reject all"
"rejectAll": "Reject all",
"hide": "Hide"
}
,
"home": {
@@ -39,14 +41,83 @@
"p2": "I love building full-stack web applications with Next.js and mobile apps with Flutter. But what really excites me is DevOps: I run my own infrastructure and automate deployments with CI/CD.",
"p3": "When I'm not coding or tinkering with servers, you'll find me gaming, jogging, or experimenting with automation workflows.",
"funFactTitle": "Fun Fact",
"funFactBody": "Even though I automate a lot, I still use pen and paper for my calendar and notes it helps me stay focused."
"funFactBody": "Even though I automate a lot, I still use pen and paper for my calendar and notes it helps me stay focused.",
"techStackTitle": "My Tech Stack",
"hobbiesTitle": "When I'm Not Coding",
"techStack": {
"categories": {
"frontendMobile": "Frontend & Mobile",
"backendDevops": "Backend & DevOps",
"toolsAutomation": "Tools & Automation",
"securityAdmin": "Security & Admin"
},
"items": {
"selfHostedServices": "Self-hosted services"
}
},
"hobbies": {
"selfHosting": "Self-Hosting & DevOps",
"gaming": "Gaming",
"gameServers": "Setting up game servers",
"jogging": "Jogging to clear my mind and stay active"
}
},
"projects": {
"title": "Selected Works",
"subtitle": "A collection of projects I've worked on, ranging from web applications to experiments.",
"featured": "Featured",
"viewAll": "View All Projects"
},
"contact": {
"title": "Contact Me",
"subtitle": "Interested in working together or have questions about my projects? Feel free to reach out!",
"getInTouch": "Get In Touch",
"getInTouchBody": "I'm always available to discuss new opportunities, interesting projects, or simply chat about technology and innovation."
"getInTouchBody": "I'm always available to discuss new opportunities, interesting projects, or simply chat about technology and innovation.",
"info": {
"email": "Email",
"location": "Location",
"locationValue": "Osnabrück, Germany"
},
"form": {
"title": "Send Message",
"sending": "Sending message…",
"send": "Send Message",
"labels": {
"name": "Name",
"email": "Email",
"subject": "Subject",
"message": "Message",
"requiredMarker": "*"
},
"placeholders": {
"name": "Your name",
"email": "your@email.com",
"subject": "What's this about?",
"message": "Tell me more about your project or question…"
},
"errors": {
"nameRequired": "Name is required",
"nameMin": "Name must be at least 2 characters",
"emailRequired": "Email is required",
"emailInvalid": "Please enter a valid email address",
"subjectRequired": "Subject is required",
"subjectMin": "Subject must be at least 3 characters",
"messageRequired": "Message is required",
"messageMin": "Message must be at least 10 characters"
},
"characters": "{count} characters"
}
}
}
,
"footer": {
"role": "Software Engineer",
"madeIn": "Made in Germany",
"legalNotice": "Legal notice",
"privacyPolicy": "Privacy policy",
"privacySettings": "Privacy settings",
"privacySettingsTitle": "Show privacy settings banner again",
"builtWith": "Built with"
}
}