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

12 KiB
Raw Blame History

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.