Skip to content

chore(i18n): string sweep, shared intl test helper, toast lint guard - #2555

Open
innolope-dev wants to merge 2 commits into
fix/locale-safe-errorsfrom
chore/i18n-sweep
Open

chore(i18n): string sweep, shared intl test helper, toast lint guard#2555
innolope-dev wants to merge 2 commits into
fix/locale-safe-errorsfrom
chore/i18n-sweep

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Stacked on #2554 — review that first; this PR's diff is against it, and the base retargets to dev once #2554 merges.

Why

Follow-up to #2447. react/jsx-no-literals only inspects JSX children, so hardcoded copy kept reaching users through toasts, placeholders, aria-labels and alt text.

The ticket was right about QRScanner

It named QRScanner as a known offender with "3–4 toasts". It had four, on dev, and three of them had keys that already existed and simply weren't being used. (An earlier pass of mine looked at a divergent branch where they'd been extracted and wrongly concluded the ticket was stale.)

The lint rule

A no-restricted-syntax selector banning string literals as the first argument to toast.error|success|info|warning|loading.

Appended to the existing array, not a new config block — ESLint flat config replaces rule options when a later block names the same rule, so a second no-restricted-syntax entry would have silently disabled the four router.back selectors. That array already sits under files: ['src/**/*.{ts,tsx}'], which is exactly the coverage needed, since every offender lives in src/hooks or src/context — outside the jsx-no-literals globs.

Verified it fires by planting a synthetic violation, not just by observing a green run.

Scoped to toast.*, not throw new Error('…'). Every throw site flagged in the ticket was traced to its catch handler: they all funnel through toFriendlyError to errors.genericSupport, or into an already-localized toast. Those strings are Sentry breadcrumbs and issue-grouping keys — translating them would fragment triage for zero user-facing gain.

Note the rule can't actually block a merge: the eslint job is still continue-on-error and absent from ci-success.needs. Left as-is deliberately; promoting it is a separate team decision.

Shared test helper

src/test-utils/intl.tsx replaces 39 hand-rolled NextIntlClientProvider wrappers (the ticket estimated ~15). Exports the three shapes call sites actually need: IntlWrapper (composable, for suites nesting QueryClientProvider), renderWithIntl, and renderHookWithIntl.

Two suites had omitted timeZone, so next-intl fell back to the runner's system zone and their date formatting was machine-dependent — the shared wrapper pins UTC. renderHookWithIntl exists so useSumsubKycFlow.test.ts (a .ts file, no JSX) can drop its React.createElement workaround and the eslint-disable react/no-children-prop that came with it.

ClientProviders.test.tsx deliberately keeps its own reference — it asserts on provider ordering, so the provider is the subject under test.

Extractions

Four QRScanner toasts, both logout/login toasts, two card-approval toasts, the waitlist email placeholder, two "Peanut Logo" aria-labels (reusing the existing navigation.peanutLogoAlt), two descriptive alt strings. The QRScanner payment-method row keeps its brand names verbatim — proper nouns — and only the EVM row, whose label is prose, moves to a key.

NavHeader gains an optional titleKey: /receipt/[entryId] is an async server component and this app has no server-side next-intl setup (locale is resolved entirely client-side by AppIntlProvider), so useTranslations is unavailable there.

Two structural fixes

Clipboard. The match on "There is no data on the clipboard" was not a locale bug — that string is a hardcoded English literal inside @capacitor/clipboard's Java (ClipboardPlugin.java:53), not an OS message. It was a dependency coupling: a plugin patch bump silently flips the branch and an empty clipboard starts reporting as a failure. readClipboard() now classifies the rejection and returns a reason.

'Enjoy Peanut!' is consolidated behind one isTestTransaction() predicate. It compares backend data, so it's locale-safe, but it was repeated in four places and one of them rendered the raw literal instead of the transaction.enjoyPeanut key that already existed and was already translated. The backend should own this flag — worth a ticket.

Verification

pnpm typecheck clean · 174 suites / 2311 tests pass · eslint 0 errors · prettier --check . clean

Note: peanut-ui's pull_request CI trigger only fires for main/dev/develop bases, so this PR's checks come from the push trigger.

