feat: integrate i18n for multilingual support in Services page
- Added i18n configuration in src/i18n.ts for English and Dutch translations. - Created locale files for English (en.json) and Dutch (nl.json) with translations for various sections. - Updated Services component to utilize translation keys for titles, descriptions, and items. - Refactored process steps in Services component to be dynamically rendered from translation keys. - Adjusted main.tsx to include i18n initialization.
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { Link } from "react-router";
|
||||
import { Phone, Mail, MapPin, Clock, Wrench, Facebook, Instagram, Twitter } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<footer className="bg-gray-900 text-gray-300">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
@@ -10,10 +13,12 @@ export function Footer() {
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Wrench className="h-8 w-8 text-orange-600" />
|
||||
<span className="font-bold text-xl text-white">Lucris Handyman</span>
|
||||
<span className="font-bold text-xl text-white">
|
||||
{t("footer.brand")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm mb-4">
|
||||
Professional handyman services for all your home repair and maintenance needs. Quality work, affordable prices.
|
||||
{t("footer.description")}
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
<a href="#" className="hover:text-orange-600 transition-colors">
|
||||
@@ -30,31 +35,33 @@ export function Footer() {
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-4">Quick Links</h3>
|
||||
<h3 className="text-white font-semibold mb-4">
|
||||
{t("footer.quickLinks")}
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link to="/" className="hover:text-orange-600 transition-colors">
|
||||
Home
|
||||
{t("footer.links.home")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/services" className="hover:text-orange-600 transition-colors">
|
||||
Services
|
||||
{t("footer.links.services")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/about" className="hover:text-orange-600 transition-colors">
|
||||
About Us
|
||||
{t("footer.links.about")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/gallery" className="hover:text-orange-600 transition-colors">
|
||||
Gallery
|
||||
{t("footer.links.gallery")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/contact" className="hover:text-orange-600 transition-colors">
|
||||
Contact
|
||||
{t("footer.links.contact")}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -62,47 +69,53 @@ export function Footer() {
|
||||
|
||||
{/* Services */}
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-4">Our Services</h3>
|
||||
<h3 className="text-white font-semibold mb-4">
|
||||
{t("footer.ourServices")}
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>Plumbing Repairs</li>
|
||||
<li>Electrical Work</li>
|
||||
<li>Painting & Drywall</li>
|
||||
<li>Carpentry</li>
|
||||
<li>General Repairs</li>
|
||||
<li>Home Maintenance</li>
|
||||
<li>{t("footer.services.plumbing")}</li>
|
||||
<li>{t("footer.services.electrical")}</li>
|
||||
<li>{t("footer.services.painting")}</li>
|
||||
<li>{t("footer.services.carpentry")}</li>
|
||||
<li>{t("footer.services.general")}</li>
|
||||
<li>{t("footer.services.maintenance")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-4">Contact Info</h3>
|
||||
<h3 className="text-white font-semibold mb-4">
|
||||
{t("footer.contactInfo")}
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start space-x-3">
|
||||
<Phone className="h-5 w-5 text-orange-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm">Call Us</p>
|
||||
<p className="text-sm">{t("footer.callUs")}</p>
|
||||
<p className="text-white">(555) 123-4567</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start space-x-3">
|
||||
<Mail className="h-5 w-5 text-orange-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm">Email Us</p>
|
||||
<p className="text-sm">{t("footer.emailUs")}</p>
|
||||
<p className="text-white">info@lucris.nl</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start space-x-3">
|
||||
<MapPin className="h-5 w-5 text-orange-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm">Location</p>
|
||||
<p className="text-white">123 Main Street<br />New York, NY 10001</p>
|
||||
<p className="text-sm">{t("footer.location")}</p>
|
||||
<p className="text-white">
|
||||
123 Main Street<br />New York, NY 10001
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start space-x-3">
|
||||
<Clock className="h-5 w-5 text-orange-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm">Working Hours</p>
|
||||
<p className="text-white">Mon-Sat: 8AM-6PM</p>
|
||||
<p className="text-sm">{t("footer.hours")}</p>
|
||||
<p className="text-white">{t("footer.hoursValue")}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -110,7 +123,7 @@ export function Footer() {
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-sm">
|
||||
<p>© 2026 Lucris Handyman. All rights reserved.</p>
|
||||
<p>{t("footer.copyright")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { Menu, X, Wrench } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||
|
||||
export function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const navItems = [
|
||||
{ name: "Home", path: "/" },
|
||||
{ name: "Services", path: "/services" },
|
||||
{ name: "About", path: "/about" },
|
||||
{ name: "Gallery", path: "/gallery" },
|
||||
{ name: "Contact", path: "/contact" },
|
||||
{ name: t("header.nav.home"), path: "/" },
|
||||
{ name: t("header.nav.services"), path: "/services" },
|
||||
{ name: t("header.nav.about"), path: "/about" },
|
||||
{ name: t("header.nav.gallery"), path: "/gallery" },
|
||||
{ name: t("header.nav.contact"), path: "/contact" },
|
||||
];
|
||||
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
@@ -23,11 +26,13 @@ export function Header() {
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center space-x-2">
|
||||
<Wrench className="h-8 w-8 text-orange-600" />
|
||||
<span className="font-bold text-xl text-gray-900">Lucris Handyman</span>
|
||||
<span className="font-bold text-xl text-gray-900">
|
||||
{t("header.brand")}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex space-x-8">
|
||||
<nav className="hidden md:flex items-center space-x-8">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
@@ -41,6 +46,7 @@ export function Header() {
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
<LanguageSwitcher />
|
||||
</nav>
|
||||
|
||||
{/* CTA Button */}
|
||||
@@ -48,7 +54,7 @@ export function Header() {
|
||||
to="/contact"
|
||||
className="hidden md:block bg-orange-600 text-white px-6 py-2 rounded-md hover:bg-orange-700 transition-colors"
|
||||
>
|
||||
Get a Quote
|
||||
{t("header.cta")}
|
||||
</Link>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
@@ -81,12 +87,15 @@ export function Header() {
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
<div className="mt-4">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<Link
|
||||
to="/contact"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="block mt-4 bg-orange-600 text-white px-6 py-2 rounded-md text-center hover:bg-orange-700 transition-colors"
|
||||
>
|
||||
Get a Quote
|
||||
{t("header.cta")}
|
||||
</Link>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function FlagGB() {
|
||||
return (
|
||||
<svg viewBox="0 0 60 30" aria-hidden="true" className="h-4 w-6">
|
||||
<clipPath id="t"><path d="M0 0h60v30H0z" /></clipPath>
|
||||
<g clipPath="url(#t)">
|
||||
<path d="M0 0v30h60V0z" fill="#012169" />
|
||||
<path d="M0 0l60 30m0-30L0 30" stroke="#fff" strokeWidth="6" />
|
||||
<path d="M0 0l60 30m0-30L0 30" stroke="#C8102E" strokeWidth="4" />
|
||||
<path d="M30 0v30M0 15h60" stroke="#fff" strokeWidth="10" />
|
||||
<path d="M30 0v30M0 15h60" stroke="#C8102E" strokeWidth="6" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function FlagNL() {
|
||||
return (
|
||||
<svg viewBox="0 0 3 2" aria-hidden="true" className="h-4 w-6">
|
||||
<rect width="3" height="2" fill="#fff" />
|
||||
<rect width="3" height="0.6667" y="0" fill="#AE1C28" />
|
||||
<rect width="3" height="0.6667" y="1.3333" fill="#21468B" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
const current = i18n.language === "nl" ? "nl" : "en";
|
||||
const other = current === "en" ? "nl" : "en";
|
||||
|
||||
const setLang = (lng: "en" | "nl") => i18n.changeLanguage(lng);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLang(other as "en" | "nl")}
|
||||
aria-label={t("header.language")}
|
||||
className="flex items-center gap-1 bg-white px-2 py-1 text-sm hover:bg-gray-50"
|
||||
>
|
||||
{other === "en" ? <FlagGB /> : <FlagNL />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user