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
18 KiB
18 KiB
Scratchpad — i18n Hardening: Formatting, Gaps, Pluralization, CI
- Plan slug:
2026-06-10-i18n-formatting-and-gaps - Companion files:
PRD.md,features.json,tests.json
Discoveries (2026-06-10, pre-implementation investigation)
Formatting components
packages/ui/src/components/CurrencyInput.tsx:16hardcodesIntl.NumberFormat('en-US'). Bigger issue: parsing at lines 44/53 doesraw.replace(/,/g, '')+parseFloat— US-only. Locale-aware formatting without locale-aware parsing → French12,5parses as 1250 (silent 100× corruption). Format+parse must change as a unit. Only 2 usage sites repo-wide.DatePicker.tsx:117andDateTimePicker.tsx:182hardcodeMM/dd/yyyydate-fns patterns (the often-cited "lines 11/10" in earlier analysis pointed at imports; real lines are 117/182).packages/ui/src/lib/dateFnsLocale.tsalready maps all 10 locales to date-fns locale objects (getDateFnsLocale) — no new locale-data plumbing needed. xx/yy map to enUS.useFormatters()inpackages/ui/src/lib/i18n/client.tsx:236already does Intl-based locale formatting;useI18n()throws outsideI18nProvider(client.tsx:182) — DatePicker/CurrencyInput render on auth pages without the provider, hence the newuseOptionalI18n().I18nProvideris mounted viaI18nWrapperfrom@alga-psa/tenancy/componentsinMspLayoutClient.tsx:178andClientPortalLayoutClient.tsx; also on some auth pages.- ReportEngine exists in two near-identical copies:
packages/reporting/src/lib/reports/core/ReportEngine.tsandserver/src/lib/reports/core/ReportEngine.ts.en-UShardcoded at lines ~205-228 (currency/number/percentage) and ~257-260 (dates) in each. Allformat*methods are static; entry isReportEngine.execute(definition, parameters, options)called fromexecuteReportactions. Dedup is out of scope — mirror changes. - Locale resolution for server actions:
getHierarchicalLocaleActioninpackages/tenancy/src/actions/locale-actions/getHierarchicalLocale.ts:47(wrapped inwithOptionalAuth). PrintOptionsDialog.tsx:67uses barevalue.toLocaleString()(browser-default locale) — minor, included in P1.
Pseudo-locales
filterPseudoLocales()inpackages/core/src/lib/i18n/config.ts:127strips PSEUDO_LOCALES + INCOMPLETE_LOCALES unconditionally. All 7 pickers route through it:LanguagePreference,ClientLanguagePreference,ClientPortalLanguageConfig,client.tsx:160,ClientInfoStep(onboarding),MspLanguageSettings,ClientPortalSettings.localeNamesalready hasPseudo (xx)/Pseudo (yy)entries;isSupportedLocaleaccepts them → saving the preference Just Works once unfiltered.NODE_ENVbranching precedent in the same file:cookie.secure(line 53),I18N_CONFIG.debug(line 95). Next.js inlinesprocess.env.NODE_ENVclient-side, including pre-built package dist code.- packages/core is pre-built (tsup) → remember
npx nx build coreafter editing.
Missing keys (P2)
node scripts/find-missing-i18n-keys.cjs→ 555 missing English keys; exits 1 on failure (CI-ready), exits 0 when clean.- Breakdown: msp/workflows 131, msp/email-providers 111, msp/integrations 81, features/tickets 81, msp/clients 54, msp/assets 40, msp/user-activities 37, rest ~120 spread across msp/settings (24), common (24), msp/invoicing (22), msp/keyboard-shortcuts (17), msp/quotes (13), msp/contracts (9), projects (7), msp/schedule (7), features/projects (6), msp/knowledge-base (4), features/documents (4), client-portal (4), msp/time-entry (3).
- Worst files:
InboundEmailRuleForm.tsx(83 refs),LevelIoIntegrationSettings.tsx(61, EE),AssetDashboardClient.tsx(37),InboundEmailRulesManager.tsx(33),TicketChecklistSection.tsx(30). - Pattern:
actions.print/actions.printOptionsreferenced across 7+ namespaces, exist in none — print/export feature shipped without keys. - Most
t()calls carrydefaultValue→ en backfill is largely mechanical extraction.
Skipped surfaces (P3) — root cause
- MSP i18n batch plans (ee/docs/plans/2026-02..04-*) were organized by feature package;
packages/msp-composition(the glue layer composing features into pages) was never claimed by any batch. 19/20 files there lackuseTranslation; only 3 have user-visible strings:MspClientTickets.tsx,MspClientAssets.tsx,MspContactTickets.tsx(hardcoded "Client Tickets", "Loading tickets...", "Select Status", "All Priorities", "Search tickets...", "Loading assets...", "All asset types..."). - Wiring:
ClientDetails.tsxrenders tabs viauseClientCrossFeature()→MspClientCrossFeatureProvider.tsx(packages/msp-composition) → these components. Tab ids stay untranslated per 2026-03-24 decision in the clients batch (stable ids). - Interactions split:
InteractionsFeed.tsx,OverallInteractionsFeed.tsx(packages/clients),SchedulingInteractionDetails.tsx(packages/scheduling) have zero i18n;InteractionDetails,QuickAddInteraction,MeetingAttendeesPicker+ 3 settings components are translated (but reference ~26 of the 555 missing keys). - ui-reflection
label/helperTextstrings (automation metadata) — decision: not translated, out of scope (PRD non-goal). AlgaDeskClientCrossFeatureProvider.tsxrenders tickets for AlgaDesk mode (renderClientAssets: () => null) — include in P3 sweep verdicts.
Pluralization (P4) — key finding
- No
compatibilityJSONis set anywhere → i18next v25 runs in v4 plural mode → legacykey_pluralsuffix keys are dead code; they never resolve. - Only 4 legacy
_pluralkeys in en:teams.details.memberCount_plural, interaction-typesimported_plural(both msp/settings.json),security.sessions.subtitle_plural(msp/profile.json + msp/settings.json). - The codebase already mostly uses v4 forms correctly — e.g.
emailLogs.results_one/_otherin en with_one/_few/_many/_otherin pl. validate-translations.cjsis CLDR-unaware: its 8 warnings about pl_few/_many("Extra key not in English") are false positives — those forms are required for Polish. Fix withnew Intl.PluralRules(locale).resolvedOptions().pluralCategories.AdminSessionManagement.tsx:239-240does manual plural selection (subtitlevssubtitle_pluralternary). Gotcha: key interpolates two counts (sessionCount,userCount) — plural selection needscount: sessionCountpassed alongside both variables.TeamDetails.tsx:401already callst('teams.details.memberCount', { count })— currently falls back to the base key since_one/_otherdon't exist; migration makes it actually pluralize.
CI (P5)
.github/workflows/validate-translations.ymltriggers only onserver/public/locales/**+ the two scripts → component-only PRs never validated. Expand paths + add find-missing job. Must land after P2 (else red on arrival).
P1 implementation notes (2026-06-10)
- DatePicker call-site audit (F004/T006): clean. 55 call sites repo-wide (packages 47, server 5, ee 3). Zero parse/serialize/test-assert on the rendered MM/dd/yyyy string; all pass Date objects (DocumentFilters constructs Dates from strings before passing). All 8 test files that touch DatePicker mock it. Three unrelated components format their own strings with MM/dd/yyyy (ContractBasicsStep summary, DeadlineFilter label, DisplaySettings config constant) — untouched by this change.
- DateTimePicker: when
timeFormatprop unset, the internal hour-list/AM-PM UI now derives 12h/24h from the locale viaIntl.DateTimeFormat(locale, {hour:'numeric'}).resolvedOptions().hour12(xx/yy normalized to en), keeping picker UI consistent with the'P p'display. - CurrencyInput parse strategy: keep digits/sign/locale-decimal-separator (normalized to '.'), drop everything else — covers group separators incl. plain-space/NBSP/narrow-NBSP variants users type. Helpers
formatCurrencyValue/parseCurrencyValue/getNumberSeparatorsexported for tests. Default placeholder now locale-formatted0.00. - PrintOptionsDialog: Date cells render via internal
<LocaleDateTime>(useOptionalI18n + Intl dateStyle/timeStyle short) sinceformatDefaultPrintValueis a plain function. - executeReport: explicit
options.localewins; otherwise resolved viagetHierarchicalLocaleAction().packages/reportinggained an@alga-psa/tenancydep (no cycle: tenancy doesn't depend on reporting). Both copies kept byte-identical (cp+ diff). - ReportEngine copies: locale threaded through formatCurrency/formatNumber/formatPercentage/formatDate in both; formatDuration left as-is (English unit words are a translation problem, out of P1 scope).
- T020 satisfied by construction: the two ReportEngine copies differ only in import paths/execute signature (verified by diff), so formatting output is identical.
- T001 inside-provider half: not unit-tested (I18nProvider pulls i18next HTTP backend); covered by the null-outside test + app usage. T017 (PrintOptionsDialog render test) skipped — trivial wrapper. T024/T025 are manual dev-stack QA.
- Pre-existing test failures (not from P1): packages/core — index exports, logger.outputs, deletionMigrations (5-6 tests); packages/ui — Calendar today-button, DeleteEntityDialog ×2, printStylesheet, BoardPicker.keyboard, TreeSelect.contract ×2, command-palette.source (6 files). Verified identical on clean tree at cb6731584f.
P2 implementation notes (2026-06-10)
- Extraction: one-off scanner re-implementation pulled
defaultValuefrom each missing-key t() call; 505/551 keys merged mechanically into en (values are byte-identical to what the UI previously rendered — T033 holds by construction). Conflicting defaults forinboundRules.fields.subject/bodyTextresolved to the capitalized field-label variants (3 of 4 usages). - Scanner fixes (find-missing-i18n-keys.cjs) — needed for exit-0 to be reachable:
- Plural-aware resolution:
t('key', { count })resolves viakey_one/key_other/… in i18next v4; the scanner now accepts CLDR-suffixed variants (e.g.emailLogs.results,quickAdd.tagCreatePartialFailurealready existed as_one/_otherpairs and were false positives). - Skip test files: contract tests assert on
t('…')source literals (AdminWebhooksSetup.ui.contract.test.tsgreps for thesecurity.webhooks.inbound.prefix;i18n.contract.test.tscontains namespace strings) — these aren't runtime calls. Also skip keys ending in '.'.
- Plural-aware resolution:
- Dynamic-default keys hand-resolved:
documents.associatedEntityPicker.*(4 keys →{{entityType}}interpolations),messages.error.saveAllPartial({{sections}}),settings.modHint(template literal →{{modKey}}+ KeyboardShortcutsPanel.tsx now passesmodKey). - header.breadcrumb restructure: msp/core
header.breadcrumbwas a string (nav aria-label) blockingheader.breadcrumb.dashboard/home. Converted to object{label, dashboard, home}in ALL 10 locales (translated label preserved); Header.tsx aria-label now usesheader.breadcrumb.label. - Locale JSON now written with
ensure_ascii=False— a handful of\uXXXXescapes became literal UTF-8 (cosmetic only). - Translation pass: 563 keys/locale (505 + hand-resolved + breadcrumb + small pre-existing drift), 7 parallel subagents → /tmp/i18n-backfill/out//, merged with placeholder-preservation validation (0 problems). The batch contained no new
_one/_otherpairs, so no pl plural expansion was needed. - P2 verification: find-missing exits 0; validate-translations 0 errors / 8 warnings (the pre-existing pl false positives P4 removes). Billing suite baseline comparison (full run, clean tree vs P2 tree): 158 → 156 failed tests, failed-file set strictly shrank — P2 fixed ContractsIntegration T066 (de wizard keys) and InvoicingLocaleSmoke T048 (stale xx pseudo file). InvoicingLocaleSmoke T002 expected-groups list updated for the new
designergroup (2 keys referenced by invoice-designer components). TicketingDashboard.i18n T011 fails pre-existing (source-contract assertion on a file P2 never touched).
P3 implementation notes (2026-06-10)
- Wired with useTranslation + keys (en + 7 locales + pseudo):
MspClientTickets(msp/clientsclientTabs.tickets.*, 13 calls),MspClientAssets(msp/clientsclientTabs.assets.*, 41 calls),MspContactTickets(msp/contactscontactTabs.tickets.*, 17 calls),InteractionsFeed(msp/clientsinteractions.feed.*),OverallInteractionsFeed(msp/clientsinteractions.feed.*/interactions.overall.*),SchedulingInteractionDetails(msp/scheduleinteractionDetails.*). - msp-composition sweep verdicts (per-file): the plan's "16 string-free files" expectation was wrong in detail. 28 of 32 files have no user-visible strings (providers/wrappers/hooks/index files, incl.
AlgaDeskClientCrossFeatureProviderwhich renders nothing itself). Four more DID have strings:MspAssetCrossFeatureProvider(2 toasts → msp/assetscrossFeature.*),MspClientDrawerProvider(3 states → msp/clientsclientDrawer.*),useTicketIntegrationValue(toasts → features/ticketsintegration.*) — all three wired in P3;Reports.tsxwas ALREADY fully translated (msp/reports + defaultValues) — the initial sweep misread its defaultValues as hardcoded strings. - 120 new en keys extracted from defaultValues (extractor now mirrors scanner semantics: skip test files/trailing dots, plural-aware); translated to all 7 locales (placeholder-validated, 0 problems), pseudo regenerated.
- Namespaces/route loading (F036): /msp/clients → msp/clients, /msp/contacts → msp/contacts preloaded via ROUTE_NAMESPACES; msp/schedule and features/tickets load on demand via react-i18next when SchedulingInteractionDetails / ticket-integration drawers render off-route (T046 left as runtime QA).
- Audit tests:
clientTabs.i18n.test.ts(msp-composition),interactionsFeed.i18n.test.ts(clients),SchedulingInteractionDetails.i18n.test.ts(scheduling) — source contracts + en/xx/all-locale key existence. Convention:// @vitest-environment node+node:fsimports (plainfsfails vite resolution). - T042-T044 satisfied via all-locale key-existence assertions (not DOM render); T038/T040 via xx fill-marker assertions.
- server tsc --noEmit clean after wiring (covers msp-composition, which has no own tsconfig).
P4 implementation notes (2026-06-10)
- Legacy
_pluralinventory (F038): exactly 4 sets × 10 locales —teams.details.memberCount,interactions.types.messages.success.imported(both msp/settings),security.sessions.subtitle(msp/profile AND msp/settings). All migrated base→_one,_plural→_other; pl got hand-written_one/_few/_many/_other.grep _plural locales= 0. - CLDR-aware validator design: required categories per locale =
Intl.PluralRules(locale).select(i)over integers 0..100, plusother(the i18next fallback). This deliberately does NOT require French/Spanish/Italian/Portuguese_many(only applies at 1e6+) while requiring Polish_one/_few/_many(+_other). Variable checks for locale-specific suffixes compare against en's_other. Any remaining_pluralkey is now an ERROR.LOCALES_DIRenv override added for fixture tests. - The new validator exposed 250 real gaps the old one couldn't see:
- Four half-migrated sets (base singular +
_other, no_one) where count=1 rendered the plural string even in English:phases.taskCount,dialogs.ticketLinkedTasks.badgeCount(features/projects),serviceTypes.toast.importedCount(msp/billing-settings),draftsTab.reverseDialog.title(msp/invoicing). Fixed by renaming base→_onein all 8 locales (consumers all pass count). - 107 plural sets missing pl
_few/_many(214 forms) — generated by a Polish-translation subagent from existing pl_one/_otherwording (_fewnominative plural,_manygenitive plural; case-governed contexts identical), placeholder-validated, merged.
- Four half-migrated sets (base singular +
- AdminSessionManagement now uses a single
t('security.sessions.subtitle', { count: sessionCount, sessionCount, userCount }). The_oneform's "1 user" is safe: one active session necessarily belongs to one user. - Tests:
packages/core/src/lib/i18n/validateTranslations.test.ts(fixture-driven CLDR checks incl. legacy-_plural error),packages/ui/src/lib/i18n/pluralResolution.test.ts(real i18next instance over real locale files; en/pl counts 1/2/5; pseudo plural-key carry-through). - validate-translations.cjs final state: 0 errors, 0 warnings across 9 locales.
P5 implementation notes (2026-06-10)
validate-translations.yml: paths expanded topackages/**,server/src/**,ee/server/src/**+ the find-missing script; newfind-missing-keysjob runsfind-missing-i18n-keys.cjs. Landed after P2/P4 zeroed both scripts — both exit 0 from a clean tree, so CI arrives green.- Not verifiable locally (left open in tests.json): T058/T059/T060 (actual CI runs on a PR), T024/T025 (dev-stack visual pseudo-locale QA), T046 (runtime namespace-warning check), T017 (PrintOptionsDialog render test, trivial wrapper skipped).
Status: all 5 phases implemented (2026-06-10)
Commits on i18n/formatting_and_gaps: P1 958f4e15e0, P2 12c732c630, P3 fa1cf5d88a, P4 f0109ed354, P5 (this commit). 48/48 features, 55/62 tests (7 open are CI/manual-QA verifications listed above).
Decisions
- (2026-06-10) Formatting stays purely locale-derived; the explicit user-facing date-format preference is a separate future effort that layers on top (preference → locale default resolution chain). Decided with user.
- (2026-06-10) Emails (EJS transactional templates) explicitly out of scope. Decided with user.
- (2026-06-10) Pseudo-locales exposed in dev mode only via
NODE_ENV === 'development'insidefilterPseudoLocales;ptstays hidden in dev too. - (2026-06-10) P3 namespaces: client/contact tab components + interactions feeds →
msp/clients/msp/contacts(already loaded by ROUTE_NAMESPACES on those routes);SchedulingInteractionDetails→msp/schedule. - (2026-06-10) ESLint hardcoded-string rule deferred (PRD open question #3).
Useful commands
node scripts/find-missing-i18n-keys.cjs # exits 1 while keys missing
node scripts/validate-translations.cjs # key parity + variables; currently 8 false-positive pl warnings
node scripts/generate-pseudo-locales.cjs # regenerate xx/yy
npx nx build core # rebuild packages/core dist after config.ts edits
npx tsc -p packages/ui/tsconfig.json --noEmit