diff --git a/eslint.config.js b/eslint.config.js
index 4a7aa6fb04..43394020d5 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -141,6 +141,21 @@ module.exports = [
message:
"Don't pass { history: 'push' } to nuqs useQueryState(s) — a history entry per URL write breaks the back button (useSafeBack steps through same-screen states instead of leaving). Use the default 'replace'; the URL stays shareable. If a flow genuinely needs push-per-step, add a scoped file exemption with a comment (see useNativePlugins).",
},
+ {
+ // Toast copy must come from next-intl. `react/jsx-no-literals` below
+ // only inspects JSX children, so toasts fired from hooks and contexts
+ // (authContext, useLogin, useSendMoney, QRScanner) shipped English to
+ // every locale unnoticed.
+ //
+ // Deliberately NOT extended to `throw new Error('…')`: those messages
+ // are developer/Sentry breadcrumbs that the friendly-error mapper
+ // collapses to `errors.genericSupport` before any user sees them, so
+ // translating them would only fragment Sentry issue grouping.
+ selector:
+ "CallExpression[callee.object.name='toast'][callee.property.name=/^(error|success|info|warning|loading)$/] > :matches(Literal, TemplateLiteral):first-child",
+ message:
+ "Don't pass a string literal to toast.* — copy must come from next-intl. Import the right namespace with useTranslations and pass t('…'). If the value genuinely isn't copy (an id, a URL), assign it to a named const first.",
+ },
],
},
},
diff --git a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
index 1c9e6f0c0a..467d5cf4ad 100644
--- a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
+++ b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
@@ -16,8 +16,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars, react/display-name */
import React from 'react'
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
+import { IntlWrapper } from '@/test-utils/intl'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { NextIntlClientProvider } from 'next-intl'
import en from '@/i18n/app/messages/en.json'
// ---------- module-level mocks (must be before imports that depend on them) ----------
@@ -897,9 +897,9 @@ function createQueryClient() {
function renderWithProviders(component: React.ReactElement) {
const queryClient = createQueryClient()
return render(
-
+ {component}
-
+
)
}
diff --git a/src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx b/src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
index 09d93db3ad..742d730b96 100644
--- a/src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
+++ b/src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
@@ -10,9 +10,8 @@
import React from 'react'
import posthog from 'posthog-js'
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
+import { IntlWrapper } from '@/test-utils/intl'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
import { parseUnits } from 'viem'
import type { RailCapability } from '@/types/capabilities'
@@ -536,13 +535,13 @@ function renderQrPay(params: Record = {}) {
}
return render(
-
+
-
+
)
}
diff --git a/src/app/(mobile-ui)/receipt/page.tsx b/src/app/(mobile-ui)/receipt/page.tsx
index 4c64cccea0..9c99cb1a2e 100644
--- a/src/app/(mobile-ui)/receipt/page.tsx
+++ b/src/app/(mobile-ui)/receipt/page.tsx
@@ -53,7 +53,7 @@ export default function NativeReceiptPage() {
return (
-
+
{isLoading || !entry ? (
diff --git a/src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx b/src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
index 289a25f3e9..356ccb7c75 100644
--- a/src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
+++ b/src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
@@ -9,9 +9,8 @@
*/
import React from 'react'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import { IntlWrapper } from '@/test-utils/intl'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
import { parseUnits } from 'viem'
// ---------- module-level mocks (must be before imports that depend on them) ----------
@@ -226,11 +225,11 @@ function renderWithdraw(params: Record = {}) {
setSearchParams(params)
const queryClient = createQueryClient()
return render(
-
+
-
+
)
}
diff --git a/src/app/(mobile-ui)/withdraw/crypto/__tests__/crypto-withdraw-confirm.test.tsx b/src/app/(mobile-ui)/withdraw/crypto/__tests__/crypto-withdraw-confirm.test.tsx
index 563047ffbf..12a16931d1 100644
--- a/src/app/(mobile-ui)/withdraw/crypto/__tests__/crypto-withdraw-confirm.test.tsx
+++ b/src/app/(mobile-ui)/withdraw/crypto/__tests__/crypto-withdraw-confirm.test.tsx
@@ -14,8 +14,7 @@
*/
import React from 'react'
import { render as rtlRender, screen, fireEvent, waitFor } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
-import enMessages from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
// ---------- module-level mocks ----------
@@ -252,12 +251,6 @@ jest.mock('@/features/payments/shared/hooks/usePaymentRecorder', () => ({
import WithdrawCryptoPage from '../page'
-const IntlWrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-)
-
const render = (ui: React.ReactElement, options?: Omit[1], 'wrapper'>) =>
rtlRender(ui, { wrapper: IntlWrapper, ...options })
diff --git a/src/app/receipt/[entryId]/page.tsx b/src/app/receipt/[entryId]/page.tsx
index 68a4131f33..3514d97d9a 100644
--- a/src/app/receipt/[entryId]/page.tsx
+++ b/src/app/receipt/[entryId]/page.tsx
@@ -192,7 +192,7 @@ export default async function ReceiptPage({
return (
-
+
diff --git a/src/components/AddMoney/components/__tests__/MantecaAddMoney.test.tsx b/src/components/AddMoney/components/__tests__/MantecaAddMoney.test.tsx
index 4bd9f4f8f4..98c143596e 100644
--- a/src/components/AddMoney/components/__tests__/MantecaAddMoney.test.tsx
+++ b/src/components/AddMoney/components/__tests__/MantecaAddMoney.test.tsx
@@ -11,17 +11,10 @@
*/
import React from 'react'
import { render as rtlRender, screen } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
-import enMessages from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
// jest.mock calls are hoisted above this import, so the mocks below apply to it
import MantecaAddMoney from '../MantecaAddMoney'
-const IntlWrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-)
-
const render = (ui: React.ReactElement, options?: Omit[1], 'wrapper'>) =>
rtlRender(ui, { wrapper: IntlWrapper, ...options })
diff --git a/src/components/AddMoney/components/__tests__/MantecaPixQrDeposit.test.tsx b/src/components/AddMoney/components/__tests__/MantecaPixQrDeposit.test.tsx
index 0a80826169..5149804217 100644
--- a/src/components/AddMoney/components/__tests__/MantecaPixQrDeposit.test.tsx
+++ b/src/components/AddMoney/components/__tests__/MantecaPixQrDeposit.test.tsx
@@ -8,15 +8,9 @@
*/
import React from 'react'
import { render as rtlRender, screen } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
-const render = (ui: React.ReactElement) =>
- rtlRender(
-
- {ui}
-
- )
+const render = (ui: React.ReactElement) => rtlRender({ui})
const mockUseMantecaDepositPolling = jest.fn()
jest.mock('@/components/AddMoney/hooks/useMantecaDepositPolling', () => ({
diff --git a/src/components/AddWithdraw/__tests__/AddWithdrawCountriesList.test.tsx b/src/components/AddWithdraw/__tests__/AddWithdrawCountriesList.test.tsx
index ede4199b00..832a3d3acf 100644
--- a/src/components/AddWithdraw/__tests__/AddWithdrawCountriesList.test.tsx
+++ b/src/components/AddWithdraw/__tests__/AddWithdrawCountriesList.test.tsx
@@ -16,17 +16,11 @@
*/
import React from 'react'
import { render as rtlRender, screen, fireEvent, within } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
import AddWithdrawCountriesList from '../AddWithdrawCountriesList'
import underMaintenanceConfig from '@/config/underMaintenance.config'
-const render = (ui: React.ReactElement) =>
- rtlRender(
-
- {ui}
-
- )
+const render = (ui: React.ReactElement) => rtlRender({ui})
// ---- routing ----
const mockPush = jest.fn()
diff --git a/src/components/AddWithdraw/__tests__/AddWithdrawRouterView.test.tsx b/src/components/AddWithdraw/__tests__/AddWithdrawRouterView.test.tsx
index 00f5d81235..28eef3d5e1 100644
--- a/src/components/AddWithdraw/__tests__/AddWithdrawRouterView.test.tsx
+++ b/src/components/AddWithdraw/__tests__/AddWithdrawRouterView.test.tsx
@@ -13,8 +13,7 @@
*/
import React, { useEffect } from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
const mockRouterPush = jest.fn()
jest.mock('next/navigation', () => ({
@@ -137,12 +136,12 @@ function SelectedMethodProbe() {
function Harness({ user }: { user: MockUser }) {
mockUser = user
return (
-
+
-
+
)
}
diff --git a/src/components/Badges/__tests__/BadgeEarnToast.test.tsx b/src/components/Badges/__tests__/BadgeEarnToast.test.tsx
index 033af47fcd..ff556bf1c1 100644
--- a/src/components/Badges/__tests__/BadgeEarnToast.test.tsx
+++ b/src/components/Badges/__tests__/BadgeEarnToast.test.tsx
@@ -1,14 +1,8 @@
import { render as rtlRender, screen, act } from '@testing-library/react'
-import type { ComponentProps, ReactNode } from 'react'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
+import type { ComponentProps } from 'react'
import BadgeEarnToast from '@/components/Badges/BadgeEarnToast'
-const IntlWrapper = ({ children }: { children: ReactNode }) => (
-
- {children}
-
-)
const render = (ui: Parameters[0]) => rtlRender(ui, { wrapper: IntlWrapper })
// next/navigation — mutable pathname so we can exercise the /home gate; stable
diff --git a/src/components/Badges/__tests__/BadgesRow.test.tsx b/src/components/Badges/__tests__/BadgesRow.test.tsx
index b78cb2755a..fb79e5560f 100644
--- a/src/components/Badges/__tests__/BadgesRow.test.tsx
+++ b/src/components/Badges/__tests__/BadgesRow.test.tsx
@@ -1,16 +1,9 @@
import { render as rtlRender } from '@testing-library/react'
+import { IntlWrapper } from '@/test-utils/intl'
import React from 'react'
-import { NextIntlClientProvider } from 'next-intl'
-import enMessages from '@/i18n/app/messages/en.json'
import type { ComponentProps } from 'react'
import BadgesRow from '@/components/Badges/BadgesRow'
-const IntlWrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-)
-
const render = (ui: React.ReactElement, options?: Omit[1], 'wrapper'>) =>
rtlRender(ui, { wrapper: IntlWrapper, ...options })
diff --git a/src/components/Card/__tests__/ApplicationStatusScreen.test.tsx b/src/components/Card/__tests__/ApplicationStatusScreen.test.tsx
index 2f3bddf37c..6a655b7f24 100644
--- a/src/components/Card/__tests__/ApplicationStatusScreen.test.tsx
+++ b/src/components/Card/__tests__/ApplicationStatusScreen.test.tsx
@@ -9,15 +9,10 @@
*/
import React from 'react'
import { render as rtlRender, screen, fireEvent } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
+import { IntlWrapper } from '@/test-utils/intl'
import en from '@/i18n/app/messages/en.json'
import ApplicationStatusScreen from '@/components/Card/ApplicationStatusScreen'
-const IntlWrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-)
const render = (ui: React.ReactElement) => rtlRender(ui, { wrapper: IntlWrapper })
// NavHeader reads useAuth; stub it so the presentational screen renders alone.
diff --git a/src/components/Card/__tests__/CardCountryConfirmScreen.test.tsx b/src/components/Card/__tests__/CardCountryConfirmScreen.test.tsx
index eb41b18d79..ac55ddde16 100644
--- a/src/components/Card/__tests__/CardCountryConfirmScreen.test.tsx
+++ b/src/components/Card/__tests__/CardCountryConfirmScreen.test.tsx
@@ -10,15 +10,9 @@
*/
import React from 'react'
import { render as rtlRender, screen, fireEvent, waitFor } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
import CardCountryConfirmScreen from '@/components/Card/CardCountryConfirmScreen'
-const IntlWrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-)
const render = (ui: React.ReactElement) => rtlRender(ui, { wrapper: IntlWrapper })
// NavHeader reads useAuth; stub it so the presentational screen renders alone.
diff --git a/src/components/Card/__tests__/CardFace.test.tsx b/src/components/Card/__tests__/CardFace.test.tsx
index 5624667e16..83516ac401 100644
--- a/src/components/Card/__tests__/CardFace.test.tsx
+++ b/src/components/Card/__tests__/CardFace.test.tsx
@@ -8,15 +8,9 @@
*/
import React from 'react'
import { render as rtlRender, screen } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
import CardFace, { type RevealedCardDetails } from '@/components/Card/CardFace'
-const IntlWrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-)
const render = (ui: React.ReactElement) => rtlRender(ui, { wrapper: IntlWrapper })
const revealed: RevealedCardDetails = {
diff --git a/src/components/Card/__tests__/PhysicalCardScreen.test.tsx b/src/components/Card/__tests__/PhysicalCardScreen.test.tsx
index 5dfb9c8f27..95a0d1e1f4 100644
--- a/src/components/Card/__tests__/PhysicalCardScreen.test.tsx
+++ b/src/components/Card/__tests__/PhysicalCardScreen.test.tsx
@@ -7,18 +7,11 @@
*/
import React from 'react'
import { render as rtlRender, screen } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
-import enMessages from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import PhysicalCardScreen from '@/components/Card/PhysicalCardScreen'
import { rainApi } from '@/services/rain'
-const IntlWrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-)
-
const render = (ui: React.ReactElement, options?: Omit[1], 'wrapper'>) =>
rtlRender(ui, { wrapper: IntlWrapper, ...options })
diff --git a/src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx b/src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx
index af26bf0040..5a627b90c2 100644
--- a/src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx
+++ b/src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx
@@ -8,8 +8,7 @@
*/
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
+import { IntlWrapper } from '@/test-utils/intl'
// ---------- module-level mocks (before importing the component) ----------
@@ -129,9 +128,9 @@ const claimLinkData = {
function renderList() {
return render(
-
+
-
+
)
}
diff --git a/src/components/Claim/__tests__/claim-states.test.tsx b/src/components/Claim/__tests__/claim-states.test.tsx
index 7672a923a1..1e4f07a9f2 100644
--- a/src/components/Claim/__tests__/claim-states.test.tsx
+++ b/src/components/Claim/__tests__/claim-states.test.tsx
@@ -10,9 +10,8 @@
*/
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
+import { IntlWrapper } from '@/test-utils/intl'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { NextIntlClientProvider } from 'next-intl'
-import en from '@/i18n/app/messages/en.json'
// ---------- module-level mocks (must be before imports that depend on them) ----------
@@ -241,11 +240,11 @@ function createQueryClient() {
function renderClaim() {
const queryClient = createQueryClient()
return render(
-
+
-
+
)
}
diff --git a/src/components/Global/BackendErrorScreen/index.tsx b/src/components/Global/BackendErrorScreen/index.tsx
index 1cc3b9db23..3a99e6f00e 100644
--- a/src/components/Global/BackendErrorScreen/index.tsx
+++ b/src/components/Global/BackendErrorScreen/index.tsx
@@ -8,14 +8,8 @@ import posthog from 'posthog-js'
import { ANALYTICS_EVENTS } from '@/constants/analytics.consts'
// inline peanut icon svg to ensure it works without needing to fetch external assets
-const PeanutIcon = ({ className }: { className?: string }) => (
-