Every intl-using suite defined its own NextIntlClientProvider wrapper with the
same locale/messages/timeZone triple. src/test-utils/intl.tsx now owns it and
exports the three shapes the call sites actually need: IntlWrapper (composable,
for suites that nest QueryClientProvider or a context provider), renderWithIntl
(the common wrapper-option case), and renderHookWithIntl.

Two suites had omitted `timeZone`, so next-intl fell back to the runner's system
zone and their date formatting was machine-dependent; the shared wrapper pins
UTC for everyone.

renderHookWithIntl exists so useSumsubKycFlow.test.ts — a .ts file that could not
use JSX — can drop its React.createElement workaround and the
`eslint-disable react/no-children-prop` that came with it.

ClientProviders.test.tsx deliberately keeps its own reference: it asserts on
provider ORDERING, so the provider is the subject under test, not a fixture.
…asts

react/jsx-no-literals only inspects JSX children, so hardcoded copy kept
reaching users through toasts, placeholders, aria-labels and alt text. A
no-restricted-syntax selector now bans string literals as the first argument to
toast.*, appended to the EXISTING array rather than a new config block —
flat config replaces rule options when a later block names the same rule, so a
second entry would have silently disabled the four router.back selectors.

The rule is deliberately scoped to toast.* and not `throw new Error('…')`.
Every throw site was traced to its catch handler: they all funnel through
toFriendlyError to errors.genericSupport or a localized toast, so those strings
never reach a user. They are Sentry breadcrumbs and issue-grouping keys, and
translating them would fragment triage for no user-facing gain.

Extracted: the four QRScanner toasts (three already had keys and simply weren't
using them), both logout/login toasts and the two card-approval toasts, the
waitlist email placeholder, the two "Peanut Logo" aria-labels (reusing the
existing navigation.peanutLogoAlt), and two descriptive alt strings. The
QRScanner payment-method row keeps its brand names verbatim — those are proper
nouns — and only the EVM row, whose label is prose, moves to a key.

NavHeader gains an optional `titleKey`, because the receipt page at
/receipt/[entryId] is an async server component and this app has no server-side
next-intl setup (locale is resolved entirely client-side by AppIntlProvider), so
useTranslations is unavailable there. The mobile receipt page is a client
component and uses plain `title` with t().

Also replaced the clipboard read's message match. "There is no data on the
clipboard" is a hardcoded English literal inside @capacitor/clipboard's Java,
not an OS string — so this was never a locale bug, but it did couple us to a
dependency's copy, where a patch bump silently flips the branch. readClipboard()
now classifies the rejection and returns a reason.

The 'Enjoy Peanut!' sentinel is consolidated behind one isTestTransaction()
predicate. It compares backend data, so it is locale-safe, but it was repeated
in four places and one of them rendered the raw literal instead of the
transaction.enjoyPeanut key that already existed and was already translated.
The backend should own this flag — noted for follow-up.
@innolope-dev innolope-dev self-assigned this Jul 28, 2026
@vercel

vercel Bot commented Jul 28, 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, Comment Jul 28, 2026 4:35pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 28, 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: c589de41-9b30-4ab8-804a-ccd6addcb7bc

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/i18n-sweep

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

innolope-dev added a commit that referenced this pull request Jul 28, 2026
…nner removal

Carries the localization follow-up cherry-picked from the dev-based PRs
(peanut-ui #2554 / #2555) plus the iOS-only removal of the pink beta feedback
ribbon:

- Backend errors now map to localized copy via a wire-code allow-list instead
  of being echoed in English; the Rain cooldown renders "try again in N
  minutes" in the user's language.
- Locale-unsafe comparisons removed (a dead branch in the bank-withdraw flow
  that compared a translated string to an English literal).
- Untranslated toasts, placeholders, aria-labels and alt text extracted.
- 39 hand-rolled test intl wrappers collapsed into one shared helper.
- The beta feedback ribbon is hidden on iOS native only; connectivity and
  maintenance banners still show, and Android/web are unchanged.

App icon note: no icon change here. The BETA badge was dropped from the iOS
icon in 1.0.38 and every build since (1.0.39, 1.0.40, 1.0.41) has carried the
clean 1024px marketing icon.
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.

1 participant