Skip to content

fix(i18n): localize backend-sourced status strings across all domains - #2604

Merged
kushagrasarathe merged 3 commits into
devfrom
fix/status-string-l10n
Aug 5, 2026
Merged

fix(i18n): localize backend-sourced status strings across all domains#2604
kushagrasarathe merged 3 commits into
devfrom
fix/status-string-l10n

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Why

Backend-sourced status strings showed in English for es-419 / pt-BR users — a transaction reading "pending", KYC status text, reason messages. The audit found the raw-English problem was narrower than expected (KYC status rendering, rail status and card status were already keyed or never rendered as text), but the sites that did leak were high-traffic: the shared StatusBadge (transaction drawer, receipts, KYC drawer, success views) had nine hardcoded English literals plus a raw-passthrough default — sitting in a directory excluded from the jsx-no-literals guard, which is exactly how it shipped unnoticed.

What

Everything keys off stable enums/codes, never backend prose (#2554's rule), following the existing TYPE_LABEL_KEYS / rejectLabelCode() patterns:

  • StatusBadge → exhaustive STATUS_LABEL_KEYS (as const satisfies Record<StatusType, string>) rendered via t() under common.status.*; unknown values collapse to status.unknown instead of rendering the raw string. The Badges/** lint ignore is removed. (Backend statuses were already normalized to the FE union by mapEntryStatusToUiStatus — no wire changes needed.)
  • PerkRewardReceipt — the one genuine raw {status} interpolation in product code, now translated through the same table.
  • Reaper fail copy + intent-strategy display names (REAPER_FAIL_COPY, 'Sent via Link', 'Card payment', …) — the transformer emits a nameKey (+ ICU params for dynamic parts like usernames and bank names, which are data, not copy); render sites translate. The English-literal displayName comparisons in TransactionDetailsHeaderCard are replaced with discriminants (the isTestTransaction() precedent) so translating the labels can't break the logic.
  • CapabilityReason.code → key table with a rejectLabelCode()-style fallback, covering the KYC/card/region reason-message sites (ApplicationStatusScreen, InitiateKycModal, UnlockedRegions, useWaitingOnProviderModal, ActivationCTAs). Where the wire carries no code (identity.actionMessage, Rain dispute prompts) the backend prose remains as fallback — adding those codes is an api-ts follow-up ([TASK-14942] feat(manteca-deposit): fix manteca deposit #1249 pattern).
  • Prop-literal stragglers keyed: ActivationCTAs spend prompts, the 'Adjustment' card row, the PIX maintenance badge, Sumsub error fallbacks (server actions now also return a code, translated client-side — no server-side next-intl in this app).

Out of scope, documented: /receipt/[entryId] server-component titles (structurally blocked on server-side next-intl).

Verification

  • Full jest: 183 suites, 2,416 passed (includes a regression the suite caught during development: customText='' means "no override" — truthiness preserved).
  • tsc --noEmit clean; eslint on all touched files: 0 errors; catalog parity + ICU compilation tests green for all three locales.

… codes

Backend-sourced status and reason strings rendered in English for
es-419/pt-BR users. Everything now keys off stable enums/codes — never
backend prose — following the TYPE_LABEL_KEYS / rejectLabelCode patterns:

- StatusBadge renders t() via an exhaustive STATUS_LABEL_KEYS table
  (common.status.*); the raw-passthrough default is gone and unknown
  values collapse to status.unknown. Its directory is no longer excluded
  from the jsx-no-literals guard — that ignore is how nine English
  literals shipped unnoticed.
- PerkRewardReceipt's else-branch translates through the same table.
- Reaper fail copy and intent-strategy display names move to a
  transaction-name key table: the transformer emits nameKey (+ ICU params
  for dynamic parts like usernames), render sites translate, and raw
  names remain as data-only fallbacks. The displayName string comparisons
  in TransactionDetailsHeaderCard now use discriminants instead of
  matching English copy.
- CapabilityReason.code maps through a reason-code table with a fallback
  to the backend's userMessage where no code exists on the wire
  (identity.actionMessage, dispute prompts — backend follow-up).
- Prop-literal stragglers keyed: ActivationCTAs spend prompts, the
  'Adjustment' card row, the PIX maintenance badge, Sumsub fallbacks
  (server actions now also return a code, translated client-side).

All new keys land in en / es-419 / pt-BR; the parity + ICU tests enforce
the set stays complete.
@innolope-dev innolope-dev self-assigned this Aug 4, 2026
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 5, 2026 9:07am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d07674ac-f81a-4dd0-bf3f-2a30d40e1822

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2442 ran, 0 failed, 0 skipped, 43.3s

📊 Coverage (unit)

metric %
statements 62.5%
branches 45.5%
functions 51.8%
lines 63.0%
⏱ 10 slowest test cases
time test
3.7s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.2s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/utils/__tests__/auth-token.test.ts › is none — never guarded — when only the guarded marker is present
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/utils/__tests__/auth-token.test.ts › ignores the guarded marker and falls back to the plain token
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/utils/__tests__/auth-token.test.ts › returns the token hydrated from Preferences after authReady
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6885.89 → 6908.74 (+22.85)
Findings: +7 net (+133 new, -126 resolved)

🆕 New findings (133)

  • critical complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 163, MI 51.34, SLOC 426
  • critical complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 157, MI 51.57, SLOC 594
  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 127, MI 56.82, SLOC 492
  • critical complexity — src/components/AddWithdraw/AddWithdrawCountriesList.tsx — CC 123, MI 56.31, SLOC 371
  • critical complexity — src/components/Claim/Link/views/BankFlowManager.view.tsx — CC 110, MI 46.92, SLOC 426
  • critical method-complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:89 — CC 109 SLOC 217
  • critical complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 108, MI 57.42, SLOC 394
  • critical complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 101, MI 53.4, SLOC 385
  • critical complexity — src/components/TransactionDetails/transactionTransformer.ts — CC 98, MI 43.69, SLOC 286
  • critical complexity — src/utils/capability-gate.ts — CC 94, MI 61.19, SLOC 246
  • critical complexity — src/hooks/useSumsubKycFlow.ts — CC 80, MI 52.63, SLOC 371
  • critical complexity — src/components/Home/ActivationCTAs.tsx — CC 67, MI 57.88, SLOC 270
  • critical complexity — src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx — CC 67, MI 45.43, SLOC 154
  • critical complexity — src/components/Profile/views/UnlockedRegions.view.tsx — CC 66, MI 60.11, SLOC 220
  • critical complexity — src/components/AddMoney/components/MantecaAddMoney.tsx — CC 58, MI 56.56, SLOC 219
  • critical complexity — src/components/TransactionDetails/TransactionCard.tsx — CC 56, MI 49.28, SLOC 179
  • high hotspot — src/app/(mobile-ui)/withdraw/manteca/page.tsx — 63 commits, +616/-374 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 57 commits, +572/-483 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 56 commits, +562/-279 lines since 6 months ago
  • high hotspot — src/components/AddWithdraw/AddWithdrawCountriesList.tsx — 48 commits, +732/-534 lines since 6 months ago

…and 113 more.

✅ Resolved (126)

  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 162, MI 51.38, SLOC 425
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 157, MI 51.58, SLOC 594
  • src/app/(mobile-ui)/card/page.tsx — CC 125, MI 56.97, SLOC 488
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx — CC 123, MI 56.36, SLOC 369
  • src/components/Claim/Link/views/BankFlowManager.view.tsx — CC 110, MI 46.94, SLOC 425
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 108, MI 57.45, SLOC 393
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:89 — CC 108 SLOC 216
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 101, MI 53.43, SLOC 384
  • src/components/TransactionDetails/transactionTransformer.ts — CC 97, MI 44.11, SLOC 276
  • src/utils/capability-gate.ts — CC 92, MI 60.96, SLOC 244
  • src/hooks/useSumsubKycFlow.ts — CC 78, MI 52.6, SLOC 354
  • src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx — CC 66, MI 45.66, SLOC 152
  • src/components/Profile/views/UnlockedRegions.view.tsx — CC 65, MI 60.42, SLOC 214
  • src/components/Home/ActivationCTAs.tsx — CC 64, MI 58.15, SLOC 254
  • src/components/AddMoney/components/MantecaAddMoney.tsx — CC 58, MI 56.57, SLOC 219
  • src/components/TransactionDetails/TransactionCard.tsx — CC 55, MI 49.44, SLOC 177
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — 62 commits, +615/-374 lines since 6 months ago
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 56 commits, +570/-482 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 55 commits, +560/-278 lines since 6 months ago
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx — 47 commits, +729/-532 lines since 6 months ago

…and 106 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/TransactionDetails/transaction-name-keys.ts 0.0 5.0 +5.0
src/constants/capability-reason-labels.consts.ts 0.0 4.8 +4.8
src/components/Global/DocsLink.tsx 3.8 5.4 +1.6
src/components/TransactionDetails/strategies/intent/p2p-send.ts 8.2 9.3 +1.1
src/components/TransactionDetails/strategies/intent/fiat-offramp.ts 7.1 8.1 +1.0
src/components/Card/ApplicationStatusScreen.tsx 9.9 10.8 +0.8
src/components/TransactionDetails/strategies/intent/crypto.ts 5.0 5.7 +0.8
src/components/TransactionDetails/provider-receipts/PerkRewardReceipt.tsx 7.3 7.9 +0.6
src/hooks/useWaitingOnProviderModal.ts 5.8 6.4 +0.6
src/components/TransactionDetails/strategies/intent/send-link.ts 7.5 8.1 +0.6
src/components/TransactionDetails/strategies/intent/card.ts 6.3 6.8 +0.5

identity.actionMessage is a pure function of status on the backend
(actionMessageFor), so the enum we already receive IS the discriminant:
its presence keeps gating the reason card, the copy now comes from the
catalog instead of rendering the English wire prose.

DocsLink re-points /en/… hrefs at the app locale's marketing twin
(app locales lowercase onto the marketing URL codes; the content
fallback chains guarantee no 404). Covers every docs/help link in the
app including the wallet nav's docs item.

Rain's dispute evidenceRequestedMessage stays verbatim on purpose —
it's case-specific free prose written by the processor's agents, with
no enum to key off.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Follow-up pushed: KYC action messages now key off identity.status (the backend's actionMessage is a pure function of status — the discriminant was already on the wire, no API change needed), and DocsLink re-points /en/… hrefs at the app locale's marketing twin, covering all ~15 app-side docs/help links incl. the wallet nav. Rain's dispute evidenceRequestedMessage stays verbatim deliberately: it's case-specific free prose from the processor's dispute agents — no enum exists to key off. Full suite: 184 suites / 2,419 passed.

…the classifier's fix instruction

The backend only emits document_rejected together with the self-heal
classifier's fixable-specific text (which document to fix and how).
Mapping the code onto a generic catalog line discarded that instruction.
Render the BE prose until the classifier's stable action code reaches
the wire (api-ts follow-up).

@kushagrasarathe kushagrasarathe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent review (pr-review rulebook) — full-diff read, all four checklists

Scope reviewed: all 48 files (+805/−137), plus the backend code this PR's claims depend on (actionMessageFor, the capability resolver, the self-heal classifier).

What checked out

  • actionMessageFor is genuinely a pure function of status (api-ts identity.ts:81) — gating on presence and rendering catalog copy is safe; the updated KycStates test encodes the contract.
  • Catalog parity: identical key sets across en / es-419 / pt-BR; every new key present in all three.
  • REAPER_FAIL_KEYS exactly mirrors REAPER_FAIL_COPY (9/9) with a safe generic fallback.
  • English-literal discriminants eliminated, not just translatednameKey === TRANSACTION_NAME_KEYS.sentViaLink replaces string comparison; zero literal comparisons remain at head.
  • eslint Badges/** ignore removal is safe — the directory contains only StatusBadge, now literal-free.
  • customText='' truthiness contract preserved; raw userName kept for data uses (test-tx marker, profile URL).
  • No deploy-order dependency: reason.code already ships from the resolver on dev; unknown codes fall back to BE prose.
  • Security: no new routes, deps, or secrets; diff matches the description exactly.

Finding (fixed in 121117b)

REASON_CODE_KEYS mapped document_rejected to a generic catalog line, but the backend only emits that code with the self-heal classifier's fixable-specific prose ("Your ID photo was blurry. Please upload a clearer photo." — classifier.ts, preferred at resolver.ts:1346). The generic line masked the fix instruction. Resolved by removing the mapping so those sites keep the BE prose, until the classifier's stable action code (REUPLOAD_ID, …) reaches the wire — api-ts follow-up, #1249 pattern.

Kept as-is (accepted): requirement-code entries stay mapped — their catalog copy is byte-identical to the resolver's messageForRequirement prose, so localization is a pure win in the common case; the rare fixable-classifier override on those codes is covered by the same api-ts follow-up. region_block also stays mapped — the BE's state/country/region word choice is lost, but the loss is cosmetic and localization wins.

Smell flags (fifth-bug rule, advisory)

Fix-commit density in the last 20 commits: useSumsubKycFlow.ts 12/20, transactionTransformer.ts 9/20, card/page.tsx 8/20, ActivationCTAs.tsx 8/20. The KYC-flow hook and the transformer are standing deep-dive candidates.

Verdict

TIER: T0 (external author; spans T2 KYC-gating surfaces)
PAIRED PR: none required — api-ts follow-up (classifier action codes on the wire) named, no deploy ordering
A. BREAKAGE:  pass — document_rejected masking fixed in 121117b
B. PERF:      pass — static table lookups, no new log/alert emission
C. QUALITY:   pass with smell flags (fifth-bug rule, above)
D. SECURITY:  pass — diff-vs-description exact match
TASK LINK:    waived by reviewer
VERDICT:      approve

Reviewed and approved by @kushagrasarathe (human, accountable); agent review advisory. Merge once CI is green on 121117b.

@kushagrasarathe
kushagrasarathe merged commit 72028e2 into dev Aug 5, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants