Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
12 KiB
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 tomain. 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
- Create
server/public/locales/en/msp/credits.jsonwith all keys needed by the 10 components - Wire
useTranslation('msp/credits')into all 8 client components (2 files are server-side or have zero visible strings) - Generate translations for 7 non-English locales (fr, es, de, nl, it, pl) + 2 pseudo-locales (xx, yy)
- Add
/msp/billing/creditsroute toROUTE_NAMESPACESinpackages/core/src/lib/i18n/config.tsloadingmsp/credits - Pass
node scripts/generate-pseudo-locales.cjs && node scripts/validate-translations.cjs - Measurable: 0% -> 100% of credit components wired for i18n
Non-goals
- Translating credit amounts, dates, or tenant data (those are formatted via
useFormattersorformatCurrency/formatDateOnlyutilities already in use) - Translating server-side error strings in
actions.tsthat 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
formatCurrencywhich 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:
'/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:
- 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. - 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.jsonexists with all keys used by credit components- All 8 client components with visible strings import
useTranslation('msp/credits')and wrap all user-visible strings viat('key', { defaultValue: 'English fallback' }) CreditsPage.tsx(server component) strings are translated via a client wrapper componentCreditsTabs.tsxconfirmed to have zero visible strings (skip)actions.tsserver-side error strings assessed: user-facing ones translated, log-only ones left in English/msp/billing/creditsadded toROUTE_NAMESPACESloadingmsp/credits- Translations generated for fr, es, de, nl, it, pl (7 locales)
node scripts/generate-pseudo-locales.cjs && node scripts/validate-translations.cjsexits 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()orformatDateOnly()(not translated) - Visual smoke test:
/msp/billing/creditsand credit management tab render correctly inenand at least one non-English locale;xxpseudo-locale shows pseudo-text for every visible string