feat(water-log): add 'holes' unit + EN/ES language toggle for mobile
Deploy to route.crispygoat.com / deploy (push) Successful in 4m2s

- 'holes' / 'hoyos' added to MOBILE_UNITS alongside CFS/GPM/gal/ac-in/ac-ft
  with integer-only validation: submit button greys out and the keyboard
  raises the numeric keypad (no decimal) when the unit is selected.
- Language toggle in the headgate list header (next to logout) flips
  every screen between English and Spanish. Choice is persisted to the
  existing 'wl_lang' cookie for 1y, so the user's last selection
  sticks across logout/relogin.
- i18n strings lifted into shared src/lib/water-log/i18n.ts (single
  source of truth, en/es parity enforced by tests). All mobile screens
  (Pin, HeadgateList, LogForm, Success) plus the loadHeadgates error
  in the orchestrator consume the dictionary. Brand strip, aria-labels,
  segmented control labels, success recap, and submit-button labels
  all go through getT()/formatUnit()/displayUnit().
- 26 new unit tests (tests/unit/water-log-i18n.test.ts) cover: en/es
  dict parity, holes catalog + hoyos translation, all the
  detectInitialLang() branches (SSR/cookie/navigator), setLangCookie
  read/write, label interpolation helpers, and the unit catalog.

Refs /tmp/refactor-water-log.md (Commit B plan)
This commit is contained in:
Tyler
2026-07-02 11:18:23 -06:00
parent b29caa0f30
commit 7795263944
9 changed files with 613 additions and 111 deletions
+43
View File
@@ -45,6 +45,14 @@ export type Labels = {
save: string;
saving: string;
back: string;
signOut: string;
a11yBackToHeadgates: string;
a11yChangeHeadgate: string;
a11ySignOut: string;
};
/** The brand strip shown on the PIN and Success screens. */
brand: {
waterLog: string;
};
/** `/water` PIN entry screen. */
pin: {
@@ -54,6 +62,8 @@ export type Labels = {
unlock: string;
forgot: string;
genericError: string;
a11yPin: string;
a11yPinEntry: string;
};
/** `/water` headgate list screen. */
headgates: {
@@ -65,14 +75,19 @@ export type Labels = {
pullToRefresh: string;
thresholdHigh: string;
thresholdLow: string;
refresh: string;
refreshing: string;
loadError: string;
};
/** `/water` measurement entry screen. */
log: {
title: string;
measurement: string;
unit: string;
timestamp: string;
loggedNow: string;
notes: string;
notesOptional: string;
notesPlaceholder: string;
notesVisibility: string;
measurementError: string;
@@ -124,6 +139,13 @@ export const LABELS: Record<Lang, Labels> = {
save: "Save",
saving: "Saving…",
back: "Back",
signOut: "Sign out",
a11yBackToHeadgates: "Back to headgates",
a11yChangeHeadgate: "Change headgate",
a11ySignOut: "Sign out",
},
brand: {
waterLog: "TUXEDO WATER LOG",
},
pin: {
title: "Enter your PIN",
@@ -132,6 +154,8 @@ export const LABELS: Record<Lang, Labels> = {
unlock: "Unlock",
forgot: "Forgot your PIN? Ask the ditch supervisor.",
genericError: "Something went wrong. Try again.",
a11yPin: "PIN",
a11yPinEntry: "PIN entry — tap to focus",
},
headgates: {
title: "Headgates",
@@ -143,13 +167,18 @@ export const LABELS: Record<Lang, Labels> = {
pullToRefresh: "Pull down to refresh the list",
thresholdHigh: "Hi",
thresholdLow: "Lo",
refresh: "Refresh",
refreshing: "Checking…",
loadError: "Couldn't load headgates. Pull down to try again.",
},
log: {
title: "Log Reading",
measurement: "Measurement",
unit: "Unit",
timestamp: "Timestamp",
loggedNow: "Logged now",
notes: "Notes",
notesOptional: "optional",
notesPlaceholder:
"Anything the supervisor should know about this reading?",
notesVisibility: "Visible to admins only",
@@ -197,6 +226,13 @@ export const LABELS: Record<Lang, Labels> = {
save: "Guardar",
saving: "Guardando…",
back: "Atrás",
signOut: "Salir",
a11yBackToHeadgates: "Volver a compuertas",
a11yChangeHeadgate: "Cambiar compuerta",
a11ySignOut: "Cerrar sesión",
},
brand: {
waterLog: "REGISTRO DE AGUA TUXEDO",
},
pin: {
title: "Ingresa tu PIN",
@@ -205,6 +241,8 @@ export const LABELS: Record<Lang, Labels> = {
unlock: "Desbloquear",
forgot: "¿Olvidaste tu PIN? Pregúntale al supervisor.",
genericError: "Algo salió mal. Inténtalo de nuevo.",
a11yPin: "PIN",
a11yPinEntry: "Entrada de PIN — toca para enfocar",
},
headgates: {
title: "Compuertas",
@@ -216,13 +254,18 @@ export const LABELS: Record<Lang, Labels> = {
pullToRefresh: "Jala hacia abajo para actualizar la lista",
thresholdHigh: "Alto",
thresholdLow: "Bajo",
refresh: "Actualizar",
refreshing: "Verificando…",
loadError: "No se pudieron cargar las compuertas. Jala para intentar de nuevo.",
},
log: {
title: "Registrar lectura",
measurement: "Medición",
unit: "Unidad",
timestamp: "Hora",
loggedNow: "Registrando ahora",
notes: "Notas",
notesOptional: "opcional",
notesPlaceholder:
"¿Algo que el supervisor deba saber sobre esta lectura?",
notesVisibility: "Visible solo para administradores",