Localization (en / es-419 / pt-BR) + zero ESLint errors - #2447
Conversation
…zation header CapacitorHttp's Android GET proxy (_capacitor_http_interceptor_) stalls under load, timing out every in-flight request after 10s (PEANUT-UI-R44): ~400 timeouts/user on Android vs ~3 on web/iOS over 14d, GETs only, in correlated bursts. Requests now go direct from the webview, same path as web. - auth: token moves from the CapacitorHttp cookie jar to @capacitor/preferences (survives webview storage eviction, unlike localStorage — PEANUT-UI-QTQ), hydrated into an in-memory cache and sent as Authorization. authReady() gates callApi and direct fetchWithSentry call sites against cold-start races. - login: the ZeroDev SDK consumes /passkeys/*/verify responses internally, so a window.fetch wrapper captures the body token on native. /users/me sliding refresh keeps it current via the existing setAuthToken path. - old binaries keep working: server still accepts the jwt-token cookie and hasNativeSession falls back to the legacy jar. Existing native sessions are not migrated — testers log in once after updating. - fetchWithSentry: idempotent requests (GET/HEAD) get one silent retry on timeout before surfacing. - canary: one-shot startup probe reports direct-fetch viability to Sentry (message:"direct-fetch canary", tags outcome/transport) so the transport switch is validated fleet-wide via OTA before the binary rolls out.
@capacitor/device requires the next native binary release; JS falls back to navigator.language on older binaries.
Hoist the lazy viem require in peanut-claim.utils (viem is already statically imported there) and scope the rule off for Jest test files, where require() after jest.mock()/resetModules() is intentional.
Remove dead imports/declarations, use bare catch where the binding was unused, and _-prefix signature-bound params and kept hook results. Also ignore ios/ and build/ (generated) in eslint.
Replace raw <a> internal-route anchors with <Link> (client-side navigation) across LandingPage components and the dev debug page.
- src/i18n/app: locale config + resolveLocale normalizer, catalogs (en/es-419/pt-BR), deep-merge loader so missing keys render English, runtime locale store (Preferences/Device on native, cookie on web), AppIntlProvider with hydration-safe English-first state - provider wired into ClientProviders; native splash gated on the startup locale being painted (2s timeout guard) - loadingStates union converted to a const array + key mapping - jest: transform ESM-only intl packages (pnpm-aware ignore pattern) Marketing i18n (src/i18n/*.json) untouched.
/settings/language screen with the supported locales, reached from a new Profile row showing the active language. Live re-render on switch, persisted via Preferences (native) / cookie (web).
The OPEN-status badge fix was picked up by 1eb7c3c ("fix(lint): resolve no-unused-vars") along with that commit's lint sweep. It is unrelated to localization and is being reviewed on its own branch against main (peanut-ui#2430), so remove it here to keep the two changes separable. Pure removal — no behaviour change beyond reverting to main's badge logic.
Step titles/descriptions keyed by screenId (removed from ISetupStep), all Setup views, wrapper, and (setup) pages on useTranslations. es-419 + pt-BR drafts included.
Home screen, activation CTAs, carousel, perk/welcome modals, tab bar, desktop sidebar and top navbar. TopNavbar maps pathname to typed navigation.* keys, replacing the pathTitles util.
ExchangeRateWidget (marketing-shared) takes an optional labels prop with English defaults; product callers pass translated labels.
Limits warning-card items now carry a kind discriminator so render sites can map them to translated copy; qr-pay uses it.
The warning card rendered raw English item text for the withdraw and add-money callers while qr-pay mapped the kind discriminator at its own render site. Resolve copy inside the card instead, so every flow gets it, and drop the duplicated mapping from qr-pay. Passkey troubleshooting steps and warnings become ids resolved against setup.passkey.help.*, so the modal's content is translated, not just its chrome.
The min/max cashout branches assigned the placeholder slugs 'offramp_lt_minimum'/'offramp_mt_maximum', which ErrorAlert rendered verbatim — users saw the raw slug. Assign real messages with the limit formatted as currency. Also return on the over-maximum branch: it set the error and then kept going, fetching a route and letting the flow continue past the limit, unlike the under-minimum branch.
validatePin returns reason codes instead of English copy so the util stays copy-free; CardPinSetupFlow maps them to messages. CardCountryConfirmScreen feeds the active locale into Intl.DisplayNames (was hardcoded to en), so country names follow the UI language.
Drop the vestigial capitalize class on the feed's type label: it existed to case raw enum values (getActionText returned the type verbatim), but the catalog now supplies cased copy, and CSS title-casing mangles multi-word labels in every locale. cardDeclineReason and the bank-account label util return codes now, with the copy resolved at the render site.
Drop the capitalize class on the badge-unlocked label for the same reason as the transaction feed: it title-cased translated copy. Remove invites.consts.ts — it held only display copy, and its last consumer now reads the setup-flow waitlist key so the two gates can't drift.
sumsub-reject-labels.consts.ts becomes a copy-free code registry; the 62 reject labels move to the kyc namespace and resolve at the render site, with unknown codes collapsing to FALLBACK as before. recover-funds no longer prints a raw loadingState or raw token amounts, and the KYC screens format dates through the active locale instead of a hardcoded en-US.
Marketing-shared components under Global/ keep their English and take copy as props instead: they render inside the app intl context, but the marketing site resolves its language from the URL, so a shared component would show the app locale on a marketing page. Fix UserCard.getTitle reading fullName/username while only depending on type, which rendered a stale name after a rename.
Extraction silently rewrote ' to the typographic form in 32 places, changing English copy that ships today (e.g. the balance-warning modal's "you're the only one who can access your funds"). Straighten them all so en matches source and reads consistently; the translated catalogs are unaffected. Also wrap the useSumsubKycFlow test in an intl provider — the hook now calls useTranslations and the suite never had one.
friendly-error.utils.tsx becomes copy-free: ErrorHandler is replaced by friendlyError(), returning a code or backend-provided text, resolved to a message by the new useFriendlyError hook. Backend copy (rain collateral, stale-card re-enable with its dynamic retry hint) passes through untranslated. Three sites compared the rendered error string against a constant to gate UI; those now compare the error CODE, so the gate survives translation.
src/features/payments was missed by every earlier pass — a lint probe caught it. Extends the existing payment namespace (no payments dupe); contributor and receipt counts use ICU plurals, the 'on <chain>' label reuses the shared tokenSelector.onChain rich-text key. Also covers the KYC status drawer, sumsub load-error, and amount-input balance label.
Scopes react/jsx-no-literals to the translated surface so new hardcoded JSX strings fail lint. Excludes tests, the /dev catalog, and marketing- shared Global components; allowlists non-copy glyphs (card masks, %, decorative emoji). Extraction stragglers the guard surfaced are also handled: the beta/demo banners and the transaction 'Enjoy Peanut!' title.
useSendMoney calls useTranslations, and it runs inside ContextProvider (via TokenContextProvider → useWallet), which was mounted ABOVE the intl provider — so every route 500'd with a missing-context error. Unit tests each wrap their subject in a provider, so only a full-app render caught it. Move AppIntlProvider to wrap ContextProvider (still inside PeanutProvider, which needs no translations).
'Video element not available' was shown to users when the video element lost the mount race. Say what it means to them instead. Adds a ClientProviders provider-order test: it walks the real element tree rather than rendering it, so the AppIntlProvider-wraps-ContextProvider contract is checked without mocking the wallet and kernel stack.
Everything except no-explicit-any and no-restricted-imports. Most of these
turned out to be malformed eslint-disable directives rather than the code
defects the rule names suggested.
react-hooks/exhaustive-deps (PerkClaimModal, 2): the disable comments used an
em-dash before the description instead of ESLint's `--` separator, so ESLint
parsed the whole string as a rule name. The suppressions were not suppressing
anything and both exhaustive-deps warnings were firing. Both effects are
deliberately mount-only, so the separator is fixed and the reasoning kept.
react/no-unknown-property (6): `<style jsx global>` is styled-jsx, a Next
built-in, not an invalid DOM attribute. Taught the rule via `ignore` in the
config — two files under Card/share-asset had already been hand-disabling it,
so those disables are now redundant and removed.
import/first (5), jsx-a11y/alt-text (1): both rules are unregistered here (the
config uses import-x, and jsx-a11y is not installed), so these directives only
produced "rule not found" errors. Removed; the Jest-ordering rationale is kept
as a plain comment. The alt-text site is a next/image test mock, not a real
accessibility defect.
react/display-name (1): named the forwardRef render function in a Card mock.
no-empty-object-type (1): ILinkDetails.rawOnchainDepositInfo `{}` ->
Record<string, unknown>; the field is declared but read nowhere.
Also removed 7 dead disable directives for rules that are off in tests
(no-require-imports) or no longer exist (no-var-requires).
eslint: 441 -> 424 errors, 157 -> 148 warnings. typecheck, jest (142 suites /
1974 tests) and prettier all green.
Rewrites all 55 bare `from '@/interfaces'` imports to the file that actually
owns the symbol. The barrel only re-exported ./interfaces and
./wallet.interfaces, so this was uniform except useAccountSetup.ts, which is
the one consumer of WalletProviderType. src/interfaces/index.ts had no
remaining referrers and is deleted.
Also removed a jest.mock('@/interfaces') in withdraw-states.test.tsx. The deep
imports made it dead (it no longer intercepted anything), and it was redundant
regardless: it stubbed AccountType with four members whose values match the
real enum exactly, while shadowing the other three (EVM_ADDRESS,
PEANUT_WALLET, MANTECA). The suite passes against the real enum.
eslint: 424 -> 368 errors. typecheck 0, jest 142 suites / 1974 tests green.
Rewrites all 45 bare `from '@/assets'` imports. The root barrel re-exported ten
category sub-barrels, so a single named import pulled the whole asset graph in.
Most symbols resolve to a leaf file (the sub-barrels are pure
`export { default as X } from './x.svg'` re-exports), so those become direct
default imports — which is already the dominant convention in this codebase.
The exceptions are the computed consts in assets/mascot and assets/cards
(PeanutWhistling = pick(webp, gif), APPLE_WALLET_STEPS); those are defined in
their category index rather than re-exported, so the index is the owning file
and they keep a named import from '@/assets/mascot'.
eslint: 368 -> 323 errors. typecheck 0, jest 142 suites / 1974 tests green.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Keeps the i18n calls while adopting dev's newer work: - AddMoneyBankDetails: dev's SEPA name-match checklist items extracted to bankDetails.doubleCheckSenderName/RecipientName in all three locales - SumsubKycWrapper test: dev's watchdog-era rewrite taken wholesale, with the NextIntlClientProvider wrapper re-added (component requires intl)
Code-analysis diffPainscore total: 6284.55 → 6627.25 (+342.7) 🆕 New findings (1381)
…and 1361 more. ✅ Resolved (1380)
…and 1360 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
Review — safe-mechanical, but too big & mis-based
|
kushagrasarathe
left a comment
There was a problem hiding this comment.
Requesting changes — split / rebase / reconcile
Sampled diff is safe-mechanical (i18n extraction + eslint cleanup; logic preserved in the dense files). But required before approval:
- Split into (a) eslint config + type-safety cleanup, (b) next-intl infra + provider + language page, (c) per-file string extraction — landed in that order. A ~314-file / +15k-line PR is unreviewable as one revert unit. If it genuinely can't be split, justify here and merge it LAST after all other FE PRs.
- Rebase onto
main(currently basedev, unlike every other FE PR) — or state thedev↔mainplan. As-is, this conflicts with everymainPR that back-merges, and thenative-build.jscollision with #2436 surfaces at the next release rather than now. - Reconcile
scripts/native-build.jswith #2436 and re-runnative-build-scan.test.js.
Reviewer spot-check target for whoever does the deep pass: the eslint "unused"-removal edits (deleted vars/interfaces, _-prefixed params) — the one place a mechanical PR this size can hide a regression. Everything sampled was clean.
kushagrasarathe
left a comment
There was a problem hiding this comment.
🟡 SECURITY conditions — supply-chain + undisclosed auth-transport rewrite
Adversarial scan of the full ~314-file diff found no external network call, no secret exfiltration, no code-exec primitive — the window.fetch monkeypatch (native-auth-capture.ts) is native-only, gated to the passkey verify endpoints, stores the token in on-device @capacitor/preferences, and sends it nowhere. Clean. But two real security concerns block a direct prod merge:
- Supply-chain age policy. New deps are below the 14-day floor (
minimum-release-age=20160):@capacitor/device+@capacitor/preferences(~3 days old),intl-messageformat(~4d),next-intl/use-intl(~10d). Wait them out, or use the documentedMIN_RELEASE_AGE_EXCLUDEoverride with justification. CI's supply-chain check must be green. - Disclose + separately review the native-auth transport rewrite riding inside this "localization" PR:
CapacitorHttpdisabled, header-basedAuthorizationauth, the fetch interceptor, andnative-canary.ts. It's security-sensitive and must not be rubber-stamped as i18n — ideally split into its own PR so it gets a focused auth review.
No malicious code found; these are policy + review-scope blocks, not defects.
Still open on this PRtwo blockers remain:
Also still applies: this targets |
Re the supply-chain age floor — that blocker is already satisfiedThe "~4 days old" / "~11 days" figures look like they came from each package's latest release rather than the version this branch actually resolves to. Checked the lockfile resolutions against npm registry publish dates:
CI agrees: The native-auth transport split is the one real remaining blocker, and I agree with it — |
Three conflicts, all the same shape — dev changed English literals that this branch had already moved into next-intl catalogs: - InvitesPage: adopt dev's vanity-claim copy split (Sign up CTA + a separate login label) via new/updated invites.* keys - InitiateKycModal: adopt the new region-unavailable (UK) variant with kyc.initiate.*RegionUnavailable + ctaWithdrawFunds keys - CardFace: adopt dev's error-state retry-eye restyle; re-localize the Virtual pill it reintroduced; Platinum stays a literal (brand lockup, jsx-no-literals expression container) All three catalogs updated in parity (en / es-419 / pt-BR).
|
Resolved the conflict with dev (merge commit 437efef). All three conflicts were the same shape — dev changed English copy that this branch had already moved into the next-intl catalogs — so the resolution keeps the
All three catalogs (en / es-419 / pt-BR) updated in parity. Verified post-merge: |
kushagrasarathe
left a comment
There was a problem hiding this comment.
Re-reviewed for the dev merge — clearing my change request. No longer conflicting (mergeable); the dev-merge introduced no net-new risk (only balance.utils.ts genuinely changed — hardcoded strings migrated into the intl errors namespace, callers updated, typecheck green). Supply-chain CI passes. Approving for merge to dev. Remaining items are dev→main PROMOTION gates, not dev-merge blockers: (1) on-device iOS+Android test of the native-auth transport rewrite, (2) reconcile native-build.js with the merged #2436 + re-run the scan test at the dev↔main integration.
… fix
Rolls up for the next TestFlight + Play internal build:
- main → mobile-release sync (push routing, app lock, step-up, CSP,
Manteca per-rail hotfix, Rhino minimums, Sentry noise) — #2483
- en / es-419 / pt-BR localization + 0 ESLint errors — #2447
- $0-balance fix: last-known spendable, balanceUnavailable, no
non-card /rain/cards poll — cherry-picked from #2482
# Conflicts: # pnpm-lock.yaml # src/app/(mobile-ui)/add-money/page.tsx # src/app/(mobile-ui)/notifications/page.tsx # src/app/(mobile-ui)/withdraw/crypto/page.tsx # src/app/(mobile-ui)/withdraw/page.tsx # src/app/ClientProviders.tsx # src/components/Card/ApplicationStatusScreen.tsx # src/components/Card/PhysicalCardScreen.tsx # src/components/Card/YourCardScreen.tsx # src/components/Global/GeneralRecipientInput/index.tsx # src/components/Global/QRScanner/index.tsx # src/components/Home/ActivationCTAs.tsx # src/components/Kyc/KycVerificationInProgressModal.tsx # src/components/Send/link/views/Initial.link.send.view.tsx # src/components/TransactionDetails/TransactionDetailsReceipt.tsx # src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx # src/components/TransactionDetails/transaction-details.utils.ts # src/components/Withdraw/views/Initial.withdraw.view.tsx # src/utils/__tests__/general.utils.test.ts
What this is
Two tightly-coupled efforts on one branch (the lint guard is what enforces the localization, so they ship together):
next-intl: 1,982 message keys per locale insrc/i18n/app/messages/for English, Latin-American Spanish (es-419), and Brazilian Portuguese (pt-BR), anAppIntlProvidermounted above the context tree, and an ESLint guard (react/jsx-no-literals, scoped to translated surfaces) that fails CI when someone hard-codes new UI copy.eslintCI job goes from 832 errors onmainto 0 on this branch.Synced with
mainas of 2026-07-17 (all 55 commits merged in, 9 conflicts resolved in favor of main's newer copy/logic with the i18n calls preserved).Lint stats: 832 → 0 errors
Baseline on
main(232 files) vs this branch:@typescript-eslint/no-explicit-any@next/next/no-html-link-for-pages<a>→next/linkacross marketing/app pages@typescript-eslint/no-unused-vars_-prefixedno-restricted-imports(barrel ban)@/interfaces,@/assets,@/context,@/components,@/config; dead barrels deleted@typescript-eslint/no-require-importsrequire-after-jest.mock()idiom exempted for test filesreact/no-unknown-propertyjsx/globalattrs whitelistedimport/firstdisplay-name,alt-text,no-empty-object-type, stale disable directives)Warnings: 183 → 61, almost all
react-hooks/exhaustive-deps. The provably-safe deps were fixed on this branch; the remainder genuinely change behavior when "fixed" and are left for a dedicated pass rather than blind suppression.The
no-explicit-anysweep (307 → 0)ChainWithTokens,IOnrampData, viem/zerodev exports,StaticImageData, react-hook-form generics);catch (e: any)→catch (e)withinstanceofnarrowing; opaque payloads (websocket, health probes) →unknownor a minimal local interface covering only the fields actually read; vendor globals (navigator.brave,screen.orientation.lock,window.opera) → typed structural casts.undefinedReact key on badge history entries (b.idis optional), and ananythat poisoned a TanStack query'sTErrorinference in qr-pay./devpages, thewindow.debugconsole cheats, the InvitesGraph debug visualization).New guards this branch adds (all clean at merge time)
react/jsx-no-literalson translated surfaces — hard-coded UI copy fails CI. Exempted: marketing (own i18n),/dev, shared primitives that take copy as props, and the hiddenfix-card-signaturesupport tool (support DMs the URL; English by design).react/no-unknown-propertyat error level with styled-jsx exemptions.router.back()ban, nuqshistory: 'push'ban, barrel-import ban, self-import ban).Verification
pnpm eslint .→ 0 errors, 61 warnings (warnings don't fail the job)pnpm tsc --noEmit→ cleanpnpm prettier --check .→ cleanNotes for reviewers
any→ typed conversions; both are covered by the test suite and were reviewed for behavior preservation (runtime semantics deliberately unchanged).feat/app-localization— every commit of that branch is contained here; it can be deleted after this merges to avoid a second divergent merge of the same work.