Updated PLANS.md, AGENTS.md, and arabic-localization.md to reflect the “foundations now, full translations later” approach and marked progress accordingly.
Implemented localization foundations across backend and frontend (locale settings/middleware, preferred language, i18n wiring, RTL support, minimal Arabic UI strings, Accept-Language). Added targeted backend and frontend tests plus a risks note for pending full translation coverage.
This commit is contained in:
+31
-12
@@ -1,10 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { apiGet } from "./api/client";
|
||||
import { setLocale } from "./i18n";
|
||||
|
||||
export default function App() {
|
||||
const [salons, setSalons] = useState([]);
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState("idle");
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
@@ -33,15 +36,31 @@ export default function App() {
|
||||
return (
|
||||
<div className="page">
|
||||
<header className="hero">
|
||||
<p className="eyebrow">Salon Booking Platform</p>
|
||||
<h1>Find, compare, and book top salons near you.</h1>
|
||||
<p className="subtitle">
|
||||
Search by city or service, compare pricing, and lock in your slot in seconds.
|
||||
</p>
|
||||
<div className="hero-top">
|
||||
<p className="eyebrow">{t("hero.eyebrow")}</p>
|
||||
<div className="locale-switch" aria-label={t("locale.label")}>
|
||||
<button
|
||||
type="button"
|
||||
className={i18n.language === "ar-sa" ? "active" : ""}
|
||||
onClick={() => setLocale("ar-sa")}
|
||||
>
|
||||
{t("locale.arabic")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={i18n.language === "en" ? "active" : ""}
|
||||
onClick={() => setLocale("en")}
|
||||
>
|
||||
{t("locale.english")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h1>{t("hero.title")}</h1>
|
||||
<p className="subtitle">{t("hero.subtitle")}</p>
|
||||
<div className="search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by salon or service"
|
||||
placeholder={t("hero.searchPlaceholder")}
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
@@ -49,12 +68,12 @@ export default function App() {
|
||||
</header>
|
||||
|
||||
<section className="results">
|
||||
<h2>Salons</h2>
|
||||
{status === "loading" && <p>Loading salons...</p>}
|
||||
<h2>{t("results.title")}</h2>
|
||||
{status === "loading" && <p>{t("results.loading")}</p>}
|
||||
{status === "error" && (
|
||||
<p className="error">Unable to load salons. Start the backend API to see results.</p>
|
||||
<p className="error">{t("results.error")}</p>
|
||||
)}
|
||||
{status === "ready" && salons.length === 0 && <p>No salons found.</p>}
|
||||
{status === "ready" && salons.length === 0 && <p>{t("results.empty")}</p>}
|
||||
<div className="grid">
|
||||
{salons.map((salon) => (
|
||||
<article className="card" key={salon.id}>
|
||||
@@ -62,10 +81,10 @@ export default function App() {
|
||||
<h3>{salon.name}</h3>
|
||||
<span className="rating">{salon.rating_avg} / 5</span>
|
||||
</div>
|
||||
<p>{salon.description || "No description yet."}</p>
|
||||
<p>{salon.description || t("card.noDescription")}</p>
|
||||
<div className="meta">
|
||||
<span>{salon.city}</span>
|
||||
<span>{salon.phone_number || "Phone unavailable"}</span>
|
||||
<span>{salon.phone_number || t("card.phoneUnavailable")}</span>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user