refactor(water-log): add i18n foundation for mobile EN/ES
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:
Tyler
2026-07-02 10:59:40 -06:00
parent c3da54af50
commit b29caa0f30
3 changed files with 498 additions and 0 deletions
+93
View File
@@ -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;