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

273 lines
12 KiB
Markdown

# PRD — MSP i18n: Credits Sub-batch
- Slug: `2026-04-09-msp-i18n-credits`
- Date: `2026-04-09`
- Status: Implemented on branch `i18n/billing_credits` (22/22 features, 31/31 tests) — pending merge to `main`. See SCRATCHPAD "Status Recheck (2026-04-17)".
- Parent plan: `/.ai/translation/MSP_i18n_plan.md`
## Summary
Create a new `msp/credits` i18n namespace and wire `useTranslation('msp/credits')` into 10
credit management components across `packages/billing/src/components/credits/` and
`packages/billing/src/components/billing-dashboard/Credit*.tsx`. None of these components
currently use `useTranslation`. This is a greenfield namespace (no existing JSON to extend)
covering ~150 user-visible strings across credit management, reconciliation, application,
and expiration flows.
## Problem
MSP users navigating to billing > credits see fully English UI while the surrounding
navigation, sidebar, and dashboard chrome are already translated. The credits module spans
10 components with ~150 hardcoded English strings across credit listing, management dashboards,
reconciliation reports, credit application, and expiration modification dialogs. No
`msp/credits.json` namespace exists, and no `ROUTE_NAMESPACES` entry loads credit-specific
translations for `/msp/billing/credits`.
## Goals
1. Create `server/public/locales/en/msp/credits.json` with all keys needed by the 10 components
2. Wire `useTranslation('msp/credits')` into all 8 client components (2 files are server-side
or have zero visible strings)
3. Generate translations for 7 non-English locales (fr, es, de, nl, it, pl) + 2 pseudo-locales
(xx, yy)
4. Add `/msp/billing/credits` route to `ROUTE_NAMESPACES` in
`packages/core/src/lib/i18n/config.ts` loading `msp/credits`
5. Pass `node scripts/generate-pseudo-locales.cjs && node scripts/validate-translations.cjs`
6. Measurable: 0% -> 100% of credit components wired for i18n
## Non-goals
- Translating credit amounts, dates, or tenant data (those are formatted via `useFormatters`
or `formatCurrency`/`formatDateOnly` utilities already in use)
- Translating server-side error strings in `actions.ts` that only appear in console logs
(e.g., `'Unknown error occurred'` in catch blocks) -- these do not surface to the user
- Moving credit components to a different package or restructuring the billing package
- Adding new UI features to credit management
- Translating Recharts axis tick formatters or tooltip formatters (they use `formatCurrency`
which is locale-aware separately)
## File Inventory
| # | File | LOC | Type | Est. Strings | Notes |
|---|------|-----|------|-------------|-------|
| 1 | `credits/CreditsPage.tsx` | 275 | Server component | ~30 | Column titles, statuses, tab labels, card titles, settings labels. **Server component** -- will need a client wrapper or inline `t()` via server-side i18n |
| 2 | `credits/CreditsTabs.tsx` | 53 | Client | 0 | Pure tab-switching logic; labels passed in via props from CreditsPage. No visible strings to translate. **Skip.** |
| 3 | `credits/AddCreditButton.tsx` | 53 | Client | ~6 | Button label, dialog title, placeholder text, cancel/submit buttons |
| 4 | `credits/BackButton.tsx` | 20 | Client | ~1 | "Back to Credits" button text |
| 5 | `credits/actions.ts` | 109 | Server action | ~5 | Error messages in catch blocks. Only `'Authentication required'` and `'Transfer amount must be greater than zero'` surface to callers. Others are console-only. |
| 6 | `billing-dashboard/CreditManagement.tsx` | 642 | Client | ~45 | Page title, chart labels/legends, stat card labels, column titles, tab labels, button text, dialog text |
| 7 | `billing-dashboard/CreditReconciliation.tsx` | 604 | Client | ~50 | Dashboard title, filter labels, stat cards, chart titles/legends, column titles, tab labels, status badges, button text, toast messages |
| 8 | `billing-dashboard/CreditApplicationUI.tsx` | 273 | Client | ~20 | Card title/description, column titles, labels, button text, error/empty states |
| 9 | `billing-dashboard/CreditExpirationInfo.tsx` | 129 | Client | ~10 | Card title/description, field labels, help text, empty/error states |
| 10 | `billing-dashboard/CreditExpirationModificationDialog.tsx` | 172 | Client | ~15 | Dialog title/description, field labels, switch label, button text, validation errors |
**Total estimated: ~150 unique strings across ~80 translation keys (some strings repeat across components).**
## Namespace Structure
File: `server/public/locales/en/msp/credits.json`
```
{
"page": {
"title": "Credit Management",
"creditsOverview": "Credits Overview",
"overviewDescription": "Manage your client credits...",
...
},
"columns": {
"creditId": "Credit ID",
"created": "Created",
"description": "Description",
"originalAmount": "Original Amount",
"remaining": "Remaining",
"expires": "Expires",
"status": "Status",
"actions": "Actions",
"context": "Context",
...
},
"status": {
"active": "Active",
"expired": "Expired",
"expiringSoon": "Expiring Soon ({{days}} days)",
"never": "Never",
"na": "N/A",
...
},
"actions": {
"view": "View",
"edit": "Edit",
"expire": "Expire",
"addCredit": "Add Credit",
"cancel": "Cancel",
"viewAllCredits": "View All Credits",
"backToCredits": "Back to Credits",
...
},
"tabs": {
"activeCredits": "Active Credits",
"allCredits": "All Credits",
"expiredCredits": "Expired Credits"
},
"settings": {
"title": "Credit Expiration Settings",
"creditExpiration": "Credit Expiration:",
"enabled": "Enabled",
"disabled": "Disabled",
"expirationPeriod": "Expiration Period:",
"daysUnit": "{{count}} days",
"notificationDays": "Notification Days:",
"none": "None"
},
"charts": {
"expirationSummary": "Credit Expiration Summary",
"expirationSummaryDescription": "Overview of credits expiring soon",
"usageTrends": "Credit Usage Trends",
"usageTrendsDescription": "Historical credit usage patterns",
"creditsIssued": "Credits Issued",
"creditsApplied": "Credits Applied",
"creditsExpired": "Credits Expired",
...
},
"stats": {
"totalActiveCredits": "Total Active Credits",
"expiringIn30Days": "Expiring in 30 Days",
"totalCreditsApplied": "Total Credits Applied",
"totalCreditsExpired": "Total Credits Expired",
...
},
"management": {
"title": "Credit Management",
"recentCredits": "Recent Credits",
"recentCreditsDescription": "View and manage your client credits...",
"addCreditPlaceholder": "Credit amount and details form would be implemented here."
},
"reconciliation": {
"title": "Credit Reconciliation Dashboard",
"selectClient": "Select Client",
"runReconciliation": "Run Reconciliation",
"running": "Running...",
"status": "Status",
"allStatuses": "All Statuses",
"open": "Open",
"inReview": "In Review",
"resolved": "Resolved",
"fromDate": "From Date",
"toDate": "To Date",
"reset": "Reset",
"totalDiscrepancies": "Total Discrepancies",
"totalDiscrepancyAmount": "Total Discrepancy Amount",
"openIssues": "Open Issues",
"statusDistribution": "Status Distribution",
"statusDistributionDescription": "Overview of reconciliation report statuses",
"discrepancyTrends": "Discrepancy Trends",
"discrepancyTrendsDescription": "Monthly trends in credit discrepancies",
"numberOfDiscrepancies": "Number of Discrepancies",
"totalAmount": "Total Amount",
"reconciliationReports": "Reconciliation Reports",
"reconciliationReportsDescription": "View and manage credit balance discrepancies",
"resolve": "Resolve",
"validationResult": "Validation completed: Found {{balanceCount}} balance discrepancies and {{trackingCount}} tracking issues.",
...
},
"application": {
"title": "Apply Credit",
"applyToInvoice": "Apply available credits to this invoice",
"applyToBalance": "Apply credits to reduce customer balance",
"totalAvailableCredit": "Total Available Credit:",
"invoiceAmount": "Invoice Amount:",
"selectCreditToApply": "Select Credit to Apply",
"amountToApply": "Amount to Apply",
"creditOrderNote": "Credits are applied in order of expiration date (oldest first)",
"noCreditsAvailable": "No credits available for this client",
"failedToLoadCredits": "Failed to load available credits",
"selectCreditError": "Please select a credit and enter a valid amount",
"failedToApply": "Failed to apply credit",
"applying": "Applying...",
"applyCredit": "Apply Credit",
"selected": "Selected",
"select": "Select",
...
},
"expiration": {
"appliedCredits": "Applied Credits",
"creditsAppliedToInvoice": "Credits applied to this invoice: {{amount}}",
"creditAmount": "Credit Amount:",
"noDetails": "No credit details available",
"failedToLoad": "Failed to load credit details",
...
},
"expirationDialog": {
"title": "Modify Credit Expiration",
"description": "Update the expiration date for this credit.",
"creditAmount": "Credit Amount:",
"remainingAmount": "Remaining Amount:",
"created": "Created:",
"currentExpiration": "Current Expiration:",
"noExpiration": "No expiration",
"removeExpiration": "Remove expiration date",
"newExpirationDate": "New Expiration Date",
"pastDateError": "Expiration date cannot be in the past",
"updateError": "An error occurred while updating the expiration date",
"saving": "Saving...",
"saveChanges": "Save Changes"
},
"context": {
"lineageMissing": "Lineage Missing",
"lineageMissingDescription": "Source invoice metadata could not be recovered...",
"transferredRecurringCredit": "Transferred Recurring Credit",
"recurringSource": "Recurring Source",
"servicePeriod": "Service Period: {{period}}",
"recurringLineagePreserved": "Recurring source lineage preserved",
"financialOnly": "Financial Only",
"noRecurringServicePeriod": "No recurring service period"
}
}
```
## ROUTE_NAMESPACES Change
In `packages/core/src/lib/i18n/config.ts`, add a new entry:
```typescript
'/msp/billing/credits': ['common', 'msp/core', 'features/billing', 'msp/credits'],
```
This must appear **before** the existing `/msp/billing` entry since `getNamespacesForRoute`
uses longest-prefix matching.
## Server Component Consideration
`CreditsPage.tsx` is an async server component (no `'use client'` directive). Two approaches:
1. **Extract a client wrapper** that calls `useTranslation('msp/credits')` and passes
translated strings as props to the server-rendered content. This is the recommended
pattern for server components in Next.js with client-side i18n.
2. **Convert to client component** by adding `'use client'` and restructuring data fetching.
This is more invasive and not recommended.
The recommended approach is (1): create a thin `CreditsPageClient.tsx` wrapper that handles
translation, and have the server component pass data to it.
## Acceptance Criteria
- [ ] `server/public/locales/en/msp/credits.json` exists with all keys used by credit components
- [ ] All 8 client components with visible strings import `useTranslation('msp/credits')`
and wrap all user-visible strings via `t('key', { defaultValue: 'English fallback' })`
- [ ] `CreditsPage.tsx` (server component) strings are translated via a client wrapper component
- [ ] `CreditsTabs.tsx` confirmed to have zero visible strings (skip)
- [ ] `actions.ts` server-side error strings assessed: user-facing ones translated, log-only
ones left in English
- [ ] `/msp/billing/credits` added to `ROUTE_NAMESPACES` loading `msp/credits`
- [ ] Translations generated for fr, es, de, nl, it, pl (7 locales)
- [ ] `node scripts/generate-pseudo-locales.cjs && node scripts/validate-translations.cjs`
exits 0
- [ ] Italian translations preserve accents (e.g., "Crediti scaduti" not "Crediti scaduti")
- [ ] `{{variable}}` interpolation tokens preserved across all 9 locale files
- [ ] Currency values still formatted via `formatCurrency()` (not translated as strings)
- [ ] Date values still formatted via `toLocaleDateString()` or `formatDateOnly()` (not translated)
- [ ] Visual smoke test: `/msp/billing/credits` and credit management tab render correctly
in `en` and at least one non-English locale; `xx` pseudo-locale shows pseudo-text for
every visible string