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:
@@ -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'm Not Coding
|
||||
{t("hobbiesTitle")}
|
||||
</motion.h3>
|
||||
<div className="space-y-3">
|
||||
{hobbies.map((hobby, idx) => (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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>
|
||||
|
||||
@@ -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 geht’s?",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user