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
273 lines
12 KiB
Markdown
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
|