refactor(water-log): add i18n foundation for mobile EN/ES
Deploy to route.crispygoat.com / deploy (push) Successful in 4m16s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m16s
NEW infrastructure for upcoming mobile app localization:
- src/lib/water-log/i18n.ts
Shared EN/ES label dictionary for the mobile (/water) portal.
Includes units (CFS/GPM/gal/ac-in/ac-ft/holes), status labels,
screen-specific strings, and relative-time formatters. No React,
no next/headers — safe to import from client, server, and tests.
- src/lib/water-log/lang-context.tsx
<LangProvider> + useLang() hook. Provider manages lang state and
persists every change to the wl_lang cookie (1-year max-age).
- src/components/water/LanguageToggle.tsx
Reusable EN | ES pill component for the mobile nav bar.
Themed for the Tuxedo green palette.
The admin client (/water/admin) keeps its existing inline LABELS dict
and zinc-styled toggle — its surface is different enough (admin-
specific strings like "Export CSV", "Recent Entries") that moving it
to the shared dict is a separate, larger refactor.
No behavior change. Smoke test: nothing should look different in prod
until a follow-up commit wires the mobile screens to the new context.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* LanguageToggle — EN | ES segmented pill.
|
||||
*
|
||||
* A compact two-button control that switches the active language.
|
||||
* Used in both the mobile app (header of the headgate list) and
|
||||
* the admin client (settings page).
|
||||
*
|
||||
* The pill is intentionally narrow so it can fit in a sticky nav bar
|
||||
* next to the logout icon. The active button has the brand-green
|
||||
* background; the inactive is a subtle pill outline.
|
||||
*/
|
||||
|
||||
import { useLang } from "@/lib/water-log/lang-context";
|
||||
|
||||
type Props = {
|
||||
/** Visual size. `sm` fits inside a 44pt iOS-style nav bar. */
|
||||
size?: "sm" | "md";
|
||||
};
|
||||
|
||||
export function LanguageToggle({ size = "sm" }: Props) {
|
||||
const { lang, setLang } = useLang();
|
||||
|
||||
const isSmall = size === "sm";
|
||||
const containerStyle: React.CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "stretch",
|
||||
borderRadius: 999,
|
||||
background: "rgba(120,120,128,0.14)",
|
||||
padding: 2,
|
||||
height: isSmall ? 28 : 32,
|
||||
};
|
||||
|
||||
const baseButton: React.CSSProperties = {
|
||||
appearance: "none",
|
||||
border: 0,
|
||||
borderRadius: 999,
|
||||
padding: isSmall ? "0 9px" : "0 12px",
|
||||
fontSize: isSmall ? 12 : 13,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.06em",
|
||||
cursor: "pointer",
|
||||
transition: "background-color 160ms, color 160ms",
|
||||
minWidth: isSmall ? 30 : 36,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
};
|
||||
|
||||
const activeStyle: React.CSSProperties = {
|
||||
...baseButton,
|
||||
background: "#14532d",
|
||||
color: "#ffffff",
|
||||
boxShadow: "0 1px 2px rgba(20,83,45,0.20)",
|
||||
};
|
||||
|
||||
const inactiveStyle: React.CSSProperties = {
|
||||
...baseButton,
|
||||
background: "transparent",
|
||||
color: "rgba(60,60,67,0.65)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Language"
|
||||
data-testid="language-toggle"
|
||||
style={containerStyle}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLang("en")}
|
||||
aria-pressed={lang === "en"}
|
||||
aria-label="English"
|
||||
style={lang === "en" ? activeStyle : inactiveStyle}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLang("es")}
|
||||
aria-pressed={lang === "es"}
|
||||
aria-label="Español"
|
||||
style={lang === "es" ? activeStyle : inactiveStyle}
|
||||
>
|
||||
ES
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageToggle;
|
||||
Reference in New Issue
Block a user