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

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:

'/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