Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

148 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PRD — i18n Hardening: Locale-Aware Formatting, Gap Backfill, Pluralization, CI Enforcement
- Slug: `2026-06-10-i18n-formatting-and-gaps`
- Date: `2026-06-10`
- Status: Draft
## Summary
Close the remaining systemic i18n gaps that sit *around* string translation rather than in it. Five phases, each independently shippable as a PR:
| Phase | Area | Size |
|-------|------|------|
| P1 | Locale-aware formatting components + pseudo-locales in dev mode | ~8 files |
| P2 | Backfill 555 missing English locale keys (+ 7 locale translations) | ~20 namespaces |
| P3 | Translate skipped surfaces: `msp-composition` client/contact tabs + interactions feeds | 6 components |
| P4 | Pluralization: migrate dead legacy `_plural` keys, fix CLDR-unaware validator | 4 keys + validator |
| P5 | CI enforcement: `find-missing-i18n-keys.cjs` in CI with full path triggers | 1 workflow |
## Problem
1. **Formatting ignores locale.** `CurrencyInput`, `DatePicker`, `DateTimePicker`, `Calendar`, and `ReportEngine` hardcode `en-US`/`MM/dd/yyyy`, so even fully translated screens render US formats for all users.
2. **555 keys referenced in code don't exist in English locale files** (`find-missing-i18n-keys.cjs`), so users see default-value fallbacks or raw keys. Concentrated in workflows (131), email-providers (111), integrations (81), features/tickets (81), clients (54), assets (40), user-activities (37).
3. **`packages/msp-composition` was never claimed by any translation batch.** The MSP i18n plans were organized by feature package; the composition layer that renders the Tickets/Assets tabs on client and contact pages (`MspClientTickets`, `MspClientAssets`, `MspContactTickets`) is fully hardcoded. The interactions feeds (`InteractionsFeed`, `OverallInteractionsFeed`, `SchedulingInteractionDetails`) were similarly skipped.
4. **Legacy `key_plural` keys are dead.** i18next v25 runs in v4 plural mode (no `compatibilityJSON` is set anywhere), so the 4 legacy `_plural` keys in `en/msp/settings.json` and `en/msp/profile.json` never resolve. Worse, `validate-translations.cjs` is CLDR-unaware: it falsely warns on Polish `_few`/`_many` forms (which are *required* for Polish) while not flagging genuinely missing plural forms.
5. **Pseudo-locales (`xx`/`yy`) were meant to be selectable in dev mode** but `filterPseudoLocales()` strips them unconditionally — the original intent slipped through the cracks.
6. **No CI guard against the "missing key" class of regression.** `find-missing-i18n-keys.cjs` exits 1 on failure but is manual-only, and the `validate-translations.yml` workflow only triggers on locale-file paths, so component changes never run it.
## Goals
1. All shared formatting components and ReportEngine derive formats from the active locale (purely locale-derived; no new preference UI).
2. Pseudo-locales appear in all language pickers when `NODE_ENV === 'development'`.
3. `find-missing-i18n-keys.cjs` reports 0 missing keys; backfilled keys translated into all 7 non-English production locales + regenerated pseudo-locales.
4. Client-page Tickets/Assets/Interactions tabs (and contact-page tickets) are fully translated.
5. All plural keys use i18next v4 count-based forms (`_one`/`_other`/`_few`/`_many` per CLDR); validator understands CLDR plural categories per locale.
6. Both i18n scripts run in CI on every PR that touches components or locale files.
## Non-goals
- Transactional EJS emails (verify/reset/welcome) — explicitly deferred.
- Invoice PDF / AssemblyScript-WASM renderer localization.
- Page metadata (`generateMetadata`) localization.
- RTL support.
- Explicit user-facing date-format preference (separate future effort; this plan stays locale-derived so that effort layers on top).
- ESLint rule against hardcoded JSX strings (deferred; revisit after P5 proves out).
- Completing the `pt` locale (stays in `INCOMPLETE_LOCALES`).
- Translating ui-reflection `label`/`helperText` automation metadata (not user-visible UI).
## Requirements
### P1a — Locale-aware formatting components
Foundation: add `useOptionalI18n()` to `packages/ui/src/lib/i18n/client.tsx` — same as `useI18n()` but returns `null` outside `I18nProvider` (DatePicker/CurrencyInput render on auth pages without the provider; they must not crash). Locale falls back to `LOCALE_CONFIG.defaultLocale`. `packages/ui/src/lib/dateFnsLocale.ts` already maps all 10 locales to date-fns locale objects.
| Component | Change |
|-----------|--------|
| `DatePicker.tsx:117` | `format(value, 'P', { locale: getDateFnsLocale(locale) })`; optional `displayFormat` prop escape hatch |
| `DateTimePicker.tsx:182` | Explicit `timeFormat` prop wins (`'P hh:mm a'` / `'P HH:mm'`); unset → `'P p'` |
| `Calendar.tsx` | Pass date-fns locale to `DayPicker`; localize `MonthYearSelect` caption `format()` calls |
| `CurrencyInput.tsx` | Locale-aware format **and parse** (see risk below) |
| `PrintOptionsDialog.tsx:67` | `formatDefaultPrintValue` uses active locale instead of bare `toLocaleString()` |
| `ReportEngine.ts` (both copies) | `locale?: string` on `ReportExecutionOptions`, threaded to all 5 `format*` call sites; default `'en-US'` preserves behavior for untouched callers. `executeReport` action resolves locale via `getHierarchicalLocaleAction` from `@alga-psa/tenancy` |
**Critical risk — CurrencyInput parsing:** current code does `raw.replace(/,/g, '')` + `parseFloat`. If formatting becomes French (`1 234,56`) but parsing stays US, typing `12,5` yields **1250** — silent 100× data corruption. Formatting and parsing must change as a unit: derive group/decimal separators from `Intl.NumberFormat(locale).formatToParts()`, strip group separators, normalize the decimal separator to `.` before `parseFloat`. Round-trip tests for `en`, `de`, `fr`, `pl` mandatory. Only 2 usage sites, limiting blast radius.
Audit step: review DatePicker's 37+ call sites for any that rely on `MM/dd/yyyy` output for parsing/serialization rather than pure display.
### P1b — Pseudo-locales in dev mode
Single change point: `filterPseudoLocales()` in `packages/core/src/lib/i18n/config.ts:127` keeps `xx`/`yy` when `process.env.NODE_ENV === 'development'`. All 7 pickers already route through it. `INCOMPLETE_LOCALES` (`pt`) stays filtered even in dev. Precedent for `NODE_ENV` branching exists in the same file (`cookie.secure`, i18next `debug`); Next.js inlines it client-side. Rebuild `packages/core` dist (`npx nx build core`).
### P2 — Missing English keys backfill (555)
Backfill `en` first (defaults are mostly recoverable from `defaultValue` in the `t()` calls), then translate to fr/es/de/nl/it/pl/pt, then regenerate pseudo-locales. Namespace order by user impact:
| Namespace | Keys | Primary files |
|-----------|------|---------------|
| msp/workflows | 131 | `WorkflowRunList`, `WorkflowDesigner`, `RunStudioShell` (EE) |
| msp/email-providers | 111 | `InboundEmailRuleForm` (83 refs) |
| msp/integrations | 81 | `LevelIoIntegrationSettings` (61), `InboundEmailRulesManager` |
| features/tickets | 81 | `TicketChecklistSection` (30), `TicketingDashboard` (23) |
| msp/clients | 54 | `Clients.tsx`, `MeetingAttendeesPicker`, `ClientDetails` |
| msp/assets | 40 | `AssetDashboardClient` (37) |
| msp/user-activities | 37 | `ActivitiesTableFilters`, `AdHocDetailPanel` |
| ~13 others | ~120 | incl. cross-namespace `actions.print`/`actions.printOptions` cluster (print/export feature shipped without any keys) |
Exit criterion: `node scripts/find-missing-i18n-keys.cjs` exits 0.
### P3 — Skipped surfaces
| Component | Location | Namespace |
|-----------|----------|-----------|
| `MspClientTickets.tsx` | `packages/msp-composition/src/clients/` | `msp/clients` |
| `MspClientAssets.tsx` | `packages/msp-composition/src/clients/` | `msp/clients` |
| `MspContactTickets.tsx` | `packages/msp-composition/src/clients/` | `msp/contacts` |
| `InteractionsFeed.tsx` | `packages/clients/src/components/interactions/` | `msp/clients` |
| `OverallInteractionsFeed.tsx` | `packages/clients/src/components/interactions/` | `msp/clients` |
| `SchedulingInteractionDetails.tsx` | `packages/scheduling/src/components/shared/` | `msp/schedule` |
Wire `useTranslation()`, add keys to `en` + 7 locales + pseudo-locales. Verify `ROUTE_NAMESPACES` already loads the chosen namespaces on `/msp/clients` and `/msp/contacts` (it does today). Sweep the other 16 `msp-composition` files and document that they have no user-visible strings (providers/wrappers); record per-file in SCRATCHPAD.
### P4 — Pluralization
Inventory (confirmed): 4 legacy `_plural` keys across `en/msp/settings.json` + `en/msp/profile.json` (`teams.details.memberCount_plural`, interaction-types `imported_plural`, `security.sessions.subtitle_plural` ×2) plus counterparts in 7 locales.
1. Migrate to i18next v4 forms: `_one`/`_other` for English; correct CLDR set per locale (Polish: `_one`/`_few`/`_many`/`_other`).
2. Fix consumers doing manual plural selection — `AdminSessionManagement.tsx:239-240` picks `subtitle` vs `subtitle_plural` in code; replace with a single `t()` call using `count`. Note: `subtitle` interpolates two counts (`sessionCount`, `userCount`); pass `count: sessionCount` for plural selection while keeping both interpolation variables.
3. Make `validate-translations.cjs` CLDR-aware via `Intl.PluralRules(locale).resolvedOptions().pluralCategories`: stop false-warning on Polish `_few`/`_many`; **flag** locales missing required plural categories for a key that has plural forms in English.
4. Verify `generate-pseudo-locales.cjs` carries plural-suffixed keys through correctly.
Exit criterion: `validate-translations.cjs` passes with 0 warnings.
### P5 — CI enforcement
- Add a `find-missing-i18n-keys` job to `.github/workflows/validate-translations.yml` (script already exits 1 on failure).
- Expand workflow `paths` triggers to include component sources: `packages/**`, `server/src/**`, `ee/server/src/**`, `scripts/find-missing-i18n-keys.cjs` (currently locale files only — component changes never trigger validation).
- Must land **after** P2 (otherwise CI is red on arrival).
## Rollout
Five PRs, one per phase, in order P1 → P2 → P3 → P4 → P5 (P3/P4 may proceed in parallel after P2; P5 strictly after P2). Each phase leaves `main` green: validation scripts pass, existing `.i18n.test.*` audit suites pass.
## Risks
| Risk | Mitigation |
|------|------------|
| CurrencyInput locale parsing corrupts amounts (100× errors) | Format+parse change as a unit; round-trip tests across 4 locales; only 2 usage sites |
| DatePicker call sites depending on fixed `MM/dd/yyyy` output | Pre-change audit of all 37+ call sites; `displayFormat` escape hatch |
| 555-key backfill drifts from on-screen defaults | Source keys from `defaultValue` in code; pseudo-locale visual QA (unlocked by P1b) |
| Machine-translated backfill quality in 7 locales | Same translation process as prior MSP batches; `pt` already flagged incomplete |
| Plural migration changes rendered strings | Per-key before/after snapshot in tests; only 4 keys |
| CI job surfaces pre-existing failures on unrelated PRs | Land P5 only after P2 zeroes the count |
## Open Questions
1. Namespace for interactions feed keys: reuse `msp/clients` `interactions.*` prefix (where `InteractionDetails`/`QuickAddInteraction` keys already live) — assumed yes.
2. Should `DateTimePicker`'s `timeFormat` default flip to locale-derived everywhere, or keep `12h` default where currently explicit? Assumed: explicit prop wins, unset becomes locale-derived.
3. ESLint hardcoded-string rule — deferred to a follow-up after P5; revisit then.
## Acceptance Criteria / Definition of Done
- [ ] A user with `fr` locale sees `dd/MM/yyyy`-style dates in DatePicker/DateTimePicker/Calendar, French month/weekday names, and `1 234,56`-style numbers in CurrencyInput — and typed `12,5` parses as 12.5.
- [ ] Reports render dates/numbers/currency in the viewer's hierarchical locale; callers passing no locale get unchanged `en-US` output.
- [ ] In dev mode, all language pickers list `Pseudo (xx)` and `Pseudo (yy)`; in production builds they don't. `pt` hidden in both.
- [ ] `node scripts/find-missing-i18n-keys.cjs` exits 0.
- [ ] Client page Tickets/Assets/Interactions tabs and contact page Tickets tab render fully in the active locale (verified via pseudo-locale).
- [ ] No `_plural`-suffixed keys remain in any locale file; `validate-translations.cjs` passes with 0 errors and 0 warnings; Polish plural forms render correctly for counts 1, 2, 5.
- [ ] Both scripts run in CI on PRs touching `packages/**`, `server/src/**`, `ee/server/src/**`, or locale files.