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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
148 lines
12 KiB
Markdown
148 lines
12 KiB
Markdown
# 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.
|