Skip to content

Conversation

@dieguezguille
Copy link
Member

@dieguezguille dieguezguille commented Jan 13, 2026

closes #615

Summary by CodeRabbit

  • New Features

    • Full Spanish translations and app-wide localization with device language detection; locale-aware dates, numbers, and currency formatting throughout the UI.
  • Bug Fixes

    • Card “not found” responses are now surfaced consistently instead of being silently suppressed.
  • Chores

    • Added device-localization support, Spanish spell-check resources, and patch release metadata for the mobile package.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 13, 2026

🦋 Changeset detected

Latest commit: 29fc6d8

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds app-wide internationalization: registers English and Spanish resources, detects device locale via expo-localization, localizes hundreds of UI strings (useTranslation/Trans), switches date/number formatting to i18n.language, adds Spanish cspell config, and makes a few small signature/error-handling tweaks.

Changes

Cohort / File(s) Summary
Release & Changesets
\.changeset/*, \.changeset/better-radios-joke.md, \.changeset/fruity-dingos-behave.md, \.changeset/shiny-socks-carry.md, \.changeset/young-items-happen.md
Added patch changeset files for @exactly/mobile.
Config & Dependencies
app.config.ts, package.json, cspell.json, .github/codecov.yml, nx.json
Added expo-localization and @cspell/dict-es-es, configured cspell overrides for Spanish translations, adjusted Codecov ignores, and added dependsOn to ESLint test target.
i18n Core & Resources
src/app/_layout.tsx, src/i18n/en.json, src/i18n/es.json
Initialize i18n with a language detector using expo-localization, register en/es resources, and add pluralization keys and a full Spanish translations file.
Routing / Tabs
src/app/(main)/(home)/_layout.tsx
Localized tab titles by using useTranslation() and t(title).
Mass UI Localization
src/components/... (e.g., activity/*, card/*, add-funds/*, auth/*, home/*, loans/*, pay-mode/*, send-funds/*, shared/*, swaps/*, defi/*, getting-started/*, benefits/*)
Widespread addition of useTranslation/Trans, replacing hard-coded strings with t(...)/<Trans> and switching date/number formatting to use i18n.language.
Formatting Helpers & Token/Selector Changes
src/components/swaps/SelectorModal.tsx, src/components/swaps/*
Token formatting helpers updated to accept language; TokenSelect/TokenList formatting and empty-state messages localized.
Component Signature / Export Changes
src/components/activity/ActivityItem.tsx, src/components/shared/LatestActivity.tsx, src/components/shared/ProcessingBalance.tsx, src/components/shared/ProcessingBalanceBanner.tsx
Small API/shape changes: getActivityTitle(item, t), formatting helpers now accept language, removed default LatestActivity.title, and converted some const components to named/default exported functions.
Utilities & Error Handling
src/utils/queryClient.ts, src/utils/server.ts, src/utils/useAuth.ts
Removed special-case suppression for card 404s, made getCard return null for specific status/code pairs and throw otherwise, and extended handleError to accept a t parameter for localized messages.
Spellchecker & Translation Config
src/i18n/es.json, cspell.json
Added Spanish translation file and configured cspell to import Spanish dictionary and allow specific Spanish words.
CI / Minor Manifest Edits
nx.json, .github/codecov.yml
Adjusted ESLint test dependencies and Codecov ignore patterns.

Sequence Diagram(s)

sequenceDiagram
    participant AppLayout as App Layout (src/app/_layout.tsx)
    participant ExpoLoc as expo-localization
    participant i18n as i18next
    participant UI as UI Components

    AppLayout->>ExpoLoc: getLocales()
    ExpoLoc-->>AppLayout: locale(s)
    AppLayout->>i18n: init({ detected locale, resources: en, es, languageDetector })
    i18n-->>UI: provide context (t / Trans / i18n.language)
    UI->>i18n: t("key") or <Trans>
    i18n-->>UI: localized string / locale value
    UI-->>AppLayout: render localized UI (numbers/dates formatted with i18n.language)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • cruzdanilo
  • franm91
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes out-of-scope changes unrelated to i18n: removal of AssetChange component, card 404 error handling changes, and codecov/nx config modifications. Remove or separate the following changes: deletion of AssetChange.tsx, card error handling in queryClient.ts/server.ts, and codecov.yml/nx.json configuration updates into distinct PRs.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '✨ add i18n' is concise and accurately describes the main change: adding internationalization support throughout the codebase.
Linked Issues check ✅ Passed The PR successfully implements i18n across components, configuration, and translation files, fulfilling the requirements of issue #615 to apply i18n to the application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch translations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link

Summary of Changes

Hello @dieguezguille, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's accessibility by implementing internationalization capabilities. The core change involves integrating expo-localization and providing a full Spanish translation, allowing the app to cater to a broader user base. This update touches almost every user-facing part of the application, ensuring a consistent and localized experience from onboarding to daily operations.

Highlights

  • Internationalization (i18n) Implementation: Introduced comprehensive internationalization support across the mobile application, enabling dynamic language switching and content translation.
  • Spanish Language Support: Added a complete Spanish translation file and configured the application to detect and utilize Spanish based on device settings.
  • Expo-Localization Integration: Integrated the expo-localization plugin for efficient language detection and management within the Expo environment.
  • Extensive UI Translation: Translated numerous user-facing strings and messages across various features, including activity details, fund management, authentication flows, card operations, DeFi integrations, loan processes, and fund transfers.
  • Improved Card Error Handling: Refined the getCard function to specifically handle 'card not found' errors by returning null, preventing generic API errors and improving application stability.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@cruzdanilo
Copy link
Member

@cursor review

@cursor
Copy link

cursor bot commented Jan 13, 2026

PR Summary

Introduces full i18n with automatic locale detection and Spanish translations, localizing major screens and components.

  • Integrates expo-localization and i18next language detector; loads en/es resources and replaces literals with t() across UI (auth, card, activity, DeFi, add funds, home, etc.)
  • Updates app config and dependencies (app.config.ts, package.json, lockfile) for localization
  • Fixes card 404 error handling in card details flow to reduce false errors
  • Adds Spanish spellcheck dictionary and per-file overrides in cspell.json
  • Tweaks build/tooling: nx.json eslint target depends on prepare; expand Codecov ignore to server/test/**

Written by Cursor Bugbot for commit fba9d73. This will update automatically on new commits. Configure here.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces internationalization (i18n) support to the application, adding Spanish translations and refactoring components to use the i18next library. The changes are extensive, covering configuration, dependency updates, and translation of hardcoded strings across numerous components. The implementation is mostly solid, but I've found a recurring issue with how i18n keys are constructed for pluralization, which goes against best practices and may cause issues. My review includes a specific comment on this pattern with a suggestion for a more robust approach.

@sentry
Copy link

sentry bot commented Jan 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.91%. Comparing base (4269f4f) to head (38f14b5).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #635      +/-   ##
==========================================
- Coverage   79.27%   78.91%   -0.37%     
==========================================
  Files          47       43       -4     
  Lines        1969     1987      +18     
  Branches      434      435       +1     
==========================================
+ Hits         1561     1568       +7     
- Misses        255      269      +14     
+ Partials      153      150       -3     
Flag Coverage Δ
github 78.91% <ø> (-0.37%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 37

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (20)
src/components/home/SpendingLimitsSheet.tsx (1)

58-67: Consider using lowercase translation keys where textTransform is applied.

The textTransform="uppercase" style (line 62) will uppercase the rendered text regardless of the translation value. Using a lowercase key like t("Pay now") would be more i18n-friendly, as some languages have different capitalization rules or may not support uppercase well.

The same applies to line 103 (t("INSTALLMENTS")) which also has textTransform="uppercase" applied.

♻️ Suggested change
-                    {t("PAY NOW")}
+                    {t("Pay now")}

And similarly for line 103:

-                      {t("INSTALLMENTS")}
+                      {t("Installments")}
src/components/send-funds/Amount.tsx (1)

172-198: Missing translations in error handling block.

The error messages and button text in this block are not translated, which is inconsistent with the rest of the component's i18n implementation.

Proposed fix
         <View gap="$s5" fullScreen padded justifyContent="center" alignItems="center">
           <Text body primary color="$uiNeutralPrimary">
-            {invalidReceiver ? "Invalid receiver address" : "Invalid asset address"}
+            {invalidReceiver ? t("Invalid receiver address") : t("Invalid asset address")}
           </Text>
           <Button
             dangerSecondary
             alignSelf="center"
             onPress={() => {
               if (router.canGoBack()) {
                 router.back();
               } else {
                 router.replace("/send-funds/asset");
               }
             }}
           >
-            <Button.Text>Go back</Button.Text>
+            <Button.Text>{t("Go back")}</Button.Text>
             <Button.Icon>
               <ArrowLeft size={24} color="$uiNeutralPrimary" />
             </Button.Icon>
           </Button>
         </View>
src/components/activity/details/SentActivity.tsx (1)

26-31: Consider using interpolation for the "Sent to" pattern.

The current implementation separates the translated label from the dynamic value with &nbsp;. For better localization support (especially RTL languages or different word orders), consider using i18next interpolation:

{t("Sent to {{address}}", { address: shortenHex(item.receiver) })}

However, since this maintains separate styling for the address portion, the current approach is acceptable if RTL support isn't required.

src/components/activity/details/PurchaseDetails.tsx (1)

85-97: Consider locale-aware date/time formatting.

The date-fns format function uses hardcoded format strings ("yyyy-MM-dd", "HH:mm:ss"). For full i18n support, consider using locale-aware formatting to match user's regional date/time preferences.

src/components/defi/AboutDefiSheet.tsx (1)

50-53: Verify the localized URL handling.

The link points to an English (/en/) Intercom article. For complete i18n support, consider making this URL locale-aware or providing localized versions of help articles.

src/components/swaps/TokenInput.tsx (1)

197-214: Consider consistent localization for currency displays.

The balanceUSD value (line 207-213) is wrapped with t() for the "Balance:" label, but valueUSD (lines 198-202) renders the currency amount directly without any translation wrapper. This creates an inconsistency in how the two currency values are presented.

If the "Balance:" label needs translation, consider whether the approximate value display (≈$X.XX) should also have a translatable pattern for consistency across locales.

src/components/shared/PaymentScheduleSheet.tsx (1)

77-77: Consider localizing the date format.

The date format "MMM d, yyyy" is hardcoded for English locales. For full i18n support, consider using a locale-aware format.

Optional: Use locale-aware date formatting

date-fns supports locale-aware formatting:

+import { enUS, es } from "date-fns/locale";
+// or use i18next's detected language to pick locale
+
-<Text title3>{format(new Date(maturity * 1000), "MMM d, yyyy")}</Text>
+<Text title3>{format(new Date(maturity * 1000), "PP", { locale: currentLocale })}</Text>

The "PP" format adapts to the locale (e.g., "Jan 13, 2026" in en-US vs "13 ene 2026" in es).

src/components/swaps/SelectorModal.tsx (1)

82-82: Title prop is passed through without translation when provided.

When a custom title is passed, it bypasses translation (line 126: title || t("Select Token")). Also, there's a casing inconsistency: default prop is "Select token" but fallback uses "Select Token".

Consider always translating the title:

Suggested fix
-  title = "Select token",
+  title = "Select Token",
-              {title || t("Select Token")}
+              {t(title)}

Also applies to: 126-126

src/components/roll-debt/RollDebt.tsx (2)

86-96: Multiple user-facing strings in the Pay component are not translated.

The RolloverButton has toast messages translated, but the parent Pay component has several hardcoded strings that should also be wrapped with t() for consistency:

  • Line 87: "Review rollover"
  • Line 93: "Debt to rollover"
  • Line 96: "due ..."
  • Line 111: "Rollover interest"
  • Line 123: "APR"
  • Line 142: "Current debt"
  • Line 160: "Total after rollover"

Consider adding const { t } = useTranslation() to the Pay component and wrapping these strings.


312-312: Button text is not translated.

The "Confirm rollover" button text should be wrapped with t() for consistency with other translated strings in this component.

Suggested fix
-      Confirm rollover
+      {t("Confirm rollover")}
src/components/send-funds/Receiver.tsx (1)

89-89: Avoid inline styles with the style prop.

This line uses an inline style object literal, which violates the coding guidelines. All styling should go through Tamagui's styled system props.

♻️ Proposed fix
                        onChangeText={handleChange}
-                       style={{ borderColor: "$uiNeutralTertiary", borderWidth: 1, fontSize: 15 }}
+                       borderColor="$uiNeutralTertiary"
+                       borderWidth={1}
+                       fontSize={15}
                        focusStyle={{ borderColor: "$borderBrandStrong", borderWidth: 1 }}

As per coding guidelines, all styling must use Tamagui's styled system props rather than React Native's style prop.

src/components/activity/details/RepayActivity.tsx (1)

29-33: Consider aligning number/currency locale with app language.

The toLocaleString(undefined, ...) calls use the system locale, which may differ from the app's selected language. This could result in Spanish UI text with English-formatted numbers (or vice versa).

For consistency, consider using the i18n language:

const { t, i18n } = useTranslation();
// ...
usdAmount.toLocaleString(i18n.language, { ... })

This is a broader consideration that applies across the codebase, not just this file.

Also applies to: 37-37

src/components/send-funds/Asset.tsx (1)

82-93: Avoid storing translated strings in the query cache.

The t("New Contact") call on lines 89-90 stores a translated string as the contact's name in the query cache. If the user changes language, this cached name will remain in the previous language, causing inconsistent UI.

Store a language-agnostic key or identifier instead, and translate at display time.

🐛 Proposed fix
                   if (hasContact) {
                     return old?.filter((contact) => contact.address !== receiver);
                   } else {
                     return old && old.length > 0
-                      ? [...old, { name: t("New Contact"), address: receiver, ens: "" }]
-                      : [{ name: t("New Contact"), address: receiver, ens: "" }];
+                      ? [...old, { name: "", address: receiver, ens: "" }]
+                      : [{ name: "", address: receiver, ens: "" }];
                   }

Then display a fallback translated string where contacts are rendered when name is empty (e.g., contact.name || t("New Contact")).

src/components/pay-mode/OverduePayments.tsx (1)

106-106: Date format is not locale-aware.

The date is formatted with format(new Date(...), "MMM dd, yyyy") which produces English month abbreviations regardless of the user's language. For proper i18n, consider using locale-aware date formatting.

♻️ Suggested approach using date-fns locale
+import { es, enUS } from "date-fns/locale";
+import { useTranslation } from "react-i18next";

// Inside the component, determine locale based on i18n language:
+const { t, i18n } = useTranslation();
+const dateLocale = i18n.language === "es" ? es : enUS;

// Then use it in format:
-{format(new Date(Number(maturity) * 1000), "MMM dd, yyyy")}
+{format(new Date(Number(maturity) * 1000), "MMM dd, yyyy", { locale: dateLocale })}
src/components/card/Card.tsx (1)

434-436: Missed translation for title prop.

The title="Latest purchases" string should be translated for consistency with other i18n changes in this file.

🐛 Suggested fix
 <LatestActivity
   activity={purchases}
-  title="Latest purchases"
+  title={t("Latest purchases")}
   emptyComponent={
src/components/add-funds/Bridge.tsx (5)

306-306: Several internal status messages remain untranslated.

While the main UI labels are translated, several status messages set via setBridgeStatus() are not:

  • Line 306: Switching to ${selectedGroup?.chain.name}...
  • Line 320: "Checking allowance..."
  • Line 341: "Submitting bridge transaction..."
  • Line 349, 358: "Bridge transaction submitted"
  • Line 395: "Submitting transfer transaction..."
  • Line 405: "Transfer transaction submitted"

These strings are displayed to users via statusMessage on line 869. Consider wrapping them with t() for consistency.

Also applies to: 320-320, 341-341, 349-349


362-366: Toast success messages are not translated.

The toast messages for successful bridge/transfer operations remain untranslated:

-toast.show("Bridge transaction submitted", {
+toast.show(t("Bridge transaction submitted"), {
-toast.show("Transfer transaction submitted", {
+toast.show(t("Transfer transaction submitted"), {

Also applies to: 408-412


971-980: Error toast in handleError is not translated.

The handleError function is defined outside the component and cannot access the t() hook. To translate the error message, either:

  1. Move handleError inside the component
  2. Pass t as a parameter:
-function handleError(error: unknown, toast: ReturnType<typeof useToastController>, isTransfer?: boolean) {
+function handleError(
+  error: unknown,
+  toast: ReturnType<typeof useToastController>,
+  t: ReturnType<typeof useTranslation>["t"],
+  isTransfer?: boolean,
+) {
   if (error instanceof UserRejectedRequestError) return;
   if (error instanceof TransactionExecutionError && error.shortMessage === "User rejected the request.") return;
-  toast.show(`${isTransfer ? "Transfer" : "Bridge"} failed. Please try again.`, {
+  toast.show(
+    isTransfer
+      ? t("Transfer failed. Please try again.")
+      : t("Bridge failed. Please try again."),
+    {

461-461: Processing screen status text is not translated.

The label variable ("Transfer" or "Bridge") is used to construct user-facing status messages that are not translated:

-const label = isTransfer ? "Transfer" : "Bridge";
+const label = isTransfer ? t("Transfer") : t("Bridge");
...
-{isError
-  ? `${label} failed`
-  : isSuccess
-    ? `${label} transaction submitted`
-    : `Processing ${label.toLowerCase()}`}
+{isError
+  ? t("{{label}} failed", { label })
+  : isSuccess
+    ? t("{{label}} transaction submitted", { label })
+    : t("Processing {{label}}", { label: label.toLowerCase() })}

Alternatively, use separate translation keys for each state to give translators more control over sentence structure.

Also applies to: 500-506


731-742: Balance label is not translated.

The balance display on line 732 uses a hardcoded string while TokenInput (from relevant code snippets) uses a translated version:

-{`Balance: ${Number(
+{t("Balance: {{value}}", { value: Number(
   formatUnits(
     (destinationBalance * parseUnits(destinationToken.priceUSD, 18)) / WAD,
     destinationToken.decimals,
   ),
 ).toLocaleString(undefined, {
   style: "currency",
   currency: "USD",
   currencyDisplay: "narrowSymbol",
-})}`}
+}) })}
🤖 Fix all issues with AI agents
In @package.json:
- Line 80: Update the expo-localization dependency version specifier to match
the Expo SDK 54 bundled version: change the version string for
"expo-localization" from ^17.0.8 to ~17.0.8 in package.json so the native module
aligns with bundledNativeModules and avoids unintended minor upgrades.

In @src/components/activity/ActivityItem.tsx:
- Around line 167-168: The current runtime-generated translation key in
ActivityItem.tsx (the expression t(item.type.charAt(0).toUpperCase() +
item.type.slice(1)) used when computing title) prevents i18n extraction and
misses translations; replace it with an explicit mapping from known item.type
values to static translation keys (e.g., a const ActivityTypeLabels = { foo:
'ActivityType.Foo', bar: 'ActivityType.Bar', ... }) and look up
ActivityTypeLabels[item.type] then call t(mappedKey) with a fallback to
t('Unknown'); update the title assignment in the same function to use this
mapping (and export/update the mapping as needed so extraction tools pick up the
keys).

In @src/components/activity/details/TransactionDetails.tsx:
- Line 87: The hardcoded date/time formats in TransactionDetails (the
format(item.timestamp, "yyyy-MM-dd") and format(item.timestamp, "HH:mm:ss")
calls) are not locale-aware; update these to use a locale-aware formatter—either
switch to date-fns format with an imported locale option (pass { locale:
yourLocale } to format) or use the built-in Intl.DateTimeFormat /
Date.prototype.toLocaleDateString and toLocaleTimeString with the current user's
locale; modify the render in TransactionDetails to call the chosen locale-aware
formatter for both the date and time values so the UI respects user locale
preferences.

In @src/components/activity/PendingProposals.tsx:
- Around line 58-75: getProposalLabel currently returns raw English strings
which are later passed to t(label) in ProposalItem; instead translate at the
source so translation extraction can find keys. Update getProposalLabel to
return translated strings by accepting the i18n function (e.g., t: TFunction) as
an argument and call t(...) for each case (reference: getProposalLabel and
ProposalItem), then remove the t(label) call in ProposalItem and ensure any
callers that used getProposalLabel now pass through the same t (also update
getProposal if it exists to accept and forward t). Keep the same keys/phrasing
when calling t to preserve existing translation keys.

In @src/components/add-funds/AddCrypto.tsx:
- Around line 179-181: The JSX contains a fragile standalone space node {" "}
before the translated text in AddCrypto.tsx; remove the {" "} and include the
leading space inside the translation value (or adjust the translation key) used
in t("Learn more about adding funds.") so the rendered Text node contains the
space as part of the translation string instead of a separate JSX child.

In @src/components/add-funds/Bridge.tsx:
- Around line 658-659: The "Exa Account" label inside the Text component is not
localized; either wrap that literal with the project's i18n translation helper
(e.g., use t('Exa Account') or the app's translation utility) where Text is
rendered, or explicitly mark it as an intentional brand string with a code
comment. Update the usage near the Text element that composes "Exa Account |
{shortenHex(account ?? zeroAddress, 4, 6)}" (in Bridge.tsx) to use translation
or add the explanatory comment so reviewers know it is intentionally
untranslated.

In @src/components/auth/PasskeysAbout.tsx:
- Around line 52-56: The JSX embeds trailing spaces in the template literal for
the label in PasskeysAbout.tsx (the Text containing {`${t("Strong
credentials.")}   `}), which should be moved into the translation or handled by
layout; update the code so the translation key itself includes any required
trailing spacing (e.g., "Strong credentials. " or a non-breaking space) or
render the bold and following sentence as two Text elements and use CSS/margin
(or a separator element) to control spacing instead of hardcoded spaces in the
template literal.

In @src/components/card/exa-card/CardContents.tsx:
- Around line 100-104: In CardContents, the same label JSX using t("Available
balance") is duplicated in both branches; extract it into a single constant
(e.g., const balanceLabel = (<View><Text color="white" emphasized caption
maxFontSizeMultiplier={1} textTransform="uppercase">{t("Available
balance")}</Text></View>);) declared before the return in the CardContents
component and replace the duplicated JSX in both the credit and debit branches
with {balanceLabel} so the label is maintained in one place.

In @src/components/card/SpendingLimits.tsx:
- Around line 57-61: The Pressable in SpendingLimits.tsx uses an inline style
object (style={{ alignSelf: "center" }}); remove the inline style and apply
Tamagui styling instead — either pass the Tamagui layout prop (e.g.,
alignSelf="center") directly if Pressable is the Tamagui component or create a
styled component (e.g., const CenteredPressable = styled(Pressable, { alignSelf:
"center" })) and replace the Pressable with that; ensure onClose, hitSlop and
children remain unchanged.

In @src/components/getting-started/GettingStarted.tsx:
- Line 206: The component GettingStarted.tsx currently uses separate translation
keys for singular/plural step messages; switch to i18next pluralization by
replacing those distinct keys with a single pluralized key (e.g., "stepsLeft")
and call t('stepsLeft', { count: stepsRemaining }) where stepsRemaining is the
number you compute in the component; also update the translation JSON to include
the plural forms (e.g., "stepsLeft_one" and "stepsLeft_other" or the
language-appropriate keys) so i18next can select the correct form automatically.

In @src/components/home/Home.tsx:
- Around line 150-153: Replace the long literal strings used as translation keys
in Home.tsx with descriptive i18n keys: instead of passing the full English
sentence to t(...) for the title and actionText, use keys like
"home.cardUpgrade.migrationNotice" and "home.cardUpgrade.actionText"; update the
call sites in the Home component (the title and actionText props) and add
corresponding entries to your translation resource files (e.g., en.json) mapping
those keys to the current English strings so existing behavior is preserved.

In @src/components/home/HomeActions.tsx:
- Line 96: The code currently calls t(title) (in Button.Text) which uses a
variable key and prevents static extraction; ensure the literal keys "Add funds"
and "Send" are added to all translation files, or refactor the static actions
array to expose literal keys (e.g., use a titleKey field with an `as const`
typed union) and change Button.Text to call t(titleKey) so i18next-parser can
statically detect keys; update occurrences around the actions array and uses at
lines where Button.Text renders (including the other instances noted at
108-111).

In @src/components/home/HomeDisclaimer.tsx:
- Around line 17-49: The three identical Text usages (the objects named app,
protocol, and terms in HomeDisclaimer.tsx) should be refactored to remove
duplication: create a single reusable styled component or shared props constant
(e.g., LinkText component or linkTextProps) that encapsulates cursor="pointer",
caption2, color="$interactiveOnDisabled", and textDecorationLine="underline",
then replace the three inline <Text ...> usages to render that component (or
spread the shared props) and keep their unique onPress handlers
(openBrowser(...) or presentCollection("10544608")). Ensure you import/export
the new component/constant appropriately and update the three references to use
it.

In @src/components/home/PortfolioSummary.tsx:
- Around line 83-93: The translation interpolation contains a redundant ternary
since the outer conditional `{processingBalance ? (` already ensures truthiness;
in the `Text` using `t("Processing balance {{amount}}", { amount:
processingBalance ? processingBalance.toLocaleString(...) : "" })` remove the
inner `processingBalance ? ... : ""` and pass the formatted string directly
(e.g., `amount: processingBalance.toLocaleString(...)`) so `processingBalance`
is only checked once and the dead branch is eliminated; update the `Text`/`t`
call in PortfolioSummary (the Processing balance block) accordingly.

In @src/components/home/VisaSignatureSheet.tsx:
- Around line 41-45: The toast message in the onSuccess handler currently calls
toast.show(t("Card deactivated successfully")) which is misleading for an
upgrade flow; update the message to reflect an upgrade (e.g., call
toast.show(t("Card upgraded successfully"), { native: true, duration: 1000,
burntOptions: { haptic: "success" } }) or use the appropriate translation key
for "Card upgraded successfully") so the user-facing text aligns with the
upgrade action.

In @src/components/loans/Amount.tsx:
- Line 92: The translation call in the Amount component currently places the
colon outside the i18n string (`t("Available funding")}:`), which prevents
translators from controlling punctuation and spacing; update the JSX to include
the colon inside the translation key (e.g., `t("Available funding:")`) and
update your locale resource files to add the corresponding "Available funding:"
key (and adjust any other occurrences of translated labels with external
punctuation), ensuring the component (Amount) uses the new key so translators
can handle language-specific spacing and punctuation.

In @src/components/loans/CreditLine.tsx:
- Around line 56-58: The date formatting in the CreditLine component currently
uses a hardcoded format string in the format(firstMaturity * 1000, "MMM d,
yyyy"), which is not locale-aware; update the call to produce localized output
by either (a) switching to a locale-aware date-fns format like format(new
Date(firstMaturity * 1000), "PPP", { locale: yourLocale }) and wiring in the
appropriate date-fns locale based on app context/props, or (b) using
Intl.DateTimeFormat with the user's locale (e.g., new
Intl.DateTimeFormat(userLocale, { dateStyle: "long" }).format(new
Date(firstMaturity * 1000))) and render that string in the Text component;
ensure the chosen locale comes from app state or props so it adapts to user
preferences and replace the current format(...) call inside the Text element in
CreditLine.

In @src/components/loans/LoanSummary.tsx:
- Around line 42-59: The useMemo for apr is being invalidated every render
because timestamp (computed from Date.now()) is included in the dependency
array; remove timestamp from the dependency array in the useMemo that defines
apr (referencing apr, useMemo, and timestamp) if slight staleness is acceptable,
or instead compute a stable timestamp (e.g., memoize timestamp in the parent or
use a useRef/useMemo to freeze Date.now() for the component lifetime) and
replace the timestamp usage so the apr memo only depends on borrow,
installments, isBorrow, and loan.amount.

In @src/components/loans/Review.tsx:
- Around line 173-177: The statusMessage variable is already set to t("Funding
request processing") when neither error nor success is true, so the separate
prefix t("Processing") + " " that is prepended during render causes duplication;
fix by removing the prefix (the code that concatenates t("Processing") + " "
before statusMessage) so you only render statusMessage, or alternatively change
the default statusMessage value to t("Funding request") so combining with the
existing prefix reads naturally; refer to the statusMessage declaration and the
render location where t("Processing") is prepended to locate and update the
code.

In @src/components/pay-mode/Pay.tsx:
- Line 546: The translation calls like t("Debt", { defaultValue: "Debt" }) are
redundant because react-i18next returns the key as fallback; remove the
unnecessary defaultValue and replace with t("Debt") (and similarly simplify
other identical pairs) inside the Pay component where this pattern appears
(search for t("Debt", { defaultValue: "Debt" }) to locate occurrences) to clean
up the code unless that defaultValue was intentionally required for a specific
tool or comment.
- Line 503: The hardcoded English format in the dueDateFormatted computation
ignores user locale; change the formatting to use date-fns locale-aware
formatting (e.g., use format(date, "PPP", { locale })) instead of "MMM dd,
yyyy", obtain the user's locale via
Intl.DateTimeFormat().resolvedOptions().language (or from app i18n settings),
map it to the corresponding date-fns locale import (with a safe fallback like
enUS), and update the dueDateFormatted expression that uses maturity to call
format(new Date(Number(maturity) * 1000), "PPP", { locale }) so dates render
according to the user locale.

In @src/components/pay-mode/PaymentsActions.tsx:
- Line 30: Remove redundant defaultValue props from i18n calls in
PaymentsActions.tsx: locate the t(...) usages where the translation key equals
the provided default (for example the Text using t("Auto-pay", { defaultValue:
"Auto-pay" })) and drop the { defaultValue: "..." } object so the calls become
t("Auto-pay") (and do the same for the two other similar t(...) calls in this
component).

In @src/components/pay-mode/PaymentSheet.tsx:
- Around line 185-188: The date formatting in the PaymentSheet component uses
format(dueDate, "MMM dd, yyyy") which forces English month names; update the
call to pass the user's locale as the third argument (e.g., format(dueDate, "MMM
dd, yyyy", { locale: <locale> })) so month names are localized. Ensure you reuse
the same locale value or import the appropriate date-fns locale used elsewhere
(like the one used with formatDistance) and update the format invocation near
the Text rendering to include that locale option.

In @src/components/pay-mode/PaySelector.tsx:
- Line 117: The t(...) calls in the PaySelector component include redundant
defaultValue properties when the default string equals the key (e.g., t("Pay
Mode", { defaultValue: "Pay Mode" }) at the PaySelector component), so remove
the { defaultValue: "..."} option from those t(...) calls (including the
occurrences noted around lines 117 and 136) — leave the raw t("Pay Mode") (and
other similar t("...")) calls intact, and only keep explicit defaultValue when
the fallback text differs from the key or you want an intentional in-code
documentation string.
- Around line 75-81: The message construction uses an inline ternary to pick
singular vs plural in defaultValue; replace that by using react-i18next
pluralization: keep the zero-case using t("Pay Now selected") when value === 0,
and for value > 0 call t("Installments selected", { count: value }) (remove the
inline ternary/defaultValue logic). Add translation keys "Installments
selected_one" and "Installments selected_other" in your locales (and a "Pay Now
selected" key) so i18n handles singular/plural automatically; locate the code
around the message variable and the t(...) calls to update.

In @src/components/pay-mode/RepayAmountSelector.tsx:
- Around line 173-179: The currency formatting in RepayAmountSelector uses
toLocaleString(undefined, ...) which picks the system locale and can mismatch
the t(...) translation language; update the formatting to use the app i18n
locale (e.g., use i18n.language from the useTranslation hook) when calling
toLocaleString for balancerBalance so the currency formatting matches the
translated text and ensure the component imports/uses useTranslation to access
i18n.language before passing it into toLocaleString.

In @src/components/send-funds/Amount.tsx:
- Around line 235-238: Move punctuation into the translation keys so translators
can localize punctuation/spacing; replace usages like t("To") with t("To:") (and
the other instance at the block around the 257-259 region) and update the i18n
resource files to include the keys with the colon (e.g., "To:": "To:" and
locale-specific variants) so the Text components render t("To:") and
t("<otherKey>:") instead of appending a colon in the JSX.

In @src/components/send-funds/QR.tsx:
- Around line 112-114: The literal string in the Text component uses hard-coded
button labels ("Continue" and "Back") which can diverge from translated button
text; update the translation call in QR.tsx (the Text with t(...) around "Press
'Continue'...") to use interpolation and pass in t("Continue") and t("Back") as
parameters so the displayed labels always match the actual translated button
labels used elsewhere.

In @src/components/shared/AddressDialog.tsx:
- Line 53: The translation strings in AddressDialog.tsx use raw sentence keys
(e.g., t("Network reminder") and the other string at line 87); replace them with
structured keys like t("addressDialog.title") and t("addressDialog.subtitle")
(or similar names) in the AddressDialog component, then add corresponding
entries to your i18n translation resource files (for all locales) mapping
addressDialog.title and addressDialog.subtitle to the original phrases; ensure
any unit tests or usages referencing the old literal keys are updated to the new
structured keys.
- Around line 69-75: Replace the split translation calls in AddressDialog.tsx
with a single interpolated translation: create a translation key (e.g.,
"addFundsDisclaimer") that includes a placeholder for the network, import Trans
from react-i18next, and render it where the current Text block is (replace the
three fragments that use t("Add funds using"), {chain.name}, and t("only.
Sending assets...")). Use Trans's components prop to map the bold/ emphasized
part to your existing Text element (e.g., <Text emphasized secondary>) so the
network name (chain.name) is injected via {{network}} and translators can
reorder the sentence as needed.

In @src/components/shared/CopyAddressSheet.tsx:
- Around line 39-43: The translation strings in CopyAddressSheet (the Text nodes
rendering t("Address copied") and t("Double-check your address before sending
funds to avoid losing them.")) use natural-language keys; change them to
structured keys (e.g., "copyAddressSheet.addressCopied" and
"copyAddressSheet.warning") and update the i18n resource files accordingly, then
replace the two t(...) calls inside the CopyAddressSheet component to use those
new keys so translations remain organized and easier to refactor.

In @src/components/shared/Failure.tsx:
- Line 58: The hardcoded date format call using format(new Date(Number(maturity)
* 1000), "MMM dd, yyyy") always emits English month names; update the component
to produce locale-aware output by formatting the date with the current locale
before passing it to t("Due {{date}}", { date: ... }). Replace the date-fns call
with a locale-aware approach: either use Intl.DateTimeFormat(i18n.language, {
month: "short", day: "2-digit", year: "numeric" }) with the existing
i18n.language (or use useTranslation()/i18n from react-i18next to get the
language) or use date-fns format with a locale option (format(date, "PP", {
locale: localeFor(i18n.language) }))—ensure you map/load the correct date-fns
locale based on i18n.language. Keep the t call as t("Due {{date}}", { date:
localizedDateString }) and remove the hardcoded "MMM dd, yyyy" usage.

In @src/components/shared/LatestActivity.tsx:
- Around line 27-29: The component LatestActivity currently calls t(title ??
"Latest activity"), which passes a dynamic key to the translation function;
change it to avoid dynamic keys by rendering the title prop as a
pre-translated/display string when provided and only calling t with the static
key for the fallback (e.g., use title if truthy, else t("Latest activity")), or
alternatively enforce/validate title against a fixed enum of allowed translation
keys before calling t; update the Text usage in LatestActivity to use the chosen
approach so t is only ever called with a static key.

In @src/components/shared/Success.tsx:
- Line 78: The displayed date is formatted with a hardcoded English pattern
(format(..., "MMM dd, yyyy")) in the Success component, so update the JSX
expression that renders the date to use a locale-aware formatter: obtain the
user locale (e.g., from i18n.language or navigator.language), then format the
date either via Date.prototype.toLocaleDateString(dateLocale, { month: "short",
day: "2-digit", year: "numeric" }) or by using date-fns format with a locale
option (pass the appropriate date-fns locale object mapped from i18n.language
into format(date, "MMM dd, yyyy", { locale })). Replace the current format(...)
call with that locale-aware call and add the necessary import/mapping for
date-fns locales if you choose the date-fns approach.

In @src/utils/languageDetector.ts:
- Around line 3-9: The custom detector object (export default {...}) lacks an
explicit i18next type; change its declaration to be typed as
LanguageDetectorAsyncModule so IDEs and the compiler validate the shape (ensure
the exported object implements type: "languageDetector", async: true, and
detect: (callback) => void), import LanguageDetectorAsyncModule from "i18next"
and annotate the exported constant with that type, keeping the existing
getLocales() usage and detect implementation unchanged.

</View>
<AssetSelectSheet
label="Select asset to send"
label={t("Select asset to send")}
Copy link

Choose a reason for hiding this comment

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

Double translation causes incorrect i18n handling

Low Severity

The AssetSelectSheet component expects an untranslated key for its label prop (default value is "Select asset") and translates it internally with {t(label)}. However, Bridge.tsx passes already-translated strings like label={t("Select asset to send")}. This causes the text to be translated twice, which may work by accident but is incorrect — the second t() call attempts to look up the translated string as a key.

Additional Locations (2)

Fix in Cursor Fix in Web

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 21

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
src/components/roll-debt/RollDebt.tsx (1)

311-313: Button text should be translated.

The t() function is already available in this component but the button label remains hardcoded. Wrap it for consistency with the toast messages.

Suggested fix
-    >
-      Confirm rollover
+    >
+      {t("Confirm rollover")}
src/components/shared/PaymentScheduleSheet.tsx (1)

77-77: Date format is not locale-aware.

The date formatting uses a hardcoded English format pattern. For consistent i18n, pass the locale to date-fns:

import { es, enUS } from "date-fns/locale";

// Then in format call:
format(new Date(maturity * 1000), "PPP", { locale: currentLocale })

Alternatively, use a format pattern appropriate for both languages or leverage i18n.language to select the locale dynamically.

src/components/loans/Review.tsx (2)

322-324: Date format is not localized.

The format() function from date-fns defaults to English locale ("MMM d, yyyy" → "Jan 14, 2026"). For consistent i18n support, consider using locale-aware date formatting.

Option 1: Use Intl.DateTimeFormat
 <Text headline color="$uiNeutralPrimary">
-  {format(new Date(Number(maturity) * 1000), "MMM d, yyyy")}
+  {new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(new Date(Number(maturity) * 1000))}
 </Text>
Option 2: Pass locale to date-fns format
import { es, enUS } from "date-fns/locale";
// Then use the current i18n language to select locale
format(new Date(Number(maturity) * 1000), "PPP", { locale: i18n.language === "es" ? es : enUS })

319-335: LGTM!

The conditional installment due translations are correctly implemented.

One optional consideration: t("Every 28 days") hardcodes the interval. If this value could change, consider using interpolation with MATURITY_INTERVAL:

t("Every {{days}} days", { days: MATURITY_INTERVAL / 86400 })
src/components/swaps/SelectorModal.tsx (1)

82-82: Inconsistent casing and unreachable fallback.

The default prop value title = "Select token" (line 82) means the t("Select Token") fallback (line 126) is only reached when title is explicitly passed as an empty string. Additionally, the casing differs ("token" vs "Token").

Consider aligning both to use the same translated key:

♻️ Suggested fix
-  title = "Select token",
+  title,

This ensures the fallback t("Select Token") is used when no title is provided, making the translation consistent.

Also applies to: 126-126

src/components/send-funds/Amount.tsx (1)

172-198: Untranslated error messages in early-return block.

The error messages and button text in this block are not wrapped with t(), unlike other UI strings in the component. This creates an inconsistent user experience for non-English users.

♻️ Suggested fix
         <Text body primary color="$uiNeutralPrimary">
-            {invalidReceiver ? "Invalid receiver address" : "Invalid asset address"}
+            {invalidReceiver ? t("Invalid receiver address") : t("Invalid asset address")}
         </Text>
-            <Button.Text>Go back</Button.Text>
+            <Button.Text>{t("Go back")}</Button.Text>
src/components/add-funds/Bridge.tsx (2)

500-506: Untranslated status labels in processing state.

The label variable ("Transfer" or "Bridge") is used directly in string construction without translation, resulting in untranslated UI text:

  • ${label} failed
  • ${label} transaction submitted
  • Processing ${label.toLowerCase()}
🔧 Proposed fix
-    const label = isTransfer ? "Transfer" : "Bridge";
+    const label = isTransfer ? t("Transfer") : t("Bridge");
...
     <Text secondary body>
       {isError
-        ? `${label} failed`
+        ? t("{{label}} failed", { label })
         : isSuccess
-          ? `${label} transaction submitted`
-          : `Processing ${label.toLowerCase()}`}
+          ? t("{{label}} transaction submitted", { label })
+          : t("Processing {{label}}", { label: label.toLowerCase() })}
     </Text>

Alternatively, use distinct translation keys for each state:

const statusText = isError
  ? t(isTransfer ? "Transfer failed" : "Bridge failed")
  : isSuccess
    ? t(isTransfer ? "Transfer transaction submitted" : "Bridge transaction submitted")
    : t(isTransfer ? "Processing transfer" : "Processing bridge");

971-980: Error toast message not translated.

The error message in handleError is hardcoded in English and should be translated for consistency with the rest of the i18n work.

🔧 Proposed fix

Since handleError is outside the component and doesn't have access to t(), you could either:

  1. Pass t as a parameter:
-function handleError(error: unknown, toast: ReturnType<typeof useToastController>, isTransfer?: boolean) {
+function handleError(
+  error: unknown,
+  toast: ReturnType<typeof useToastController>,
+  t: (key: string) => string,
+  isTransfer?: boolean
+) {
   if (error instanceof UserRejectedRequestError) return;
   if (error instanceof TransactionExecutionError && error.shortMessage === "User rejected the request.") return;
-  toast.show(`${isTransfer ? "Transfer" : "Bridge"} failed. Please try again.`, {
+  toast.show(t(isTransfer ? "Transfer failed. Please try again." : "Bridge failed. Please try again."), {
     native: true,
     duration: 1000,
     burntOptions: { haptic: "error", preset: "error" },
   });
   reportError(error);
 }
  1. Or move the function inside the component to access t directly.
src/components/card/Card.tsx (1)

434-437: Missed translation: title prop not localized.

The title="Latest purchases" prop passed to LatestActivity is not wrapped with t(), while other strings in this component are localized.

Suggested fix
               <LatestActivity
                 activity={purchases}
-                title="Latest purchases"
+                title={t("Latest purchases")}
                 emptyComponent={
🤖 Fix all issues with AI agents
In @src/components/activity/details/OperationDetails.tsx:
- Line 107: The hardcoded date/time format in the JSX (format(item.timestamp,
"yyyy-MM-dd") and the similar time formatting) should be replaced with a
locale-aware formatter; update the rendering to use either Intl.DateTimeFormat
(e.g., new Intl.DateTimeFormat(userLocale, { dateStyle: 'short' }) or timeStyle:
'medium') or date-fns with locale option, passing the app/user locale and
formatting options instead of the fixed strings; locate the uses around
format(item.timestamp, "yyyy-MM-dd") and the time formatting call and replace
them with a call that derives the locale (from i18n or navigator.language) and
formats accordingly so dates/times follow user locale conventions.

In @src/components/activity/details/PurchaseDetails.tsx:
- Line 88: Replace the hardcoded format(...) calls in PurchaseDetails (e.g., the
call showing format(item.timestamp, "yyyy-MM-dd") and the time formatting on the
other format(...) call) with a locale-aware formatter: either use date-fns
format with a locale argument and locale-aware tokens like "P" and "pp" (passing
the user's/ browser locale) or use Intl.DateTimeFormat with the user's locale
and appropriate options; update the imports and any props/context needed to
obtain the current locale and apply it to both date and time renderings so
formatting adapts to user locale conventions.

In @src/components/card/CardDisclaimer.tsx:
- Around line 80-82: The long disclaimer string passed inline to t() in the
CardDisclaimer component should be extracted to a semantic translation key or a
named constant to improve readability and maintainability; replace the inline
call t("*The Exa Card...") with something like t("cardDisclaimer.visaNotice")
(or import a const CARD_VISA_DISCLAIMER and pass that to t) and move the full
text into your i18n translation files (or into the named constant) so the
component uses the short key/constant instead of the long literal.

In @src/components/home/AssetChange.tsx:
- Line 9: In the AssetChange component replace the horizontal View usage with
Tamagui's XStack: import XStack from Tamagui (or add it to the existing tamagui
import) and change the JSX element from View (the one with display="flex"
flexDirection="row") to XStack, removing the display and flexDirection props
while preserving alignItems, justifyContent and gap props; ensure any style/prop
differences are adjusted to XStack-compatible props and run type checks to
update the component import/exports accordingly.

In @src/components/home/AssetList.tsx:
- Around line 29-31: AssetRow currently calls useTranslation() redundantly;
remove the hook from the AssetRow component and change its signature to accept
the translation function as a prop (e.g., add a t prop to the AssetRow prop
type), update any references inside AssetRow to use that passed-in t, and update
the AssetList where AssetRow is rendered (the .map call) to pass the parent’s t
(obtained once via useTranslation() in AssetList) into each AssetRow. Ensure the
AssetRow props typing reflects the new t prop and delete the unused
useTranslation import from the child.

In @src/components/home/card-upgrade/Intro.tsx:
- Around line 28-30: The translation string in Intro.tsx hardcodes the date
"June 21st, 2025" which reduces flexibility; update the call to t(...) in the
component to use an interpolation parameter (e.g., pass a date variable like
upgradeDeadline) instead of embedding the literal, and create/modify the
translation key to include a placeholder (e.g., "{{date}}"); prepare the date
value in the component (format it per locale using Intl.DateTimeFormat or a date
utility) and pass it into t as the parameter so translations and formatting can
be managed centrally.

In @src/components/pay-mode/OverduePayments.tsx:
- Around line 149-155: Replace the manual ternary absolute-value expression used
when formatting the percent in OverduePayments (the expression `discount >= 0 ?
discount : discount * -1`) with Math.abs(discount); update the JSX where
t("Penalties {{percent}}", { percent: ... }) is called to pass
Math.abs(discount) into toLocaleString so the code is simpler and clearer.

In @src/components/pay-mode/PaymentSheet.tsx:
- Line 81: formatDistance is called without a locale, so relative times always
render in English; import date-fns locales (e.g., es and enUS) and get the
user's language from the i18n hook (useTranslation -> i18n.language) then pass
the appropriate locale to formatDistance via its options argument; update the
component where formatDistance is used (the timeDistance computation) to choose
locale based on i18n.language and call formatDistance(..., { locale }) so
Spanish users see Spanish strings.

In @src/components/pay-mode/UpcomingPayments.tsx:
- Around line 196-198: The penalty percentage is hardcoded inside the translated
string in UpcomingPayments; extract it to a constant (e.g., PENALTY_RATE) or
config and pass it into the i18n call via interpolation (call t with the key and
{ rate: PENALTY_RATE_STRING }) and update the translation entry to use the
{rate} placeholder; ensure the value includes the percent sign (or format it
consistently) and replace the inline "0.45%" occurrences in UpcomingPayments
(and any similar components) to use the interpolated value.

In @src/components/roll-debt/RollDebt.tsx:
- Line 14: The Pay component imports useTranslation but several user-facing
strings are still hardcoded; update the Pay component to call t() for each label
and button text listed (replace "Review rollover", "Debt to rollover", "Rollover
interest", "Current debt", "Total after rollover", and "Confirm rollover" with
t('...') calls using appropriate keys) and also wrap the additional occurrence
noted around line 202; ensure you use the existing translation hook instance
(const { t } = useTranslation()) and add or reference corresponding i18n keys in
your translation files so the UI strings are consistently localized.

In @src/components/send-funds/Asset.tsx:
- Around line 71-73: The colon punctuation is placed outside the translation key
in the Text rendering (t("To") followed by ":"), which hinders localization;
update the translation usage in Asset.tsx to include punctuation inside the key
(e.g., change t("To") + ":" to a single key like t("ToColon") or
t("ToWithColon") and update the i18n resource accordingly) so translators can
control spacing and punctuation for the "To:" label rendered by the Text
component.
- Around line 88-91: The snippet in Asset.tsx repeats the translation call
t("New Contact") when building the new contact object; extract that translation
into a local variable (e.g., const newContactLabel = t("New Contact")) before
the conditional and use newContactLabel for the name field in both branches (the
array-spread branch and the single-item branch) to avoid duplicate calls and
improve readability.

In @src/components/send-funds/Receiver.tsx:
- Line 161: The JSX in Receiver.tsx currently hardcodes the interpolation value
for arrival time: t("Arrival time ≈ {{minutes}} min.", { minutes: 1 }); make the
arrival time dynamic by accepting a prop or computing it inside the Receiver
component (e.g., add an arrivalMinutes prop or compute estimateArrivalMinutes
via an estimateArrivalTime/computeArrivalMinutes function) and pass that value
into the i18n call (t(..., { minutes: arrivalMinutes })). Also update Receiver's
props/interface to include arrivalMinutes and provide a sensible default (1)
when undefined so existing behavior is preserved.

In @src/components/send-funds/RecentContacts.tsx:
- Line 25: Move the useTranslation hook call above any derived-value hooks so
hooks are ordered consistently: place the const { t } = useTranslation(); line
before the useMemo(...) invocation(s) in the RecentContacts component (so all
hook calls like useTranslation and useMemo are grouped at the top of the
component body, e.g., before the memoized recentContacts variable or any other
useMemo usages).

In @src/components/shared/InstallmentSelector.tsx:
- Line 108: The JSX in InstallmentSelector currently calls t("{{count}}
installments of", { count: installment }) which splits the phrase; change the
translation call in the InstallmentSelector component to include the amount in
one key, e.g. t("{{count}} installments of {{amount}}", { count: installment,
amount: price }) when hasInstallments is true, leaving the existing t("Pay Now")
branch unchanged; update any surrounding JSX that interpolates price/amount so
the full sentence is produced by this single t(...) call.

In @src/components/shared/ProcessingBalance.tsx:
- Around line 27-33: The currency is being formatted with
toLocaleString(undefined, ...) which uses the device locale and can mismatch the
i18n translation language; update the ProcessingBalance component to use the
current i18n locale (e.g., i18n.language or i18n.resolvedLanguage) when calling
toLocaleString for processingBalance, or switch to the i18n library's
number/currency formatter (e.g., i18n.formatNumber/formatCurrency) so the
currency formatting consistently uses the same locale as the t(...) translation.

In @src/components/shared/ProcessingBalanceBanner.tsx:
- Line 10: The component currently uses an arrow function expression (const
ProcessingBalanceBanner = () => { ... }) but should be declared as a function
declaration; replace the arrow-style component with a function declaration
(function ProcessingBalanceBanner() { ... }) preserving the component body,
props/return and any exports (e.g., export default ProcessingBalanceBanner) so
behavior and usage remain unchanged.

In @src/components/swaps/Swaps.tsx:
- Around line 356-369: The nested ternary assigning buttonLabel is hard to read;
extract that logic into a small helper (e.g., getButtonLabel) and call it from
where buttonLabel is defined. The helper should accept t and the context ({
isSimulating, route, isInsufficientBalance, simulationError, danger, fromToken,
toToken }) and implement the same precedence: if isSimulating && route -> return
isInsufficientBalance ? t("Insufficient balance") : t("Please wait..."); else if
simulationError -> t("Cannot proceed"); else if danger -> t("Enter a lower
amount to swap"); else if fromToken && toToken -> t("Swap {{from}} for {{to}}",
{...}); else -> t("Swap"); Replace the nested ternary that sets
swapActionLabel/buttonLabel with a call to getButtonLabel to improve
readability.

In @src/utils/useAuth.ts:
- Around line 60-68: Replace the inline English translation strings in the
toast.show calls inside useAuth.ts with structured keys (e.g., change
t("Authentication cancelled") to t("auth.cancelled") and t("Your password
manager does not support passkey backups. Please try a different one") to
t("auth.passkeyBackupRequired")), update the corresponding entries in your
i18n/translation files to map those keys to the current English text, and ensure
any tests or components that rely on the old literal strings use the new keys
(locate the toast.show usages in useAuth.ts to apply the changes).
♻️ Duplicate comments (36)
src/components/loans/Review.tsx (1)

173-177: Redundant "Processing" prefix creates awkward text.

The default statusMessage is t("Funding request processing"), but Line 433 prepends t("Processing") + " " when neither error nor success is true, resulting in "Processing Funding request processing".

Consider changing the default to something like t("Funding request") so the combined text reads naturally, or remove the prefix on Line 433.

src/components/home/HomeActions.tsx (1)

96-96: Dynamic translation keys limit static analysis tooling.

This pattern was flagged in a previous review. Using t(title) with a variable prevents static extraction tools from detecting these keys. Ensure "Add funds" and "Send" are manually added to all translation files.

Also applies to: 108-111

src/components/shared/Failure.tsx (1)

58-58: Hardcoded date format produces English output regardless of locale.

The format(date, "MMM dd, yyyy") call outputs English month abbreviations even when the app language is Spanish. Use locale-aware formatting with date-fns/locale or Intl.DateTimeFormat.

src/components/shared/AddressDialog.tsx (1)

69-75: Split translations prevent proper localization.

This issue was previously flagged. Splitting the sentence into t("Add funds using") + {chain.name} + t("only. Sending assets...") prevents translators from reordering words to match target language grammar. Use Trans component with interpolation instead.

src/components/auth/PasskeysAbout.tsx (1)

52-56: Trailing spaces in template literals for visual spacing.

This pattern was previously flagged. Embedding trailing spaces ({${t("Strong credentials.")} }) in JavaScript rather than using CSS margins makes spacing harder for translators to adjust. Use marginRight prop on the Text component instead.

Also applies to: 58-61, 63-70

src/components/home/PortfolioSummary.tsx (1)

83-93: Redundant truthy check inside the translation interpolation.

The outer condition on line 69 ({processingBalance ? () already guarantees processingBalance is truthy when this block executes. The inner ternary processingBalance ? ... : "" is dead code.

♻️ Suggested simplification
          <Text emphasized subHeadline secondary>
            {t("Processing balance {{amount}}", {
-              amount: processingBalance
-                ? processingBalance.toLocaleString(undefined, {
-                    style: "currency",
-                    currency: "USD",
-                    currencyDisplay: "narrowSymbol",
-                  })
-                : "",
+              amount: processingBalance.toLocaleString(undefined, {
+                style: "currency",
+                currency: "USD",
+                currencyDisplay: "narrowSymbol",
+              }),
            })}
          </Text>
src/components/pay-mode/PaymentsActions.tsx (1)

30-30: Remove redundant defaultValue options.

When the translation key matches the default value, defaultValue is unnecessary—react-i18next falls back to the key itself when no translation is found.

♻️ Suggested fix
-            <Text fontSize={15}>{t("Auto-pay", { defaultValue: "Auto-pay" })}</Text>
+            <Text fontSize={15}>{t("Auto-pay")}</Text>
-              {t("Collateral", { defaultValue: "Collateral" })}
+              {t("Collateral")}
-              {t("Manage", { defaultValue: "Manage" })}
+              {t("Manage")}

Also applies to: 42-42, 45-45

src/components/pay-mode/RepayAmountSelector.tsx (1)

173-179: Locale inconsistency between translated text and currency format.

toLocaleString(undefined, ...) uses the device's system locale rather than the app's selected i18n language. This could result in mismatched formatting—e.g., Spanish text with US-formatted currency ($1,234.56) instead of Spanish format (1.234,56 $).

♻️ Suggested approach
- const { t } = useTranslation();
+ const { t, i18n } = useTranslation();
- amount: (Number(balancerBalance) / 1e6).toLocaleString(undefined, {
+ amount: (Number(balancerBalance) / 1e6).toLocaleString(i18n.language, {
src/components/loans/Amount.tsx (1)

92-92: Consider including punctuation in translation keys for better l10n.

The colon after "Available funding" is placed outside the translation: {t("Available funding")}:{" "}. In some languages (e.g., French), different spacing rules apply around colons. Including the colon in the translation key gives translators control over punctuation conventions.

♻️ Suggested fix
-                      {t("Available funding")}:{" "}
+                      {t("Available funding:")}{" "}

Then update the translation resources to include the colon in the key.

src/components/send-funds/Amount.tsx (1)

235-238: Punctuation outside translation keys limits localization flexibility.

The colon is placed outside the t() call, which prevents translators from adjusting punctuation per locale (e.g., French uses a non-breaking space before colons).

Also applies to: 257-259

src/components/loans/LoanSummary.tsx (1)

42-59: timestamp in dependency array causes unnecessary recomputation.

The timestamp variable is computed from Date.now() on each render (line 26), so including it in the dependency array defeats memoization. The apr value will be recalculated on every render.

src/components/getting-started/GettingStarted.tsx (1)

206-206: Consider using i18next's pluralization feature.

Using separate translation keys for "One step left" and "Two steps left" is a common i18n anti-pattern. i18next supports pluralization natively, which handles varying grammatical rules across languages more gracefully.

♻️ Suggested refactor using pluralization
 <Text emphasized subHeadline color="$uiBrandTertiary">
-  {completedSteps > 1 ? t("One step left") : t("Two steps left")}
+  {t("stepsLeft", { count: 3 - completedSteps })}
 </Text>

Then in your translation files, define the pluralized key:

{
  "stepsLeft_one": "One step left",
  "stepsLeft_other": "{{count}} steps left"
}

Also applies to: 222-222

src/components/shared/LatestActivity.tsx (1)

27-29: Dynamic translation key may cause untranslated text.

Using t(title ?? "Latest activity") with a dynamic prop as a translation key is problematic. If callers pass a title prop that doesn't exist in the translation files, it will render untranslated. Translation keys should be static for extraction tools and type safety.

♻️ Suggested fix (translate at call site)
 <Text emphasized headline flex={1}>
-  {t(title ?? "Latest activity")}
+  {title ?? t("Latest activity")}
 </Text>

This way callers pass already-translated strings, and only the fallback uses t() with a static key.

src/components/home/HomeDisclaimer.tsx (1)

14-51: LGTM on the i18n integration.

The Trans component is properly used to enable localization while preserving interactive elements. The component slots (app, protocol, terms) correctly render clickable Text elements with their respective handlers.

One minor suggestion: consider using regular string literals instead of template literals for the static URLs on lines 24 and 35 (e.g., "https://docs.exact.ly/..." instead of `https://docs.exact.ly/...`).

src/components/activity/PendingProposals.tsx (1)

58-75: Translation key pattern concern already noted.

The getProposalLabel function returns raw English strings that are later passed through t() in ProposalItem. This works but makes static extraction of translation keys impossible.

src/components/pay-mode/PaySelector.tsx (3)

75-81: Pluralization pattern already noted.

The inline ternary for singular/plural forms could be replaced with i18n plural suffixes (_one, _other).


117-117: Redundant defaultValue already noted.

When defaultValue matches the key exactly, it's redundant.

Also applies to: 136-136


226-229: Date format locale-awareness already noted.

The hardcoded "MMM dd, yyyy" pattern will display English month abbreviations regardless of locale.

src/components/add-funds/Bridge.tsx (1)

658-659: "Exa Account" label not translated - already noted.

src/components/home/Home.tsx (1)

150-153: Implementation follows the PR's i18n pattern.

The translation wrapping is correctly applied. The suggestion to use descriptive keys instead of full strings was already noted in a previous review.

src/components/home/card-upgrade/Progression.tsx (1)

26-28: Use a static translation key for proper pluralization support.

The translation key "{{count}} step remaining" embeds the interpolation placeholder directly in the key, which doesn't follow i18next best practices for pluralization. Use a static key and define plural forms in your translation files.

The recommended approach:

{t("step_remaining", { count: remainingSteps })}

With corresponding translation entries:

// en.json
"step_remaining_one": "{{count}} step remaining",
"step_remaining_other": "{{count}} steps remaining"

// es.json  
"step_remaining_one": "{{count}} paso restante",
"step_remaining_other": "{{count}} pasos restantes"
src/i18n/es.json (1)

1-10: Missing English translation file will break the app for non-Spanish users.

This issue was already flagged in a previous review. The en.json fallback file must contain all translation keys present in es.json. Without this, i18next will display raw keys (e.g., "Enter amount") as UI text for English-speaking users, resulting in a broken interface.

#!/bin/bash
# Verify if en.json exists and compare key counts with es.json
echo "=== Checking en.json existence and key count ==="
if fd -t f 'en.json' src/i18n; then
  echo "en.json found:"
  cat src/i18n/en.json | head -20
  echo ""
  echo "Key count in en.json:"
  grep -c '".*":' src/i18n/en.json || echo "0"
else
  echo "en.json NOT FOUND in src/i18n/"
fi

echo ""
echo "Key count in es.json:"
grep -c '".*":' src/i18n/es.json
src/components/loans/CreditLine.tsx (1)

56-58: Consider localizing the date format.

The date on line 57 uses a hardcoded English format ("MMM d, yyyy"). For proper localization, consider using locale-aware date formatting with date-fns locales or Intl.DateTimeFormat.

src/components/add-funds/AssetSelectSheet.tsx (1)

67-67: Double translation of label prop.

The label prop may already be translated by callers (e.g., Bridge.tsx passes t("Select asset to send")). Wrapping it again with t(label) attempts to translate an already-translated string, which won't find a matching key in non-English locales.

Either remove t() here and document that callers must translate, or expect callers to pass translation keys (not pre-translated strings).

Suggested fix
 <Text fontSize={15} fontWeight="bold" textAlign="center">
-  {t(label)}
+  {label}
 </Text>

And ensure the default value is translated where this component is used without a label prop.

src/components/shared/Success.tsx (1)

78-78: Date formatting still uses hardcoded English locale.

The interpolation t("Due {{date}}", { date: format(..., "MMM dd, yyyy") }) will produce English month abbreviations regardless of the user's language setting. This was already flagged in a previous review.

src/components/pay-mode/Pay.tsx (2)

503-503: Date format is not locale-aware.

The date format "MMM dd, yyyy" is hardcoded in English. For proper i18n, use a locale-aware format from date-fns/locale or the PPP format token.


546-546: Redundant defaultValue when key equals fallback.

Multiple translation calls use the pattern t("Key", { defaultValue: "Key" }) where the key and defaultValue are identical. In react-i18next, the key itself is returned when not found, making defaultValue redundant. Consider simplifying to t("Key") for cleaner code.

Also applies to: 579-579, 601-601, 617-617, 657-657, 712-712, 745-745, 785-785

src/components/send-funds/QR.tsx (1)

112-114: Consider using interpolation for button label references.

The instruction text references "Continue" and "Back" as literal strings, which may diverge from the actual translated button labels on lines 103 and 125.

{t("Press '{{continue}}' to proceed or '{{back}}' to cancel.", { continue: t("Continue"), back: t("Back") })}
src/components/activity/details/TransactionDetails.tsx (1)

87-95: Date/time formatting uses hardcoded format strings.

The date and time values use fixed format strings ("yyyy-MM-dd", "HH:mm:ss") which won't adapt to user locale preferences. If locale-sensitive formatting is desired, consider using date-fns with locale support or toLocaleDateString/toLocaleTimeString.

This may be intentional for consistent display—if so, disregard.

src/components/card/exa-card/CardContents.tsx (1)

79-83: Consider extracting the duplicated balance label.

The identical t("Available balance") JSX block appears in both the credit (lines 79-83) and debit (lines 100-104) branches. Extract to a shared constant for maintainability.

Also applies to: 100-104

src/components/home/VisaSignatureSheet.tsx (1)

41-45: Misleading toast message: says "deactivated" but context is "upgrade".

The success toast shows "Card deactivated successfully" but the user action is upgrading their card. Consider using "Card upgraded successfully" for better UX alignment with the upgrade flow.

Suggested fix
-      toast.show(t("Card deactivated successfully"), {
+      toast.show(t("Card upgraded successfully"), {
src/components/card/SpendingLimits.tsx (1)

57-61: Inline style object violates coding guidelines.

The style={{ alignSelf: "center" }} on Pressable conflicts with the guideline prohibiting inline styles with React Native's style prop. Wrap with a Tamagui layout component instead.

Suggested fix
-              <Pressable onPress={onClose} style={{ alignSelf: "center" }} hitSlop={20}>
+              <XStack alignSelf="center">
+                <Pressable onPress={onClose} hitSlop={20}>
+                  <Text emphasized footnote color="$interactiveTextBrandDefault">
+                    {t("Close")}
+                  </Text>
+                </Pressable>
+              </XStack>
-                <Text emphasized footnote color="$interactiveTextBrandDefault">
-                  {t("Close")}
-                </Text>
-              </Pressable>

As per coding guidelines, all styling must go through Tamagui's styled system props.

src/components/card/Card.tsx (1)

453-468: Consider using a shorter, descriptive translation key.

Using the full English text as the i18nKey works but is verbose. A shorter key like "card.disclaimer" would be cleaner and decouple the key from the English content.

src/components/activity/ActivityItem.tsx (1)

167-168: Dynamic translation key prevents static extraction.

The expression t(item.type.charAt(0).toUpperCase() + item.type.slice(1)) generates translation keys at runtime. This breaks i18n extraction tooling and risks missing translations for new activity types.

Use an explicit mapping from known item.type values to static translation keys instead.

src/components/add-funds/AddCrypto.tsx (1)

179-180: Fragile whitespace handling with separate JSX node.

The {" "} before the translated string is fragile. Include the leading space in the translation value instead.

♻️ Suggested improvement
-              {" "}
-              {t("Learn more about adding funds.")}
+              {t(" Learn more about adding funds.")}

Or update the translation file to include the leading space in the value for this key.

src/components/pay-mode/PaymentSheet.tsx (1)

185-188: Date format should use locale for proper i18n.

format(dueDate, "MMM dd, yyyy") uses date-fns which defaults to English. Month abbreviations will display in English for Spanish users.

{t("Date")}
</Text>
<Text callout color="$uiNeutralPrimary">
{format(item.timestamp, "yyyy-MM-dd")}
Copy link

@coderabbitai coderabbitai bot Jan 14, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider localizing date and time formats.

The date (yyyy-MM-dd) and time (HH:mm:ss) formats are hardcoded, which is inconsistent with the i18n effort elsewhere in this file. Different locales expect different formats (e.g., MM/dd/yyyy in the US, dd/MM/yyyy in Europe).

You can use date-fns locale support or Intl.DateTimeFormat to respect the user's locale preferences.

♻️ Suggested approach using Intl.DateTimeFormat
         <Text callout color="$uiNeutralPrimary">
-          {format(item.timestamp, "yyyy-MM-dd")}
+          {new Date(item.timestamp).toLocaleDateString()}
         </Text>
         <Text callout color="$uiNeutralPrimary">
-          {format(item.timestamp, "HH:mm:ss")}
+          {new Date(item.timestamp).toLocaleTimeString()}
         </Text>

Also applies to: 115-115

🤖 Prompt for AI Agents
In @src/components/activity/details/OperationDetails.tsx at line 107, The
hardcoded date/time format in the JSX (format(item.timestamp, "yyyy-MM-dd") and
the similar time formatting) should be replaced with a locale-aware formatter;
update the rendering to use either Intl.DateTimeFormat (e.g., new
Intl.DateTimeFormat(userLocale, { dateStyle: 'short' }) or timeStyle: 'medium')
or date-fns with locale option, passing the app/user locale and formatting
options instead of the fixed strings; locate the uses around
format(item.timestamp, "yyyy-MM-dd") and the time formatting call and replace
them with a call that derives the locale (from i18n or navigator.language) and
formats accordingly so dates/times follow user locale conventions.

Copy link
Member Author

Choose a reason for hiding this comment

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

we prefer ISO format here

Copy link

Choose a reason for hiding this comment

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

@dieguezguille, understood! I see you're maintaining ISO format (yyyy-MM-dd and HH:mm:ss) consistently across the app. This approach ensures uniform date/time display regardless of locale, which simplifies the user experience and avoids potential confusion from varying formats. Thank you for the clarification!


🧠 Learnings used
Learnt from: dieguezguille
Repo: exactly/exa PR: 635
File: src/components/activity/details/TransactionDetails.tsx:87-87
Timestamp: 2026-01-14T13:34:45.742Z
Learning: In the Exa mobile app (TypeScript/React Native), date and time formatting deliberately uses ISO format (yyyy-MM-dd for dates, HH:mm:ss for times) for consistency across all locales, rather than locale-aware formatting.

export default function AssetChange() {
const { t } = useTranslation();
return (
<View display="flex" flexDirection="row" alignItems="center" justifyContent="center" gap={5}>
Copy link

@coderabbitai coderabbitai bot Jan 14, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Prefer XStack over View with flexDirection="row".

As per coding guidelines, use Tamagui's XStack for horizontal layout instead of View with flexDirection="row".

♻️ Suggested refactor
-import { View, Text } from "tamagui";
+import { XStack, Text } from "tamagui";
-    <View display="flex" flexDirection="row" alignItems="center" justifyContent="center" gap={5}>
+    <XStack alignItems="center" justifyContent="center" gap={5}>
       <Minus size={20} color="$uiNeutralSecondary" fontWeight="bold" />
       ...
-    </View>
+    </XStack>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<View display="flex" flexDirection="row" alignItems="center" justifyContent="center" gap={5}>
import { XStack, Text } from "tamagui";
<XStack alignItems="center" justifyContent="center" gap={5}>
<Minus size={20} color="$uiNeutralSecondary" fontWeight="bold" />
...
</XStack>
🤖 Prompt for AI Agents
In @src/components/home/AssetChange.tsx at line 9, In the AssetChange component
replace the horizontal View usage with Tamagui's XStack: import XStack from
Tamagui (or add it to the existing tamagui import) and change the JSX element
from View (the one with display="flex" flexDirection="row") to XStack, removing
the display and flexDirection props while preserving alignItems, justifyContent
and gap props; ensure any style/prop differences are adjusted to
XStack-compatible props and run type checks to update the component
import/exports accordingly.

Copy link
Member Author

Choose a reason for hiding this comment

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

this component was not used or referenced anywhere. will remove

Copy link

Choose a reason for hiding this comment

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

@dieguezguille, thanks for catching that! Removing unused code is definitely the right approach. Good cleanup! 👍


🧠 Learnings used
Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/app.mdc:0-0
Timestamp: 2025-12-23T19:56:31.043Z
Learning: Applies to src/**/*.{ts,tsx} : Use Tamagui's `XStack` and `YStack` for layout instead of React Native's `View`.

Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/app.mdc:0-0
Timestamp: 2025-12-23T19:56:31.043Z
Learning: Applies to src/**/*.{ts,tsx} : Prefer shorthand props for Tamagui styling for brevity and consistency. Examples: `p="$4"`, `m="$2"`, `f={1}`. Avoid: `paddingLeft="$4"`, `marginRight="$2"`.

Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to app/**/*.{ts,tsx} : Mobile app styling: use tamagui tokens only, no inline styles, no manual dark mode logic

Learnt from: CR
Repo: exactly/exa PR: 0
File: .cursor/rules/app.mdc:0-0
Timestamp: 2025-12-23T19:56:31.043Z
Learning: Applies to src/**/*.{ts,tsx} : Do not use inline styles with the React Native `style` prop using object literals. All styling must go through Tamagui's styled system props.

Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-23T19:58:16.574Z
Learning: Applies to app/**/*.{ts,tsx} : React Native mobile architecture: separate smart components (screens, perform fetching) from dumb components (ui only)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
src/components/send-funds/ReviewSheet.tsx (1)

83-88: Consider defensive handling for invalid usdValue.

If usdValue is an empty string or non-numeric, Number(usdValue) returns NaN, which would display "NaN" to users.

♻️ Suggested defensive fallback
-                      {Number(usdValue).toLocaleString(language, {
+                      {(Number(usdValue) || 0).toLocaleString(language, {
                         style: "currency",
                         currency: "USD",
                         minimumFractionDigits: 2,
                         maximumFractionDigits: 2,
                       })}
src/components/shared/AssetSelector.tsx (1)

44-46: Missing translation for user-facing string.

The "No available assets." text should be translated using t() for consistency with the i18n effort across the codebase.

🌐 Proposed fix
+  const {
+    t,
+    i18n: { language },
+  } = useTranslation();
-  const {
-    i18n: { language },
-  } = useTranslation();

And update line 45:

-        No available assets.
+        {t("No available assets.")}
src/components/swaps/TokenInput.tsx (1)

177-183: Inline style object violates coding guidelines.

The style prop uses an object literal, which contradicts the guideline: "Do not use inline styles with the React Native style prop using object literals. All styling must go through Tamagui's styled system props."

Consider using Tamagui's styled system props or creating a styled component.

♻️ Suggested approach
                      <Input
                        value={value}
                        onChangeText={handleAmountChange}
                        onFocus={onFocus}
                        disabled={disabled}
                        cursor={disabled ? undefined : "pointer"}
                        placeholder={token ? formatUnits(amount, token.decimals) : String(amount)}
                        color={
                          isDanger ? "$uiErrorSecondary" : isActive ? "$uiNeutralPrimary" : "$uiNeutralPlaceholder"
                        }
-                       style={{
-                         fontFamily: "BDOGrotesk-Regular",
-                         fontSize: 28,
-                         fontWeight: "bold",
-                         lineHeight: 34,
-                         letterSpacing: -0.2,
-                       }}
+                       fontFamily="$body"
+                       fontSize={28}
+                       fontWeight="bold"
+                       lineHeight={34}
+                       letterSpacing={-0.2}
                        textAlign="left"

Note: Adjust fontFamily to the appropriate Tamagui token if $body doesn't map to "BDOGrotesk-Regular".

src/components/loans/Review.tsx (1)

283-286: Inconsistent locale: use language instead of undefined.

This toLocaleString call uses undefined as the locale, while all other formatting calls in this file (Lines 238, 262, 311, 448) correctly use language. This inconsistency will cause this specific value to use the device's default locale rather than the user's selected language.

🛠️ Suggested fix
                      <Text title3 color="$uiNeutralPrimary">
                        {(Number(singleInstallment ? totalAmount : installmentsAmount) / 1e6).toLocaleString(
-                          undefined,
+                          language,
                          { minimumFractionDigits: 2, maximumFractionDigits: 2 },
                        )}
                      </Text>
src/components/swaps/Pending.tsx (1)

84-84: Consider locale-aware formatting for crypto amounts.

USD amounts use toLocaleString(language, ...) but crypto amounts use toFixed(8). For consistency, consider using locale-aware formatting here as well, unless the fixed decimal notation is intentional for precision display.

♻️ Optional: Locale-aware crypto amount formatting
-                      {Number(formatUnits(fromAmount, fromToken.decimals)).toFixed(8)}
+                      {Number(formatUnits(fromAmount, fromToken.decimals)).toLocaleString(language, { maximumFractionDigits: 8, minimumFractionDigits: 0 })}

Apply the same pattern to line 98 for toAmount.

src/components/send-funds/Receiver.tsx (1)

89-89: Avoid inline styles with the style prop.

Per coding guidelines, all styling must go through Tamagui's styled system props. The style prop with an object literal should be replaced with Tamagui props.

🔧 Suggested fix
                        style={{ borderColor: "$uiNeutralTertiary", borderWidth: 1, fontSize: 15 }}
+                       borderWidth={1}
+                       fontSize={15}

Note: borderColor is already set via the borderColor prop on line 84, so it can be removed from the inline style.

src/components/activity/details/PaymentDetails.tsx (1)

56-60: Simplify redundant condition in Number() call.

The expression Number(item.mode > 0 && (item as CreditActivity).borrow.rate) includes a redundant check since this code is already inside the {item.mode > 0 && ...} block (line 50). The boolean AND can never be false here.

♻️ Suggested simplification
-              {Number(item.mode > 0 && (item as CreditActivity).borrow.rate).toLocaleString(language, {
+              {Number((item as CreditActivity).borrow.rate).toLocaleString(language, {
src/components/activity/details/OperationDetails.tsx (1)

96-100: Simplify redundant condition in Number() call.

Same issue as in PaymentDetails.tsx: the item.mode > 0 check is redundant since this block is already guarded by {item.mode > 0 && ...} (line 90).

♻️ Suggested simplification
-              {Number(item.mode > 0 && (item as CreditActivity).borrow.rate).toLocaleString(language, {
+              {Number((item as CreditActivity).borrow.rate).toLocaleString(language, {
src/components/send-funds/Amount.tsx (1)

176-201: Untranslated error messages in fallback UI.

The error messages and button text in this fallback section are not wrapped with t(...), creating inconsistency with the rest of the localized UI.

Proposed fix
           <Text body primary color="$uiNeutralPrimary">
-            {invalidReceiver ? "Invalid receiver address" : "Invalid asset address"}
+            {invalidReceiver ? t("Invalid receiver address") : t("Invalid asset address")}
           </Text>
           <Button
             dangerSecondary
             alignSelf="center"
             onPress={() => {
               if (router.canGoBack()) {
                 router.back();
               } else {
                 router.replace("/send-funds/asset");
               }
             }}
           >
-            <Button.Text>Go back</Button.Text>
+            <Button.Text>{t("Go back")}</Button.Text>
             <Button.Icon>
               <ArrowLeft size={24} color="$uiNeutralPrimary" />
             </Button.Icon>
           </Button>
src/components/add-funds/Bridge.tsx (2)

974-982: handleError toast messages are not translated.

The error toast message in handleError is hardcoded in English. For consistency with the i18n effort, consider passing the t function or pre-translated messages.

♻️ Suggested fix
-function handleError(error: unknown, toast: ReturnType<typeof useToastController>, isTransfer?: boolean) {
+function handleError(
+  error: unknown,
+  toast: ReturnType<typeof useToastController>,
+  t: ReturnType<typeof useTranslation>["t"],
+  isTransfer?: boolean,
+) {
   if (error instanceof UserRejectedRequestError) return;
   if (error instanceof TransactionExecutionError && error.shortMessage === "User rejected the request.") return;
-  toast.show(`${isTransfer ? "Transfer" : "Bridge"} failed. Please try again.`, {
+  toast.show(t(isTransfer ? "Transfer failed. Please try again." : "Bridge failed. Please try again."), {
     native: true,
     duration: 1000,
     burntOptions: { haptic: "error", preset: "error" },
   });
   reportError(error);
 }

Then update call sites to pass t.


504-508: Status labels in processing view are not translated.

The label variable ("Transfer" or "Bridge") is used directly in template strings without translation, resulting in English-only status messages like "Transfer failed" or "Processing bridge".

♻️ Suggested fix

Consider using translated status messages:

-        const label = isTransfer ? "Transfer" : "Bridge";
+        const translatedLabel = isTransfer ? t("Transfer") : t("Bridge");
         ...
         <Text secondary body>
           {isError
-            ? `${label} failed`
+            ? t("{{action}} failed", { action: translatedLabel })
             : isSuccess
-              ? `${label} transaction submitted`
+              ? t("{{action}} transaction submitted", { action: translatedLabel })
-              : `Processing ${label.toLowerCase()}`}
+              : t("Processing {{action}}", { action: translatedLabel.toLowerCase() })}
         </Text>
🤖 Fix all issues with AI agents
In @.changeset/better-radios-joke.md:
- Line 5: The changeset message currently says "🐛 fix card 404 error handling"
which doesn't match the PR titled "✨ add i18n"; update the changeset summary and
body to accurately describe the i18n work (e.g., adding Spanish translations,
locale detection, and localized strings across components) and remove the
unrelated bug-fix wording so release notes reflect the actual changes.

In `@src/components/card/CardDetails.tsx`:
- Around line 145-146: The label in CardDetails.tsx currently renders t("CVV")
followed by an external non-breaking space ("\u00A0"); move that NBSP into the
translation so translators control spacing by either updating the key value to
include the NBSP (e.g., set the translation value to "CVV\u00A0") or create a
dedicated translation key that contains the trailing NBSP and call t for that
key; update usages of t("CVV") in this component to reference the new/updated
translation key and adjust locale files accordingly so spacing is defined inside
the translation strings.

In `@src/components/home/AssetChange.tsx`:
- Around line 17-19: Add the missing "7D" translation key to the English locale
so t("7D") in AssetChange.tsx resolves; open src/i18n/en.json and add the entry
"7D": "7D" (matching the existing Spanish key in src/i18n/es.json) to the
top-level translations object, save, and run a quick build or i18n lint to
verify no missing-key errors remain.

In `@src/components/home/card-upgrade/ActivateCard.tsx`:
- Around line 139-141: The long Visa/Exa disclaimer string is duplicated between
ActivateCard.tsx and CardDisclaimer.tsx; replace the inline text in both places
with a semantic i18n key (e.g., t("card.visaDisclaimer")) used by the t(...)
calls in those components (ActivateCard and CardDisclaimer) and add that
key/value to the locale resource files, preserving the exact original copy of
the disclaimer as the value so both components render identical text.

In `@src/components/home/GettingStarted.tsx`:
- Around line 72-73: The current line computing activeStepTitle uses
t(currentStep.title) which couples display titles to translation keys and can
break silently; update the step data or rendering to use an explicit translation
key (e.g., add a translationKey property on the step objects or create a mapping
from step id/title to i18n keys) and change the lookup to
t(currentStep.translationKey || fallbackKey) with a safe fallback (like a plain
currentStep.title) to avoid silent failures; update usages of activeStepTitle
and any step definitions (the currentStep object and where steps are declared)
to include or reference the explicit translation keys.

In `@src/components/loans/LoanSummary.tsx`:
- Around line 45-62: The useMemo that computes apr references the locale
variable language in toLocaleString but language is missing from the dependency
array; update the dependency list for the apr useMemo (the useMemo that defines
apr) to include language so the memo recomputes when the user's locale changes
(i.e., add language alongside borrow, installments, isBorrow, loan.amount,
timestamp).

In `@src/components/pay-mode/PaymentSheet.tsx`:
- Line 63: Extract the repeated locale mapping into a shared utility (e.g.,
getDateFnsLocale) and replace the inline ternary in PaymentSheet (the const
dateFnsLocale assignment) with a call to that utility; implement
getDateFnsLocale to import date-fns locales (enUS, es, etc.), map language keys
to locale objects, return a default (enUS) for unknown languages, and update
PaymentSheet.tsx to import and call getDateFnsLocale(language).

In `@src/components/pay-mode/PaySelector.tsx`:
- Around line 275-288: Extract the nested ternary APR calculation into a named
helper like calculateApr(installment, installments, borrowPreview,
calculationAssets, timestamp) that returns a number; implement the same control
flow (return 0 if installment === 0, return
Number(installments.effectiveRate)/1e18 when installment > 1 && installments,
compute the borrowPreview formula using WAD and maturity when borrowPreview
exists, otherwise return 0), then replace the inline apr expression with a call
to calculateApr so the PaySelector.tsx component uses apr =
calculateApr(installment, installments, borrowPreview, calculationAssets,
timestamp).

In `@src/components/send-funds/Amount.tsx`:
- Around line 289-297: The first validator in Amount.tsx uses t("Amount cannot
be {{amount}}", { amount: 0 }) which constantly interpolates 0 and is unclear
for translators; change the translation key to a direct message like t("Amount
cannot be zero") in the check((value) => value !== 0n, ...) call and update your
i18n resource entries accordingly (remove the interpolation key and add a
"Amount cannot be zero" entry) so translators see a clear, static string; leave
the second check((value) => value <= available, ...) as-is.

In `@src/components/send-funds/ReviewSheet.tsx`:
- Around line 62-68: The missing translation key "Simulation failed" is
referenced in ReviewSheet (ReviewSheet.tsx) but absent from the locales; add the
key to both locale JSONs by adding "Simulation failed": "Simulation failed" to
en.json and "Simulation failed": "Simulación fallida" to es.json so the
t("Simulation failed") call resolves correctly; update the JSON files in
src/i18n (or your project's locales) and run a quick i18n lint/validation to
ensure no trailing commas or syntax errors.

In `@src/components/swaps/SwapDetails.tsx`:
- Around line 54-62: Replace the hardcoded "0.025%" and the current slippage
rendering with locale-aware formatting using toLocaleString; determine whether
slippage is a decimal (e.g. 0.00025) or a percentage value and convert
accordingly (e.g. multiply by 100 for percent or pass style: "percent" to
toLocaleString after converting to a fraction), then render like: formatted =
value.toLocaleString(language, { style: "percent", minimumFractionDigits: 2,
maximumFractionDigits: 3 }); update the JSX that references the literal "0.025%"
and the Text that outputs (Number(slippage) * 100) / 1000 to use this formatted
string and reuse the existing language variable/context used elsewhere in the
PR.

In `@src/i18n/es.json`:
- Around line 462-464: Remove the redundant unsuffixed i18n key "{{count}} step
remaining" and keep only the pluralized keys "{{count}} step remaining_one" and
"{{count}} step remaining_other" so i18next can resolve plurals correctly;
update the JSON by deleting the line with the unsuffixed key and ensure the
remaining "_one" and "_other" entries ("{{count}} step remaining_one" and
"{{count}} step remaining_other") have the correct Spanish translations.
♻️ Duplicate comments (24)
src/components/home/AssetChange.tsx (1)

9-9: Prefer XStack over View with flexDirection="row".

As per coding guidelines, use Tamagui's XStack for horizontal layout instead of View with flexDirection="row".

♻️ Suggested refactor
-import { View, Text } from "tamagui";
+import { XStack, Text } from "tamagui";
-    <View display="flex" flexDirection="row" alignItems="center" justifyContent="center" gap={5}>
+    <XStack alignItems="center" justifyContent="center" gap={5}>
       ...
-    </View>
+    </XStack>
src/components/home/card-upgrade/Progression.tsx (1)

26-28: Pluralization pattern already discussed.

The use of t("{{count}} step remaining", { count: remainingSteps }) for pluralization has been debated in previous review comments. The team has chosen this pragmatic approach. Ensure the translation files include the appropriate plural forms (e.g., "{{count}} step remaining_one" and "{{count}} step remaining_other" in the JSON resources) for correct pluralization in different languages.

src/components/shared/CopyAddressSheet.tsx (1)

22-22: LGTM! All user-facing strings are properly translated.

The i18n implementation correctly wraps all static UI text with t(). The use of natural-language keys is consistent with the pattern established across this PR.

Also applies to: 39-39, 43-43, 58-58, 61-61, 97-97

src/components/auth/PasskeysAbout.tsx (1)

52-56: Trailing spaces in template literals reduce localization flexibility.

The pattern {${t("...")} } embeds visual spacing in JavaScript. This was flagged previously. Consider using margin props on the Text component or including spacing in translation keys for better translator control.

Also applies to: 58-61, 63-69

src/components/card/exa-card/CardContents.tsx (1)

82-86: Duplicated label JSX should be extracted.

The "Available balance" label JSX is identical in both credit and debit branches. This was previously flagged. Extract to a shared variable for maintainability.

Also applies to: 103-107

src/components/add-funds/AssetSelectSheet.tsx (1)

70-70: Double translation issue persists.

If callers pass an already-translated string (e.g., t("Select asset to send")), then t(label) will attempt to translate an already-translated string. Either:

  1. Remove t() here and require callers to pass translation keys that are translated at this location, or
  2. Remove t() from caller sites and pass raw translation keys

The default value "Select asset" suggests this component expects translation keys, so option 1 is consistent—but callers must then pass untranslated keys.

src/components/home/HomeDisclaimer.tsx (1)

20-20: Use plain strings instead of template literals for static URLs.

Template literals with no interpolation add unnecessary complexity.

♻️ Suggested fix
-                  openBrowser(`https://docs.exact.ly/exa-app/how-the-exa-app-works`).catch(reportError);
+                  openBrowser("https://docs.exact.ly/exa-app/how-the-exa-app-works").catch(reportError);
...
-                  openBrowser(`https://exact.ly/`).catch(reportError);
+                  openBrowser("https://exact.ly/").catch(reportError);

Also applies to: 27-27

src/components/getting-started/GettingStarted.tsx (1)

220-223: Consider using i18next's pluralization feature.

This suggestion was already made in a previous review. Using separate translation keys for "One step left" and "Two steps left" could be improved with i18next's native pluralization.

src/components/pay-mode/PaymentSheet.tsx (2)

86-91: formatDistance now correctly uses locale.

The previous concern has been addressed - locale is now passed to formatDistance, ensuring relative time strings render in the user's language.


192-199: Date formatting now uses locale.

Using toLocaleDateString(language, ...) resolves the previous concern about month names appearing in English for non-English users.

src/components/activity/PendingProposals.tsx (1)

58-75: LGTM! Translation at source implemented correctly.

The previous review suggestion to translate at the source has been implemented - getProposalLabel now accepts t and returns translated strings directly. This is cleaner and allows static analysis tools to extract translation keys.

src/components/send-funds/Receiver.tsx (1)

161-161: Hardcoded arrival time value.

The minutes interpolation is hardcoded to 1. If the arrival time could vary, consider making this dynamic.

src/components/shared/InstallmentSelector.tsx (1)

111-111: Split translation phrase breaks i18n best practices.

The translation key "{{count}} installments of" ends with "of", implying the amount follows in separate rendering. This splits a grammatical phrase across components, which breaks word order in languages where the structure differs (e.g., amount before installment count).

src/utils/useAuth.ts (1)

60-68: Consider structured translation keys for maintainability.

Using full English sentences as translation keys works but can be fragile if the source text changes. Structured keys like t("auth.cancelled") are more maintainable.

src/components/shared/TransactionDetails.tsx (1)

84-96: Date and time formatting uses ISO format intentionally.

The hardcoded "yyyy-MM-dd" and "HH:mm:ss" patterns are kept for consistency across all locales, which aligns with the project's deliberate design choice. Based on learnings, this is the expected behavior.

src/components/activity/details/PurchaseDetails.tsx (1)

86-101: Date and time formatting uses ISO format intentionally.

The hardcoded "yyyy-MM-dd" and "HH:mm:ss" patterns are the project's deliberate choice for consistency across all locales. Based on learnings, this is expected behavior.

src/components/card/CardDisclaimer.tsx (1)

80-82: Consider using a semantic key for the long disclaimer.

As noted in a previous review, this long string could use a shorter semantic key (e.g., t("card.visaDisclaimer")) with the full text in translation files for better readability.

src/components/card/SpendingLimits.tsx (1)

57-61: Inline style object violates coding guidelines.

The style={{ alignSelf: "center" }} on Pressable conflicts with the guideline prohibiting inline styles with React Native's style prop.

src/components/swaps/Swaps.tsx (1)

356-364: Nested ternary for buttonLabel is functional but complex.

This was flagged in a previous review as a readability concern. The implementation is correct and works, but consider extracting to a helper function for maintainability if this logic grows.

src/components/pay-mode/PaySelector.tsx (2)

171-171: Redundant defaultValue when identical to key.

The defaultValue option is redundant when it matches the key pattern. react-i18next uses the key as fallback by default. Consider simplifying:

-{t("Instant pay label", { asset: "USDC", defaultValue: "INSTANT PAY ({{asset}})" })}
+{t("INSTANT PAY ({{asset}})", { asset: "USDC" })}

Or define a proper key in translation files without the inline defaultValue.


196-196: Same redundant defaultValue pattern.

-{t("Installment plans label", { defaultValue: "INSTALLMENT PLANS" })}
+{t("INSTALLMENT PLANS")}
src/components/add-funds/Bridge.tsx (1)

943-943: Double translation issue with AssetSelectSheet label prop.

The AssetSelectSheet component internally translates its label prop with t(label). Passing already-translated strings like t("Select asset to send") causes double translation lookup, which may work by accident but is incorrect.

🐛 Proposed fix

Pass the untranslated key instead:

         <AssetSelectSheet
-          label={t("Select asset to send")}
+          label="Select asset to send"
           open={assetSheetOpen}
         <AssetSelectSheet
           hideBalances
-          label={t("Select asset to receive")}
+          label="Select asset to receive"
           open={destinationModalOpen}

Also applies to: 957-957

src/components/card/Card.tsx (1)

456-471: Ensure the Trans i18nKey exists in en.json to prevent rendering issues.

Using the full English text as i18nKey works but requires the key to be present in en.json. If missing, the <link> component placeholders may render as literal text instead of clickable links for English users.

Verify the key exists in the English translation file:

#!/bin/bash
# Check if the Trans i18nKey exists in en.json
fd -t f 'en.json' --exec grep -l "The Exa Card is issued by Third National Bank" {} \;
src/components/add-funds/AddCrypto.tsx (1)

179-180: Consider including the leading space in the translation string.

Having {" "} as a separate JSX node before the translated text is fragile. Including the space in the translation value would be cleaner.

♻️ Suggested improvement
-              {" "}
-              {t("Learn more about adding funds.")}
+              {t(" Learn more about adding funds.")}

Or update the translation file to include the leading space in the value.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 21

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (27)
src/components/swaps/SelectorModal.tsx (1)

92-92: Title default value prevents translation from being used.

The default value title = "Select token" on line 92 means the || t("Select Token") fallback on line 139 will never trigger (unless the caller explicitly passes an empty string). If the intent is to use translations by default, remove the default value:

🐛 Proposed fix
   title = "Select token",
-  title = "Select token",
+  title,

And on line 139, use nullish coalescing for the fallback:

-              {title || t("Select Token")}
+              {title ?? t("Select Token")}

Also applies to: 139-139

src/components/pay-mode/PaySelector.tsx (1)

178-207: Consider centralizing number/percent formatting to reduce duplication.
There are several repeated toLocaleString(language, { ... }) blocks; a small formatAmount(language, value) / formatPercent(language, value) helper would make future changes safer and keep formatting consistent.

Also applies to: 339-340, 349-355, 385-386

src/components/swaps/Success.tsx (2)

70-72: Add accessibility props for icon-only Pressables (now that t is available).
The close icon button should expose an accessible label/role.

Proposed change
-                <Pressable onPress={onClose}>
+                <Pressable onPress={onClose} accessibilityRole="button" accessibilityLabel={t("common.close", { defaultValue: "Close" })}>
                   <X size={24} color="$uiNeutralPrimary" />
                 </Pressable>

Also applies to: 129-139


86-90: Verify Intl currency formatting on target runtimes (Hermes/iOS/Android).
toLocaleString(language, { style: "currency", currencyDisplay: "narrowSymbol" }) is correct conceptually, but Intl/currencyDisplay support varies by RN/Expo/Hermes versions—please validate output (symbol, grouping, decimals) on devices.

Also applies to: 100-104

src/components/activity/details/TransactionDetails.tsx (1)

56-77: Add explicit guards inside onPress handlers to ensure type safety.
Callbacks access item.receiver and item.transactionHash without guards, relying on conditional rendering for type narrowing. Add guards at the start of each handler to avoid relying on closure-captured values when query state changes.

proposed diff
         {item?.type === "sent" && (
           <XStack justifyContent="space-between">
             <Text emphasized footnote color="$uiNeutralSecondary">
               {t("To")}
             </Text>
             <XStack
               alignItems="center"
               gap="$s3"
               onPress={() => {
+                if (!item || item.type !== "sent") return;
                 setStringAsync(item.receiver).catch(reportError);
                 toast.show(t("Address copied!"), {
                   native: true,
                   duration: 1000,
                   burntOptions: { haptic: "success" },
                 });
               }}
             >

         {item?.type !== "panda" && item?.transactionHash && (
           <XStack justifyContent="space-between">
             <Text emphasized footnote color="$uiNeutralSecondary">
               {t("Transaction hash")}
             </Text>
             <XStack alignItems="center" gap="$s3">
               <Text
                 textDecorationLine="underline"
                 callout
                 color="$uiNeutralPrimary"
                 cursor="pointer"
                 onPress={() => {
+                  if (!item?.transactionHash) return;
                   const explorerUrl = chain.blockExplorers?.default.url;
                   if (!explorerUrl) return;
                   openBrowser(`${explorerUrl}/tx/${item.transactionHash}`).catch(reportError);
                 }}
               >
               <XStack
                 cursor="pointer"
                 onPress={() => {
+                  if (!item?.transactionHash) return;
                   const explorerUrl = chain.blockExplorers?.default.url;
                   if (!explorerUrl) return;
                   setStringAsync(`${explorerUrl}/tx/${item.transactionHash}`).catch(reportError);
                   toast.show(t("Link copied!"), { native: true, duration: 1000, burntOptions: { haptic: "success" } });
                 }}
               >
src/components/settings/Settings.tsx (1)

24-47: Localize all a11y labels to support the selected language.

Both aria-label="Back" (line 33) and accessibilityLabel="Finished" (line 106) should use the translation function. The e2e-only "Submit coverage" text (line 101) can remain untranslated since it's scoped behind an environment check, but for consistency consider translating it as well.

proposed diffs
             <Pressable
-              aria-label="Back"
+              aria-label={t("Back")}
               onPress={() => {
                 if (router.canGoBack()) {
                   router.back();
                 } else {
                   router.replace("/(main)/(home)");
                 }
               }}
             >
                       {(coverageSuccess || coverageError) && (
                         <Check
-                          accessibilityLabel="Finished"
+                          accessibilityLabel={t("Finished")}
                           color={coverageSuccess ? "$backgroundBrand" : "$interactiveBaseErrorDefault"}
                         />
                       )}
src/components/activity/details/SentActivity.tsx (1)

16-35: Use a single translation key with interpolation to keep the full sentence together for translators.

Some languages may need different word order or prepositions (e.g., receiver before verb, or different articles). Replace the split across components with:

<Text secondary body>
-  {t("Sent to")}
-  <Text emphasized primary body>
-    &nbsp;
-    {shortenHex(item.receiver)}
-  </Text>
+  {t("Sent to {{receiver}}", { receiver: shortenHex(item.receiver) })}
</Text>

Then add the key to both translation files:

  • en.json: "Sent to {{receiver}}": "Sent to {{receiver}}"
  • es.json: "Sent to {{receiver}}": "Enviado a {{receiver}}"

If styling the receiver differently is essential, use the Trans component with component slotting instead (requires import { Trans } from "react-i18next"), but the interpolation approach above maintains consistency with your codebase's existing i18n patterns.

src/components/shared/TransactionDetails.tsx (1)

46-58: Add explicit hash guards inside press handlers (don’t rely on conditional-render narrowing).
This follows the project guidance to narrow inside callbacks (even if hash && (...) currently encloses the JSX). Based on learnings, do explicit guards.

Proposed change
             onPress={() => {
-              setStringAsync(hash)
+              if (!hash) return;
+              setStringAsync(hash)
                 .then(() => Alert.alert(t("Copied"), t("The transaction hash has been copied to the clipboard.")))
                 .catch((error: unknown) => {
                   reportError(error);
                   Alert.alert(t("Error"), t("Failed to copy the transaction hash to the clipboard."));
                 });
             }}
@@
               onPress={(event) => {
                 event.stopPropagation();
+                if (!hash) return;
                 const explorerUrl = chain.blockExplorers?.default.url;
                 if (!explorerUrl) return;
                 openBrowser(`${explorerUrl}/tx/${hash}`).catch(reportError);
               }}

Also applies to: 68-73

src/components/send-funds/Receiver.tsx (1)

141-158: Prefer Trans for “paragraph + inline link” to avoid {" "} and hard-coded link placement.
This gives translators full control over spacing and link position.

Possible refactor
+import { Trans, useTranslation } from "react-i18next";
-import { useTranslation } from "react-i18next";
@@
-              <Text color="$uiNeutralPlaceholder" fontSize={13} lineHeight={16} textAlign="justify">
-                {t(
-                  "Make sure that the receiving address is compatible with {{chain}} network. Sending assets on other networks may result in irreversible loss of funds.",
-                  { chain: chain.name },
-                )}
-                <Text
-                  color="$uiBrandSecondary"
-                  fontSize={13}
-                  lineHeight={16}
-                  fontWeight="bold"
-                  cursor="pointer"
-                  onPress={() => {
-                    presentArticle("9056481").catch(reportError);
-                  }}
-                >
-                  {" "}
-                  {t("Learn more about sending funds.")}
-                </Text>
-              </Text>
+              <Text color="$uiNeutralPlaceholder" fontSize={13} lineHeight={16} textAlign="justify">
+                <Trans
+                  i18nKey="receiverCompatibilityHelp"
+                  values={{ chain: chain.name }}
+                  components={{
+                    link: (
+                      <Text
+                        color="$uiBrandSecondary"
+                        fontSize={13}
+                        lineHeight={16}
+                        fontWeight="bold"
+                        cursor="pointer"
+                        onPress={() => {
+                          presentArticle("9056481").catch(reportError);
+                        }}
+                      />
+                    ),
+                  }}
+                />
+              </Text>
src/components/send-funds/Asset.tsx (1)

82-90: Store contact names as empty/sentinel values, translating at render time.

The ["contacts", "saved"] cache is purely client-side and ephemeral (never sent to backend, not persisted). However, storing localized default labels (t("New Contact")) in query cache state violates separation of concerns—UI text should be rendered, not stored. Replace with an empty string or null for the name and translate when rendering.

Also note: The query type on line 26 omits the name field, but setQueryData includes it. Align the type definition.

src/components/home/PortfolioSummary.tsx (1)

66-70: Use formatUnits() from viem for consistency instead of Number(usdBalance) / 1e18.

The codebase already establishes formatUnits() as the standard pattern for converting bigint wei values (used across Bridge, Swaps, AmountSelector, and other components). This is safer and more readable than manual conversion. While precision loss from Number() conversion is technically possible, it's impractical for USD balances—you'd need amounts exceeding Number.MAX_SAFE_INTEGER (9e15) to cause issues. The real concern is standardization: consolidate on formatUnits(usdBalance, 18).toLocaleString(language, { ... }) to align with the established pattern throughout the codebase.

src/components/shared/AssetSelector.tsx (1)

35-48: Add missing localization and fix invalid ARIA attribute.

  • Line 45: "No available assets." needs localization. The component imports useTranslation but only destructures i18n.language; add const { t } = useTranslation() and wrap the string with t("No available assets.").
  • Line 89: aria-describedby="tap to select" is invalid ARIA—this attribute should reference an element ID, not a description string. Remove it since aria-label on line 88 already provides sufficient context.

Also applies to: 87-90

src/components/shared/InstallmentSelector.tsx (1)

66-69: Add missing i18next pluralization keys for "{{count}} installments of".

The code at line 111 calls t("{{count}} installments of", { count: installment }), but the translation file lacks the required _one and _other plural variants for this key. When installment === 1, i18next cannot properly pluralize and will fallback instead of displaying "1 installment of". Either add the missing translation keys to src/i18n/en.json and src/i18n/es.json, or refactor to use the existing "You repay {{count}} installments of" keys or handle the singular case explicitly with the isBorrow variable already defined at line 67.

src/components/activity/details/PaymentDetails.tsx (1)

56-60: Simplify the redundant condition.

Since this code is already inside a {item.mode > 0 && ...} block (line 50), the inner item.mode > 0 && check is redundant.

♻️ Suggested simplification
-              {Number(item.mode > 0 && (item as CreditActivity).borrow.rate).toLocaleString(language, {
+              {Number((item as CreditActivity).borrow.rate).toLocaleString(language, {
src/components/home/Portfolio.tsx (1)

93-98: Avoid Number(usdBalance) for currency display (precision/overflow risk).

usdBalance appears to be 18-decimal bigint; converting to number can lose cents (or far more) once values exceed Number.MAX_SAFE_INTEGER. If this is a portfolio “source of truth” display, consider a safer formatter path (or at least confirm the max expected magnitude).

Possible adjustment (still validate constraints)
+import { formatUnits } from "viem";
...
-  {(Number(usdBalance) / 1e18).toLocaleString(language, {
-    style: "currency",
-    currency: "USD",
-    currencyDisplay: "narrowSymbol",
-  })}
+  {new Intl.NumberFormat(language, {
+    style: "currency",
+    currency: "USD",
+    currencyDisplay: "narrowSymbol",
+  }).format(Number.parseFloat(formatUnits(usdBalance, 18)))}

If balances can be very large, you’ll want a string/decimal-based formatter instead of Number.parseFloat(...).

src/components/getting-started/GettingStarted.tsx (1)

51-57: Prefer tamagui shorthand props (px/py) over paddingHorizontal/paddingVertical. As per coding guidelines/learnings.

Proposed diff
             <YStack
               backgroundColor="$backgroundSoft"
-              paddingHorizontal="$s5"
-              paddingVertical="$s7"
+              px="$s5"
+              py="$s7"
               gap="$s6"
               height="100%"
             >
src/components/card/CardDisclaimer.tsx (1)

39-46: Avoid style={StyleSheet.absoluteFillObject}; use tamagui positioning props instead. As per coding guidelines (no style prop).

Proposed diff
                 <View width="100%" aspectRatio={1} justifyContent="center" alignItems="center">
                   <View width="100%" height="100%">
                     <Blob width="100%" height="100%" />
                   </View>
-                  <View width="100%" height="100%" style={StyleSheet.absoluteFillObject}>
+                  <View position="absolute" top={0} right={0} bottom={0} left={0}>
                     <ExaCard width="100%" height="100%" />
                   </View>
                 </View>
src/components/shared/Success.tsx (1)

90-97: Don’t coerce bigint to number for money (Number(repayAssets)). This can overflow/round and display the wrong amount.

Safer direction (avoid bigint->number unless you can prove it’s safe)
-                    {(Number(repayAssets) / 1e6).toLocaleString(language, {
-                      minimumFractionDigits: 2,
-                      maximumFractionDigits: 2,
-                      useGrouping: false,
-                    })}
+                    {formatUsdc(repayAssets, language)}

You’ll likely want a shared helper (e.g., formatUsdc(value: bigint, language: string)) that:

  • uses formatUnits(value, 6) to get a decimal string, and
  • formats without losing precision (either via a safe-range check before Number(...), or a string-based decimal formatter).
src/components/loans/Amount.tsx (4)

60-68: queryClient.setQueryData updater can crash when old is undefined. You’re spreading old but it may not exist (fresh session / cache cleared).

Proposed diff
-            queryClient.setQueryData(["loan"], (old: Loan) => ({ ...old, amount: null }));
+            queryClient.setQueryData<Loan | undefined>(["loan"], (old) => (old ? { ...old, amount: null } : old));
@@
-                queryClient.setQueryData(["loan"], (old: Loan) => ({ ...old, amount: state.amount }));
+                queryClient.setQueryData<Loan | undefined>(["loan"], (old) =>
+                  old ? { ...old, amount: state.amount } : old,
+                );

Also applies to: 157-160


48-50: Avoid bigint→number coercion for limit checks (Number(state.amount)). This can overflow/lose precision and miscompute disabled. Compare using bigint end-to-end.

Proposed diff (assuming `borrowAvailable` is bigint)
-  const insufficient = Number(state.amount) > borrowAvailable;
+  const insufficient = state.amount > borrowAvailable;

If borrowAvailable is not bigint, please standardize both to the same type (prefer bigint for onchain units) before comparing.

Also applies to: 95-100


80-84: Inline style object literal in contentContainerStyle violates styling rules. Prefer a constant StyleSheet entry (at minimum), or a tamagui-native alternative.

Proposed diff (StyleSheet constant)
-import { Pressable } from "react-native";
+import { Pressable, StyleSheet } from "react-native";
@@
       <ScrollView
         backgroundColor="$backgroundMild"
         showsVerticalScrollIndicator={false}
-        contentContainerStyle={{ flexGrow: 1 }}
+        contentContainerStyle={styles.contentContainer}
       >
@@
 }
+
+const styles = StyleSheet.create({
+  contentContainer: { flexGrow: 1 },
+});

136-140: Use functional state update for toggles. Avoids stale closures during rapid presses.

Proposed diff
-                  setAcknowledged(!acknowledged);
+                  setAcknowledged((previous) => !previous);
src/components/send-funds/Amount.tsx (1)

266-277: Avoid Number(...) for on-chain amounts (precision loss risk).
available is compared as a bigint but formatted via Number(available) / 10 ** decimals; large values can overflow/round and show incorrect balances. Prefer formatUnits + a guarded locale formatter.

Proposed fix (guarded formatter + formatUnits)
@@
 import { encodeAbiParameters, erc20Abi, formatUnits, parseUnits, zeroAddress as viemZeroAddress } from "viem";
@@
 import View from "../shared/View";
 
+function formatLocaleNumber(value: string, language: string, options?: Intl.NumberFormatOptions) {
+  const numeric = Number(value);
+  return Number.isFinite(numeric) ? numeric.toLocaleString(language, options) : value;
+}
+
 export default function Amount() {
@@
                       <Text callout color="$uiNeutralPrimary" numberOfLines={1}>
                         {market ? (
                           <>
-                            {`${(Number(available) / 10 ** market.decimals).toLocaleString(language, {
-                              minimumFractionDigits: 0,
-                              maximumFractionDigits: market.decimals,
-                            })} ${market.symbol.slice(3)}`}
+                            {`${formatLocaleNumber(formatUnits(available, market.decimals), language, {
+                              minimumFractionDigits: 0,
+                              maximumFractionDigits: market.decimals,
+                            })} ${market.symbol.slice(3)}`}
                           </>
                         ) : external ? (
                           <>
-                            {`${(Number(available) / 10 ** external.decimals).toLocaleString(language, {
-                              minimumFractionDigits: 0,
-                              maximumFractionDigits: external.decimals,
-                            })} ${external.symbol}`}
+                            {`${formatLocaleNumber(formatUnits(available, external.decimals), language, {
+                              minimumFractionDigits: 0,
+                              maximumFractionDigits: external.decimals,
+                            })} ${external.symbol}`}
                           </>
                         ) : null}
                       </Text>
@@
             <Text title primary color="$uiNeutralPrimary">
-              {Number(details.usdValue).toLocaleString(language, {
+              {formatLocaleNumber(details.usdValue, language, {
                 style: "currency",
                 currency: "USD",
                 currencyDisplay: "narrowSymbol",
-              })}
+              })}
             </Text>
@@
               <Text emphasized secondary subHeadline>
-                {Number(details.amount).toLocaleString(language, { maximumFractionDigits: 8 })}
+                {formatLocaleNumber(details.amount, language, { maximumFractionDigits: 8 })}
               </Text>

Also applies to: 415-424

src/components/loans/Receiver.tsx (1)

33-35: Guard symbol interpolation to avoid “undefined”/blank copy during loading.

Proposed fix
   const { data: loan } = useQuery<Loan>({ queryKey: ["loan"], enabled: !!address });
   const { market } = useAsset(loan?.market);
-  const symbol = market?.symbol.slice(3) === "WETH" ? "ETH" : market?.symbol.slice(3);
+  const marketSymbol = market?.symbol.slice(3);
+  const symbol = marketSymbol === "WETH" ? "ETH" : marketSymbol;
@@
                       {t("Deposit {{symbol}} into your Exa App wallet", { symbol })}
@@
                       {t("Deposit {{symbol}} directly to an external wallet", { symbol })}
-                      {t("Deposit {{symbol}} into your Exa App wallet", { symbol })}
+                      {t("Deposit {{symbol}} into your Exa App wallet", { symbol: symbol ?? "" })}
@@
-                      {t("Deposit {{symbol}} directly to an external wallet", { symbol })}
+                      {t("Deposit {{symbol}} directly to an external wallet", { symbol: symbol ?? "" })}

Also applies to: 129-130, 160-161

src/components/add-funds/Bridge.tsx (1)

309-378: Localize remaining user-facing status/toast strings (bridgeStatus + processing + handleError).
Right now Spanish users will still see English for bridgeStatus, success toasts, processing labels, and the failure toast in handleError().

Proposed fix (use t() with interpolation and pass t into handleError)
@@
+import type { TFunction } from "i18next";
 import React, { useEffect, useMemo, useRef, useState } from "react";
 import { useTranslation } from "react-i18next";
@@
     mutationFn: async (from) => {
@@
-      setBridgeStatus(`Switching to ${selectedGroup?.chain.name ?? `Chain ${from.chainId}`}...`);
+      setBridgeStatus(
+        t("Switching to {{chain}}...", {
+          chain: selectedGroup?.chain.name ?? t("Chain {{chainId}}", { chainId: from.chainId }),
+        }),
+      );
       await switchChain(senderConfig, { chainId: from.chainId });
@@
-        setBridgeStatus("Checking allowance...");
+        setBridgeStatus(t("Checking allowance..."));
@@
-      setBridgeStatus("Submitting bridge transaction...");
+      setBridgeStatus(t("Submitting bridge transaction..."));
       try {
@@
-        setBridgeStatus("Bridge transaction submitted");
+        setBridgeStatus(t("Bridge transaction submitted"));
       } catch (error) {
@@
-        setBridgeStatus("Bridge transaction submitted");
+        setBridgeStatus(t("Bridge transaction submitted"));
       }
     },
     onSuccess: async () => {
-      toast.show("Bridge transaction submitted", {
+      toast.show(t("Bridge transaction submitted"), {
         native: true,
         duration: 1000,
         burntOptions: { haptic: "success", preset: "done" },
       });
@@
     onError: (error: unknown) => {
-      handleError(error, toast);
+      handleError(error, toast, t);
     },
@@
-      setBridgeStatus("Submitting transfer transaction...");
+      setBridgeStatus(t("Submitting transfer transaction..."));
@@
-      setBridgeStatus("Transfer transaction submitted");
+      setBridgeStatus(t("Transfer transaction submitted"));
     },
     onSuccess: async () => {
-      toast.show("Transfer transaction submitted", {
+      toast.show(t("Transfer transaction submitted"), {
         native: true,
         duration: 1000,
         burntOptions: { haptic: "success", preset: "done" },
       });
@@
     onError: (error: unknown) => {
-      handleError(error, toast, true);
+      handleError(error, toast, t, true);
     },
@@
 function handleError(
   error: unknown,
   toast: ReturnType<typeof useToastController>,
+  t: TFunction,
   isTransfer?: boolean
 ) {
@@
-  toast.show(`${isTransfer ? "Transfer" : "Bridge"} failed. Please try again.`, {
+  toast.show(
+    t("{{action}} failed. Please try again.", { action: isTransfer ? t("Transfer") : t("Bridge") }),
+    {
       native: true,
       duration: 1000,
       burntOptions: { haptic: "error", preset: "error" },
-  });
+    },
+  );
   reportError(error);
 }

Also applies to: 397-424, 450-457, 503-509, 974-983

src/components/activity/PendingProposals.tsx (2)

209-268: Avoid calling renderMaturity() twice; use function declaration instead.

The function is called twice (at line 263 in the condition check and line 264 in the render), which is inefficient. Compute the result once and reuse it. Additionally, convert the multi-line arrow function to a function declaration per coding guidelines.

Example refactor
-  const renderMaturity = () => {
+  function renderMaturity() {
     if (!decoded) return null;
     if ("maturity" in decoded) {
       return new Date(Number(decoded.maturity) * 1000).toLocaleDateString(language, {
         month: "short",
         day: "numeric",
       });
     }
     if ("repayMaturity" in decoded && "borrowMaturity" in decoded) {
       return `${new Date(Number(decoded.repayMaturity) * 1000).toLocaleDateString(language, {
         month: "short",
         day: "numeric",
       })} → ${new Date(Number(decoded.borrowMaturity) * 1000).toLocaleDateString(language, {
         month: "short",
         day: "numeric",
       })}`;
     }
     if ("receiver" in decoded) return shortenHex(decoded.receiver, 5, 5);
     return null;
-  };
+  }
+
+  const maturityLabel = renderMaturity();
-          {renderMaturity() && (
+          {maturityLabel && (
             <Text footnote maxFontSizeMultiplier={1} color="$uiNeutralSecondary" numberOfLines={1}>
-              {renderMaturity()}
+              {maturityLabel}
             </Text>
           )}

272-290: Use a bigint-safe formatting utility instead of direct Number conversion.

usdValue and proposal.amount are bigints that can exceed JavaScript's safe integer range (2^53 - 1). Converting them directly with Number() before decimal division loses precision silently for large amounts. The codebase already uses formatUnits elsewhere for this purpose. Move decimal handling to a utility that operates on bigints before converting to Number for display.

🤖 Fix all issues with AI agents
In `@src/components/auth/Auth.tsx`:
- Around line 144-147: The Auth component calls t(title) (Auth component around
t(title)), but the English translation file is missing the corresponding keys;
open src/i18n/en.json and add the four missing keys with their English string
values ("Introducing the first onchain card", "Pay later in installments and
hold your crypto", "Maximize earnings, effortlessly", "In-store QR payments,
with crypto") so t(title) returns the proper English text rather than the raw
key.

In `@src/components/card/CardPIN.tsx`:
- Around line 125-127: Replace the separate countdown child with a single
translated label that includes the countdown via interpolation: change the
Button label logic (using the Button component and the t function) so it calls
one translation key when showing the PIN (e.g., t("card.showPinWithCountdown", {
count: countdown })) and falls back to t("Retry") when error is true and t("Hide
PIN") when displayPIN is true without a countdown; only pass count when !error
&& displayPIN && countdown > 0 to ensure the correct key/plural form is chosen
and update the i18n resource to include a "{{count}}" interpolation (and plural
forms if needed).

In `@src/components/card/VerificationFailure.tsx`:
- Around line 38-42: The translation keys in VerificationFailure.tsx use a curly
apostrophe in t("We couldn’t verify your identity") which can mismatch the JSON;
replace the phrase-based key with a stable identifier (e.g.,
verification.failure.title and verification.failure.body) or at minimum change
the text to use a straight apostrophe ("couldn't") to match your JSON, then
update the corresponding i18n entries to use the exact same keys/strings so the
lookup succeeds (update the t(...) calls and the translation JSON entries for
those identifiers accordingly).

In `@src/components/defi/AboutDefiSheet.tsx`:
- Around line 28-30: The translation call uses a full sentence as the key which
is hard to maintain; replace the long literal in the t(...) call inside
AboutDefiSheet (where t is invoked) with a short, descriptive key like
"about_defi_description", then add that key to your i18n translation files with
the current sentence as its value and run/verify the extractor or translation
loader so the new key is picked up.

In `@src/components/home/HomeDisclaimer.tsx`:
- Around line 14-39: The custom DisclaimerLink component is swallowing the
placeholder link text injected by Trans because it returns a self-closing Text
element instead of rendering its children; update the DisclaimerLink component
(the functional/component definition named DisclaimerLink) to accept a children
prop (React.ReactNode) and render it inside the returned <Text> (or
Touchable/Text wrapper) so that the Trans placeholders ("Exa App", "Exactly
Protocol", "Terms and conditions") are displayed, and ensure any onPress prop is
forwarded and used as before (preserve reportError handling).

In `@src/components/home/Portfolio.tsx`:
- Around line 115-130: Replace the inline English sentence used as the Trans
i18nKey with a stable key (e.g., "portfolio.performanceDisclaimer") and move the
full sentence into your locale files so Trans looks up the text by key; for the
clickable "protocol" component (the Text inside components.protocol) add link
accessibility metadata such as accessibilityRole="link" and an
accessibilityLabel or accessibilityHint describing the destination, and keep the
existing onPress that calls openBrowser(...). Ensure you still call reportError
in the promise catch and update any tests/strings that expect the original
inline text key.

In `@src/components/loans/LoanSummary.tsx`:
- Around line 20-23: Guard all numeric calculations and formatting in
LoanSummary (references: borrow, pending, timestamp, and any APR/amount
formatting logic) by first checking that borrow is defined and numeric inputs
are finite; replace direct Number(borrow?.assets) and APR formulas with checks
like borrow && Number.isFinite(+borrow.assets) before converting/using values,
and treat non-finite/undefined inputs as null/placeholder for display.
Specifically ensure the APR computation avoids division-by-zero by verifying
borrow.maturity > timestamp before dividing (otherwise return a safe default or
display “—”), and use Number.isFinite or isFinite on parsed values to prevent
NaN/Infinity from reaching the UI; apply these guards to all places performing
numeric conversion/formatting referenced in the comment (the Amount and APR
calculation sites).

In `@src/components/loans/Review.tsx`:
- Around line 321-330: The rendered date uses locale-aware toLocaleDateString;
replace it with an ISO-formatted date using date-fns (e.g., import formatISO or
format and fromUnixTime) so the Text headline uses
formatISO(fromUnixTime(Number(maturity))) or
format(fromUnixTime(Number(maturity)), "yyyy-MM-dd") instead of
toLocaleDateString; update the import list in Review.tsx to include the chosen
date-fns helpers and remove the language/locale argument from this date
rendering, keeping the surrounding Text and singleInstallment logic intact.

In `@src/components/pay-mode/Pay.tsx`:
- Around line 608-616: Extract the repeated percentage formatting into a small
helper (e.g., formatCompactPercent) and replace the inline chains inside the Pay
component where
Math.abs(discountOrPenaltyPercentage).toLocaleString(...).replaceAll(...) is
used; the helper should accept (value: number, language: string), perform
Math.abs + toLocaleString with style "percent" and min/maxFractionDigits 2, then
strip whitespace, and call it from the two t(...) expressions that currently
format discountOrPenaltyPercentage using language.

In `@src/components/pay-mode/PaymentSheet.tsx`:
- Around line 83-92: Replace the locale-dependent date rendering that uses
dueDate.toLocaleDateString(...) with a fixed ISO format using date-fns: call
format(dueDate, "yyyy-MM-dd") (from the date-fns `format` function) wherever the
code displays the due date (the dueDate variable and the line invoking
toLocaleDateString), mirroring the pattern used in TransactionDetails; add the
import for format from 'date-fns' if missing and remove the month: "short"
locale option so the app consistently shows yyyy-MM-dd across locales.

In `@src/components/pay-mode/PaySelector.tsx`:
- Around line 222-228: The displayed date in PaySelector.tsx is using
locale-aware toLocaleDateString (inside the t(...) call) which conflicts with
the repo decision to use ISO dates; replace the date expression new
Date(firstMaturity * 1000).toLocaleDateString(...) with an ISO yyyy-MM-dd string
(e.g., new Date(firstMaturity * 1000).toISOString().slice(0,10) or equivalent)
so the translated string still wraps the ISO date but the rendered date is
consistent across locales.
- Around line 124-129: The Trans component in PaySelector is using the full
English sentence as i18nKey which is brittle; replace that long sentence key
with a stable key (e.g., "payMode.description") in the Trans props and move the
full English string into your locale file(s) under that key, keeping the
interpolation ({{max}}) and component placeholder (<strong>) intact; update the
locale JSON to include "payMode.description": "Choose <strong>Pay Now</strong>
to pay from your USDC balance, or Pay Later to split your purchase into up to
{{max}} fixed-rate USDC installments, powered by Exactly Protocol.*" and ensure
Trans still receives values={{ max: MAX_INSTALLMENTS }} and components={{
strong: <Text emphasized /> }} so runtime behavior is unchanged.

In `@src/components/send-funds/Contacts.tsx`:
- Around line 13-16: The useTranslation hook is declared after useQuery which
breaks the convention of grouping hooks at the top; move the call to
useTranslation() (the t constant) above the useQuery invocation so hooks are
declared together at the top of the Contacts component (ensure useTranslation
and useQuery remain in the same scope and update any references to t and
savedContacts accordingly).

In `@src/components/shared/PluginUpgrade.tsx`:
- Line 14: Add the two missing English translation entries to src/i18n/en.json
so they match the keys used by the PluginUpgrade component: add "An account
upgrade is required to access the latest features." and "Upgrade account now"
with their English text as values, ensure valid JSON syntax (commas, quotes) and
consistent key naming exactly as present in es.json so the useTranslation hook
in PluginUpgrade.tsx resolves correctly.

In `@src/components/shared/ProcessingBalanceBanner.tsx`:
- Around line 10-14: The current truthy check in ProcessingBalanceBanner (if
(!processingBalance) return null) incorrectly hides the banner when
processingBalance === 0; replace that with an explicit check: if the component
should only show when processingBalance > 0 use an explicit numeric check (e.g.,
return null when processingBalance <= 0), otherwise if the intent is to hide
only while loading use a null/undefined check (e.g., return null when
processingBalance == null); update the same explicit check used at the other
occurrence around the handling at the lines referenced (the second
processingBalance conditional) so both places use the chosen explicit condition.

In `@src/components/shared/Success.tsx`:
- Around line 38-42: The component currently formats maturity dates with
locale-dependent toLocaleDateString; update the code to use ISO format by adding
format to the existing date-fns import and replacing the toLocaleDateString
usage with format(new Date(Number(maturity) * 1000), "yyyy-MM-dd") inside the
i18n "Due {{date}}" wrapper (e.g., in the Success component where
useAsset/selectedAsset and maturity are used). Make the same change in
Pending.tsx, Failure.tsx, and any other components that use toLocaleDateString
to ensure consistent yyyy-MM-dd output.

In `@src/i18n/en.json`:
- Around line 2-3: The JSON keys use sentence-like keys containing the count
placeholder ("Installments selected_one"/"_other"), which is fragile; rename
them to a stable, structured key such as "installments.selected" and supply
plural forms as values (e.g., "installments.selected_zero"/"_one"/"_other" or
the i18n library's plural nesting), then update all call sites to use
t("installments.selected", { count }) (also apply the same refactor for the
other keys noted at lines 10-11).
♻️ Duplicate comments (25)
src/components/pay-mode/PaySelector.tsx (1)

278-289: APR calculation is still hard to read; extracting a helper would help.
This nested ternary remains complex and easy to regress when i18n/formatting evolves.

.changeset/better-radios-joke.md (1)

1-5: Changeset summary doesn’t match the PR scope (i18n).
Please align the release note with the i18n work being shipped. Based on learnings, keep the “ ” format.

Proposed fix
-🐛 fix card 404 error handling
+✨ add i18n support
src/components/home/card-upgrade/Progression.tsx (1)

26-28: i18n integration looks correct.

The interpolation key pattern "{{count}} step remaining" was previously discussed in this PR. The implementation correctly passes the count option which enables i18next's pluralization. Ensure the corresponding plural keys (_one, _other suffixes) exist in en.json and es.json.

src/components/send-funds/QR.tsx (1)

112-126: Keep instructional text consistent with translated button labels via interpolation.
Right now the sentence hardcodes 'Continue'/'Back', which can diverge from the actual translated button texts.

proposed diff
             <Text secondary footnote textAlign="center">
-              {t("Press 'Continue' to proceed or 'Back' to cancel.")}
+              {t("Press '{{continue}}' to proceed or '{{back}}' to cancel.", {
+                continue: t("Continue"),
+                back: t("Back"),
+              })}
             </Text>
src/components/send-funds/ReviewSheet.tsx (1)

44-47: i18n implementation looks correct.

The translation hook is properly destructured to extract both t and language. Locale-aware currency formatting on lines 83-88 correctly uses the language variable. All UI strings are wrapped with t().

Note: The missing translation key "Simulation failed" (line 116) was flagged in a previous review and should be addressed in the translation files.

Also applies to: 62-62, 68-68, 83-88, 95-95, 107-107, 116-116, 123-123

src/components/swaps/SwapDetails.tsx (1)

21-64: i18n implementation is correct.

All label strings are properly wrapped with t(). The locale-aware percentage formatting suggestion was noted in a previous review as an optional enhancement.

src/components/home/AssetList.tsx (1)

29-33: Redundant useTranslation hook in child component.

This was flagged in a previous review. AssetRow is rendered multiple times via .map() (line 109), and each instance invokes useTranslation(). Consider passing t and language as props from the parent AssetList component.

src/components/send-funds/Receiver.tsx (1)

161-161: Arrival time is hardcoded to 1 minute — confirm this is intended.
If it can vary, pass/compute the estimate instead of hardcoding.

src/components/add-funds/AddCrypto.tsx (1)

179-180: Consider moving the leading space into the translation.

Having {" "} as a separate JSX node is fragile. Including the space in the translation key itself would be cleaner.

src/components/add-funds/AssetSelectSheet.tsx (1)

70-70: Double translation pattern detected.

The label prop is translated with t(label), but callers may already pass a translated string (e.g., t("Select asset to send")). This results in t(t("...")), which attempts to translate an already-translated string. Either translate here and pass raw keys from callers, or pass translated strings and render directly without t().

src/components/auth/PasskeysAbout.tsx (1)

52-56: Consider using margins instead of embedded trailing spaces.

The pattern {`${t("...")} `} embeds visual spacing in JavaScript. This makes spacing dependent on the code rather than the translation or styling system. Using marginRight on the Text component would be more maintainable.

src/components/shared/CopyAddressSheet.tsx (1)

39-43: Consider moving away from natural-language keys for long sentences (maintainability).

src/components/shared/InstallmentSelector.tsx (1)

62-66: Don’t split “installments of + amount” across nodes; keep as one translatable unit.

This still leaves translators stuck with English word order (“… of” trailing). Use a single key with amount interpolation (and pluralization) instead.

Also applies to: 110-112

src/components/home/AssetChange.tsx (1)

4-4: Prefer XStack over View flexDirection="row" for layout.

Also applies to: 9-10, 20-20

src/components/activity/details/OperationDetails.tsx (1)

110-110: Acknowledging intentional ISO date format.

The hardcoded "yyyy-MM-dd" format is noted. Based on learnings, the Exa app deliberately uses ISO format for dates and times for consistency across all locales, so this is the expected behavior.

src/components/home/card-upgrade/ActivateCard.tsx (1)

139-141: Consider extracting the disclaimer to a shared semantic key.

This long legal disclaimer text appears to be duplicated in CardDisclaimer.tsx. Using a shared i18n key (e.g., t("card.visaDisclaimer")) would improve maintainability and ensure consistency across components.

src/components/shared/AddressDialog.tsx (1)

53-53: Replace sentence-as-keys with structured keys (already requested previously).

This still uses literal sentences as keys (t("Network reminder"), t("Copy account address"), and the Trans i18nKey), which hurts maintainability and increases collision risk.

Example change
-  {t("Network reminder")}
+  {t("addressDialog.networkReminderTitle")}

   <Trans
-    i18nKey="Add funds using <emphasis>{{network}}</emphasis> only. Sending assets on any other network will cause irreversible loss of funds."
+    i18nKey="addressDialog.addFundsDisclaimer"
     values={{ network: chain.name }}
     components={{ emphasis: <Text emphasized secondary /> }}
   />

-  {t("Copy account address")}
+  {t("addressDialog.copyAccountAddress")}

Then add the corresponding entries to src/i18n/en.json and src/i18n/es.json.

Also applies to: 70-74, 87-87

src/components/card/exa-card/CardContents.tsx (1)

97-107: Locale-aware currency formatting correctly implemented for debit branch.

The duplication of the "Available balance" label JSX between credit (lines 82-86) and debit (lines 103-107) branches was already flagged in a previous review.

src/components/getting-started/GettingStarted.tsx (1)

205-227: Fix/verify “steps left” logic and switch to i18next pluralization. If completedSteps can ever be 0, the current text shows “Two steps left” when it should be “Three steps left”. Also, pluralization shouldn’t be hard-coded with 2 keys.

Proposed diff (uses count = 3 - completedSteps)
 function StepCounter({ completedSteps }: { completedSteps: number }) {
   const { t } = useTranslation();
+  const stepsLeft = Math.max(0, 3 - completedSteps);
   return (
     <YStack gap="$s3_5">
@@
       <XStack justifyContent="space-between" gap="$s3">
         <Text emphasized subHeadline color="$uiBrandTertiary">
-          {completedSteps > 1 ? t("One step left") : t("Two steps left")}
+          {t("stepsLeft", { count: stepsLeft })}
         </Text>
         <Text emphasized subHeadline color="$uiBrandTertiary">
           {completedSteps}/3
         </Text>
       </XStack>
     </YStack>
   );
 }

Please confirm completedSteps is guaranteed to start at 1 (e.g., “Account created” is always counted). If yes, the off-by-one is moot, but pluralization is still the better i18n shape.

src/components/home/GettingStarted.tsx (1)

20-22: Decouple step titles from translation keys (t(currentStep.title)). This will break silently if currentStep.title changes.

Minimal safer diff (fallback to raw title)
-  const activeStepTitle = currentStep ? t(currentStep.title) : "";
+  const activeStepTitle = currentStep ? t(currentStep.title, { defaultValue: currentStep.title }) : "";

Longer-term, prefer t(stepTranslationKeyById[currentStep.id]) (or add translationKey to your step objects) so display strings and keys don’t drift.

Also applies to: 72-73, 101-103

src/components/card/CardDisclaimer.tsx (1)

79-83: Avoid passing the full disclaimer text directly to t(); use a semantic key. This is hard to maintain and review, and makes translation churn noisy.

src/components/card/SpendingLimits.tsx (1)

57-61: Remove inline style={{ ... }} on Pressable (guideline violation).
This is the same issue previously flagged. As per coding guidelines / learnings, avoid inline style objects in style props.

Proposed fix
-                <Pressable onPress={onClose} style={{ alignSelf: "center" }} hitSlop={20}>
-                  <Text emphasized footnote color="$interactiveTextBrandDefault">
-                    {t("Close")}
-                  </Text>
-                </Pressable>
+                <YStack alignSelf="center">
+                  <Pressable onPress={onClose} hitSlop={20}>
+                    <Text emphasized footnote color="$interactiveTextBrandDefault">
+                      {t("Close")}
+                    </Text>
+                  </Pressable>
+                </YStack>
src/components/send-funds/Amount.tsx (1)

289-294: Use a direct validation key instead of interpolating a constant.
This is the same issue previously flagged: t("Amount cannot be {{amount}}", { amount: 0 }) always interpolates 0.

Proposed fix
                     check(
                       (value) => {
                         return value !== 0n;
                       },
-                      t("Amount cannot be {{amount}}", { amount: 0 }),
+                      t("Amount cannot be zero"),
                     ),
src/components/add-funds/Bridge.tsx (1)

942-968: Avoid double translation for AssetSelectSheet’s label prop.
If AssetSelectSheet still does t(label), passing label={t("...")} will re-translate a translated string (and makes extraction harder).

Proposed fix (pass the key, not the translated value)
 <AssetSelectSheet
-  label={t("Select asset to send")}
+  label="Select asset to send"
   open={assetSheetOpen}
   onClose={() => {
     setAssetSheetOpen(false);
   }}
@@
 <AssetSelectSheet
   hideBalances
-  label={t("Select asset to receive")}
+  label="Select asset to receive"
   open={destinationModalOpen}
   onClose={() => {
     setDestinationModalOpen(false);
   }}

To verify whether this is needed, check if AssetSelectSheet does something like t(label):

#!/bin/bash
set -euo pipefail

fd -a 'AssetSelectSheet\.tsx$' .
rg -n --hidden --no-ignore -S 't\s*\(\s*label\s*\)' $(fd -a 'AssetSelectSheet\.tsx$' .)
src/components/card/Card.tsx (1)

456-471: Verify that the translation key exists in en.json.

The Trans component uses the full English sentence as the i18nKey. While this works, a previous review flagged that this key may be missing from en.json, which would cause the <link> component to render as plain text for English users.

#!/bin/bash
# Description: Verify that the Trans i18nKey for Card disclaimer exists in en.json

# Search for the key in en.json
rg -F "The Exa Card is issued by Third National Bank" src/i18n/en.json

Comment on lines +38 to 42
{t("We couldnt verify your identity")}
</Text>
<Text color="$uiNeutralPlaceholder" footnote textAlign="center">
This may be due to missing or incorrect information. Please contact support to resolve it.
{t("This may be due to missing or incorrect information. Please contact support to resolve it.")}
</Text>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Be careful with smart quotes in translation keys (easy to mismatch).
If keys are phrase-based, ensure the JSON uses the exact same character for couldn’t (curly vs straight). Consider stable key IDs long-term.

🤖 Prompt for AI Agents
In `@src/components/card/VerificationFailure.tsx` around lines 38 - 42, The
translation keys in VerificationFailure.tsx use a curly apostrophe in t("We
couldn’t verify your identity") which can mismatch the JSON; replace the
phrase-based key with a stable identifier (e.g., verification.failure.title and
verification.failure.body) or at minimum change the text to use a straight
apostrophe ("couldn't") to match your JSON, then update the corresponding i18n
entries to use the exact same keys/strings so the lookup succeeds (update the
t(...) calls and the translation JSON entries for those identifiers
accordingly).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
src/components/shared/AssetSelector.tsx (2)

44-46: Hardcoded string should be internationalized.

"No available assets." is not translated, which is inconsistent with the i18n effort in this PR.

Suggested fix
+  const { t, i18n: { language } } = useTranslation();
-  const {
-    i18n: { language },
-  } = useTranslation();

Then at line 44-46:

       <Text textAlign="center" emphasized footnote color="$uiNeutralSecondary">
-        No available assets.
+        {t("No available assets.")}
       </Text>

88-89: Accessibility labels should be internationalized.

The aria-label and aria-describedby contain hardcoded English text. For proper accessibility in all supported languages, these should use the translation function.

Suggested fix
-              aria-label={`${symbol}, ${balance} available`}
-              aria-describedby="tap to select"
+              aria-label={t("{{symbol}}, {{balance}} available", { symbol, balance })}
+              aria-describedby={t("tap to select")}
src/components/swaps/Success.tsx (1)

61-65: Inline style object in contentContainerStyle.

The coding guidelines prohibit inline object literals for the style prop. While contentContainerStyle is a ScrollView-specific prop and may be harder to avoid, consider extracting this to a constant or using Tamagui's ScrollView styling capabilities if available.

♻️ Suggested refactor
+const scrollContentStyle = {
+  flexGrow: 1,
+  flexDirection: "column",
+  justifyContent: "space-between",
+} as const;
+
 export default function Success({
   ...
 }) {
   ...
   return (
     ...
           <ScrollView
             showsVerticalScrollIndicator={false}
             stickyHeaderIndices={[0]}
-            contentContainerStyle={{
-              flexGrow: 1,
-              flexDirection: "column",
-              justifyContent: "space-between",
-            }}
+            contentContainerStyle={scrollContentStyle}
             stickyHeaderHiddenOnScroll
           >
src/components/send-funds/QR.tsx (1)

164-176: Add accessibility label for screen reader support.

The Pressable back button lacks an accessibilityLabel, which is needed for screen reader users to understand its purpose. Per jsx-a11y/recommended guidelines, interactive elements should have accessible names.

♿ Suggested fix
-      <View position="absolute" borderRadius="$r_0" backgroundColor="transparent" top={top} left="$s4" padding="$s3">
-        <Pressable
-          hitSlop={15}
-          onPress={() => {
-            if (router.canGoBack()) {
-              router.back();
-            } else {
-              router.replace("/send-funds");
-            }
-          }}
-        >
-          <ArrowLeft size={24} color="white" />
-        </Pressable>
-      </View>
+      <View position="absolute" borderRadius="$r_0" backgroundColor="transparent" top={top} left="$s4" padding="$s3">
+        <Pressable
+          hitSlop={15}
+          accessibilityLabel={t("Back")}
+          accessibilityRole="button"
+          onPress={() => {
+            if (router.canGoBack()) {
+              router.back();
+            } else {
+              router.replace("/send-funds");
+            }
+          }}
+        >
+          <ArrowLeft size={24} color="white" />
+        </Pressable>
+      </View>
src/components/send-funds/Amount.tsx (1)

176-200: Untranslated error fallback strings.

The error fallback UI for invalid receiver/asset addresses uses hardcoded English strings ("Invalid receiver address", "Invalid asset address", "Go back") while the rest of the component uses i18n. This creates an inconsistent user experience for non-English users.

Proposed fix
         <View gap="$s5" fullScreen padded justifyContent="center" alignItems="center">
           <Text body primary color="$uiNeutralPrimary">
-            {invalidReceiver ? "Invalid receiver address" : "Invalid asset address"}
+            {invalidReceiver ? t("Invalid receiver address") : t("Invalid asset address")}
           </Text>
           <Button
             dangerSecondary
             alignSelf="center"
             onPress={() => {
               if (router.canGoBack()) {
                 router.back();
               } else {
                 router.replace("/send-funds/asset");
               }
             }}
           >
-            <Button.Text>Go back</Button.Text>
+            <Button.Text>{t("Go back")}</Button.Text>
             <Button.Icon>
               <ArrowLeft size={24} color="$uiNeutralPrimary" />
             </Button.Icon>
           </Button>
         </View>
src/components/loans/Review.tsx (1)

282-287: Inconsistent locale parameter in toLocaleString.

This toLocaleString call uses undefined as the locale parameter, while other calls in this file (lines 238, 262, 311, 448) correctly use language. This will result in inconsistent number formatting for users with non-English locales.

Proposed fix
                     <Text title3 color="$uiNeutralPrimary">
                       {(Number(singleInstallment ? totalAmount : installmentsAmount) / 1e6).toLocaleString(
-                        undefined,
+                        language,
                         { minimumFractionDigits: 2, maximumFractionDigits: 2 },
                       )}
                     </Text>
src/components/loans/LoanSummary.tsx (1)

78-83: Potential NaN display when both installments and borrow are undefined.

If pending is false but both installments and borrow?.assets are undefined, Number(undefined) produces NaN, which will display in the UI. The apr memo handles this with a fallback to "N/A", but this amount display does not.

Proposed fix
            <Text title3>
-             {(
-               Number(!isBorrow && installments ? installments.amounts.reduce((a, b) => a + b, 0n) : borrow?.assets) /
-               1e6
-             ).toLocaleString(language, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+             {(() => {
+               const assets = !isBorrow && installments
+                 ? installments.amounts.reduce((a, b) => a + b, 0n)
+                 : borrow?.assets;
+               if (assets == null) return "N/A";
+               return (Number(assets) / 1e6).toLocaleString(language, {
+                 minimumFractionDigits: 2,
+                 maximumFractionDigits: 2,
+               });
+             })()}
            </Text>
src/components/add-funds/Bridge.tsx (2)

503-508: Untranslated status labels in processing view.

The label variable and the status text in the processing view use English strings directly without translation:

const label = isTransfer ? "Transfer" : "Bridge";
// ...
{isError ? `${label} failed` : isSuccess ? `${label} transaction submitted` : `Processing ${label.toLowerCase()}`}

These should be translated for consistency.

♻️ Suggested fix
-    const label = isTransfer ? "Transfer" : "Bridge";
+    const label = isTransfer ? t("Transfer") : t("Bridge");
// ...
-                  {isError
-                    ? `${label} failed`
-                    : isSuccess
-                      ? `${label} transaction submitted`
-                      : `Processing ${label.toLowerCase()}`}
+                  {isError
+                    ? t("{{label}} failed", { label })
+                    : isSuccess
+                      ? t("{{label}} transaction submitted", { label })
+                      : t("Processing {{label}}", { label: label.toLowerCase() })}

974-982: Untranslated error message in handleError.

The error toast message is hardcoded in English. Since handleError doesn't have access to t, consider passing it as a parameter or moving error handling inline.

♻️ Suggested approach
-function handleError(error: unknown, toast: ReturnType<typeof useToastController>, isTransfer?: boolean) {
+function handleError(
+  error: unknown,
+  toast: ReturnType<typeof useToastController>,
+  t: ReturnType<typeof useTranslation>["t"],
+  isTransfer?: boolean,
+) {
   if (error instanceof UserRejectedRequestError) return;
   if (error instanceof TransactionExecutionError && error.shortMessage === "User rejected the request.") return;
-  toast.show(`${isTransfer ? "Transfer" : "Bridge"} failed. Please try again.`, {
+  toast.show(t(isTransfer ? "Transfer failed. Please try again." : "Bridge failed. Please try again."), {
     native: true,
     duration: 1000,
     burntOptions: { haptic: "error", preset: "error" },
   });
   reportError(error);
 }

Then update call sites to pass t.

src/components/swaps/TokenInput.tsx (1)

177-183: Pre-existing issue: Inline styles violate coding guidelines.

This inline style prop with hardcoded values (fontFamily, fontSize, fontWeight, lineHeight, letterSpacing) exists outside the scope of this PR's i18n changes but violates the guideline to avoid inline styles and use Tamagui's styled system props instead.

As per coding guidelines, all styling must go through Tamagui's styled system props.

src/components/swaps/SelectorModal.tsx (1)

92-92: Default prop makes translation fallback unreachable.

The default prop title = "Select token" is always truthy, so title || t("Select Token") on Line 139 will never invoke the translation function. Additionally, there's a casing mismatch ("token" vs "Token").

Remove the default prop and rely on t() for the default:

Suggested fix
   onClose,
   isLoading = false,
-  title = "Select token",
+  title,
   withBalanceOnly = false,

Then update Line 139 to use the translation as the fallback:

-              {title || t("Select Token")}
+              {title ?? t("Select Token")}

Also applies to: 139-139

🤖 Fix all issues with AI agents
In `@src/components/activity/Activity.tsx`:
- Around line 59-63: Inline style and contentContainerStyle on the FlatList
violate Tamagui styling rules; remove the inline style={{ flex: 1 }} and move
that layout prop into the Tamagui styled component (e.g., StyledFlatList) using
Tamagui props (e.g., flex={1} or its shorthand) and convert background colors to
Tamagui tokens rather than accessing theme.*.val directly. For
contentContainerStyle: either (A) move the static parts into StyledFlatList's
styled definition, or (B) if you must keep the dynamic background based on
data.length, keep contentContainerStyle but add a clear comment documenting this
FlatList exception and derive the color via Tamagui tokens/theme API (not .val)
before passing it in. Update references to the FlatList instance (StyledFlatList
or the component rendering it) and remove all inline object literal styles to
comply with Tamagui.

In `@src/components/activity/details/PaymentDetails.tsx`:
- Line 56: The inner redundant check `item.mode > 0` within the numeric
formatting expression should be removed because the surrounding JSX already
guards the block with `item.mode > 0`; update the expression in PaymentDetails
(where `Number(item.mode > 0 && (item as
CreditActivity).borrow.rate).toLocaleString`) to directly access the value e.g.
`Number((item as CreditActivity).borrow.rate).toLocaleString(language, ...)`,
ensuring you still cast `item` to `CreditActivity` before reading `borrow.rate`
and preserve the existing locale/options.

In `@src/components/activity/details/ReceivedActivity.tsx`:
- Around line 12-13: The TODO notes the prop type for ReceivedActivity is
unclear: replace the inline Omit with a named prop interface (e.g., interface
ReceivedActivityProps) that explicitly lists the fields this component actually
uses (or use Pick<DepositActivity, 'fieldA' | 'fieldB'> to select only needed
props), and update the component signature to export default function
ReceivedActivity(props: ReceivedActivityProps); if blockNumber is required
elsewhere, include it in the interface instead of omitting it.

In `@src/components/activity/details/TransactionDetails.tsx`:
- Around line 24-27: Move the hook call useTranslation() up with the other hooks
so all hooks are declared before any derived state; specifically, reorder the
component so useToastController(), useQuery<ActivityItem>(), and
useTranslation() are all called consecutively at the top of the component, then
compute derived values like const item = source ?? query.data;; ensure you
reference the existing symbols useToastController, useQuery, useTranslation,
source, query, and item so the hook ordering is corrected without changing
logic.

In `@src/components/card/Card.tsx`:
- Around line 456-471: The Trans usage in Card.tsx references a long i18n key
(contains a <link> placeholder) that is missing from the locale files; add the
exact key string as a new entry in en.json and es.json with the same
<link></link> placeholder so i18next can replace it with the React <Text>
component, e.g., add the English entry under the exact key used in the Trans
i18nKey and a corresponding Spanish translation in es.json (keeping the <link>
tags intact), then run the app to verify the link renders instead of the raw key
text.

In `@src/components/home/HomeDisclaimer.tsx`:
- Around line 14-51: Change the two onPress handlers in the DisclaimerLink
components to use plain string literals instead of template literals for static
URLs: update the openBrowser calls in the DisclaimerLink passed as "app"
(currently using `https://docs.exact.ly/exa-app/how-the-exa-app-works`) and
"protocol" (currently using `https://exact.ly/`) to use normal string syntax;
keep the same onPress behavior, error handling (catch(reportError)), and the
presentCollection call for "terms" unchanged.

In `@src/components/loans/Loans.tsx`:
- Line 63: The component Loans.tsx uses translation keys like "Exactly Protocol"
and the long-form descriptions about fixed-interest funding and protocol access
that are missing from i18n; add these keys with appropriate English and Spanish
values to src/i18n/en.json and src/i18n/es.json (use the exact keys as used in
Loans.tsx), ensuring the English text goes into en.json and the Spanish
equivalents into es.json so the t(...) calls in Loans.tsx resolve without
runtime warnings.

In `@src/components/pay-mode/ManualRepaymentSheet.tsx`:
- Around line 44-54: Extract the magic number 0.0045 into a clearly named
constant (e.g., DAILY_PENALTY_RATE = 0.0045) and replace the inline literal in
the Trans component's values with that constant (values={{ rate:
DAILY_PENALTY_RATE.toLocaleString(language, { style: "percent",
minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}); declare the constant
near the top of the module (or export it if shared) and keep the Trans usage and
components prop unchanged.

In `@src/components/shared/Error.tsx`:
- Around line 32-54: The interpolated link Text components inside the Trans call
(components.x and components.discord) are missing the footnote prop, so their
size differs from the parent Text; update both child Text elements used for the
X and Discord links to include the footnote prop (same prop used on the parent
Text) so the link text renders with the same footnote styling, retaining
existing onPress handlers that call openBrowser(...).catch(reportError).

In `@src/components/shared/LiquidationAlert.tsx`:
- Line 3: The English translations file is missing two keys used by the
LiquidationAlert component (it uses useTranslation in
src/components/shared/LiquidationAlert.tsx); open src/i18n/en.json and add the
keys for "Some of your assets are at risk of being liquidated." and "Learn more"
with appropriate English values matching the Spanish entries so the i18n lookup
succeeds at runtime.

In `@src/components/shared/ProcessingBalance.tsx`:
- Line 26: The component currently uses a falsy check "if (!processingBalance)
return null;" which incorrectly hides the UI when processingBalance === 0;
change this to an explicit null/undefined check (e.g. use "if (processingBalance
== null) return null" or "if (processingBalance === null || processingBalance
=== undefined) return null") in ProcessingBalance (and make the same explicit
check in ProcessingBalanceBanner) so zero values render correctly.
♻️ Duplicate comments (42)
src/components/card/SpendingLimits.tsx (1)

57-61: Inline style object violates coding guidelines.

The style={{ alignSelf: "center" }} on Pressable should use Tamagui's styled system instead of an inline style object.

.changeset/better-radios-joke.md (1)

5-5: Changeset message doesn't match PR objectives.

The changeset describes "fix card 404 error handling" but the PR is titled "✨ add i18n" and adds internationalization features (Spanish translations, locale detection, localized strings across components). This mismatch will confuse users about what's included in this release.

Based on learnings, ensure the changeset message accurately reflects the changes being shipped—in this case, the i18n additions.

♻️ Suggested changeset message aligned with PR objectives
-🐛 fix card 404 error handling
+✨ add internationalization support
src/components/shared/InstallmentSelector.tsx (1)

111-111: Translation phrase splitting issue already flagged.

The trailing "of" in the translation key creates a split phrase with the amount rendered separately (line 126). This has been addressed in a previous review comment.

src/components/getting-started/GettingStarted.tsx (1)

205-230: Consider using i18next's pluralization feature.

The separate translation keys for "One step left" and "Two steps left" could be consolidated using i18next's built-in pluralization, which handles varying grammatical rules across languages more gracefully.

src/components/card/VerificationFailure.tsx (1)

38-42: Smart quote / phrase-based key concern already raised.

The previous review comment correctly identified the potential mismatch issue with curly vs. straight apostrophe in couldn't. That guidance still applies.

src/components/shared/CopyAddressSheet.tsx (1)

39-43: Translations correctly wrap all user-facing strings.

All static UI text is now localized. The structured key suggestion from the previous review applies uniformly to these translations as well.

Also applies to: 58-61, 97-97

src/utils/useAuth.ts (1)

60-68: Structured translation keys would improve maintainability.

Using full English sentences as translation keys works but is fragile if text changes. This concern was previously raised.

src/components/shared/AmountSelector.tsx (1)

169-175: Guard against invalid/unsupported locale strings before calling toLocaleString.

The language value from i18n may contain underscores (e.g., es_ES) or be unsupported, causing toLocaleString to throw a RangeError. Normalize and validate the locale before use.

Proposed fix
   const {
     i18n: { language },
   } = useTranslation();
+  const locale = language.replaceAll("_", "-");
+  const safeLocale = Intl.NumberFormat.supportedLocalesOf([locale]).length > 0 ? locale : undefined;

Then use safeLocale instead of language:

-                  {Number(value.replaceAll(/\D/g, ".").replaceAll(/\.(?=.*\.)/g, "")).toLocaleString(language, {
+                  {Number(value.replaceAll(/\D/g, ".").replaceAll(/\.(?=.*\.)/g, "")).toLocaleString(safeLocale, {
                     style: "currency",
                     currency: "USD",
                     currencyDisplay: "narrowSymbol",
                     minimumFractionDigits: 0,
                     maximumFractionDigits: 2,
                   })}
src/components/shared/Success.tsx (1)

81-87: Use ISO date format (yyyy-MM-dd) instead of toLocaleDateString() for consistency.

Per established codebase conventions, date formatting uses ISO format for consistency across all locales. Add format to the date-fns import and use:

-                    {t("Due {{date}}", {
-                      date: new Date(Number(maturity) * 1000).toLocaleDateString(language, {
-                        year: "numeric",
-                        month: "short",
-                        day: "numeric",
-                      }),
-                    })}
+                    {t("Due {{date}}", {
+                      date: format(new Date(Number(maturity) * 1000), "yyyy-MM-dd"),
+                    })}

Update line 5 import:

-import { isAfter } from "date-fns";
+import { format, isAfter } from "date-fns";

Based on learnings, the app intentionally uses ISO format for date consistency across all locales.

src/components/add-funds/AssetSelectSheet.tsx (1)

70-70: Double translation of label prop.

The label prop may already be translated at the call site (e.g., t("Select asset to send") in Bridge.tsx), so wrapping it again with t(label) attempts to translate an already-translated string. This is logically incorrect even if it appears to work.

Either accept pre-translated labels and render directly (like TokenInput), or always pass untranslated keys and translate here—but be consistent.

Suggested fix: render label directly without re-translating
          <Text fontSize={15} fontWeight="bold" textAlign="center">
-            {t(label)}
+            {label}
          </Text>

Then ensure callers pass translated strings: label={t("Select asset to send")}.

src/components/send-funds/Receiver.tsx (1)

161-161: Consider making the arrival time dynamic.

The minutes interpolation value is hardcoded to 1. If arrival time could vary, consider deriving this from network conditions or passing it as a prop.

src/components/swaps/Success.tsx (2)

80-83: Translation keys using English sentences already flagged.

The use of English sentences as i18nKey values ("Swap request <em>sent</em>", "View pending request", "Close") was already identified in a previous review. Please address those suggestions for stable, structured keys.

Also applies to: 129-129, 138-138


70-72: Missing accessibility label already flagged.

The close button's missing aria-label or accessibilityLabel was already raised in a previous review. As per coding guidelines, enforce jsx-a11y/recommended rules.

src/components/home/AssetList.tsx (1)

29-34: Redundant useTranslation hook in child component.

As noted in the previous review, AssetRow is rendered multiple times within .map() (Line 109), and each instance calls useTranslation(). Since both t and language are now needed, consider passing them as props from the parent AssetList which already has access to both.

src/components/pay-mode/UpcomingPayments.tsx (1)

200-212: Consider extracting the penalty rate to a named constant.

The interpolation is correctly implemented. For improved maintainability, consider extracting 0.0045 to a named constant (e.g., DAILY_PENALTY_RATE) so the value is defined once and self-documenting.

♻️ Suggested improvement
+const DAILY_PENALTY_RATE = 0.0045;
+
 // ... in the component:
-            {t("If not, a {{rate}} penalty is added every day the payment is late.", {
-              rate: (0.0045).toLocaleString(language, {
+            {t("If not, a {{rate}} penalty is added every day the payment is late.", {
+              rate: DAILY_PENALTY_RATE.toLocaleString(language, {
                 style: "percent",
                 minimumFractionDigits: 2,
                 maximumFractionDigits: 2,
               }),
             })}
src/components/pay-mode/PaymentSheet.tsx (2)

59-63: Consider extracting locale mapping for reusability.

The language === "es" ? es : enUS pattern appears in multiple files. Extracting to a shared utility (e.g., getDateFnsLocale(language)) would centralize updates when adding new languages.


192-199: Date display conflicts with the app's ISO-format convention.

Based on learnings, the app uses ISO format (yyyy-MM-dd) for dates across all locales for consistency. The current toLocaleDateString with month: "short" renders locale-dependent month names.

🔧 Suggested fix
-import { formatDistance, isAfter } from "date-fns";
+import { format, formatDistance, isAfter } from "date-fns";

Then at lines 192-199:

                 {dueStatus}
                   <Text secondary textAlign="center" emphasized subHeadline color="$uiNeutralSecondary">
-                    {" - "}
-                    {dueDate.toLocaleDateString(language, {
-                      year: "numeric",
-                      month: "short",
-                      day: "numeric",
-                    })}
+                    {" - "}
+                    {format(dueDate, "yyyy-MM-dd")}
                   </Text>
src/components/shared/PluginUpgrade.tsx (1)

87-88: Verify English translations exist for these keys.

A previous review flagged that these translation keys may be missing from src/i18n/en.json. Ensure both keys are present:

  • "An account upgrade is required to access the latest features."
  • "Upgrade account now"
#!/bin/bash
# Verify both translation keys exist in en.json
echo "Checking en.json for PluginUpgrade keys..."
rg -n "An account upgrade is required to access the latest features" src/i18n/en.json
rg -n "Upgrade account now" src/i18n/en.json
src/components/shared/TransactionDetails.tsx (1)

84-96: LGTM!

The "Date" and "Time" labels are properly translated. The hardcoded ISO date/time formats ("yyyy-MM-dd", "HH:mm:ss") are intentionally kept consistent across all locales per established team conventions. Based on learnings, this is the deliberate design choice.

src/components/auth/PasskeysAbout.tsx (1)

52-56: Trailing spaces in template literals are still present.

The pattern {\${t("Strong credentials.")} `}` embeds visual spacing in JavaScript. This concern was flagged in a previous review.

src/components/send-funds/ReviewSheet.tsx (1)

116-116: Missing "Simulation failed" translation key.

This concern was flagged in a previous review. Ensure the key is added to both en.json and es.json.

src/components/home/AssetChange.tsx (1)

9-9: Prefer XStack over View with flexDirection="row".

As per coding guidelines, use Tamagui's XStack for horizontal layout instead of View with flexDirection="row".

♻️ Suggested refactor
-import { View, Text } from "tamagui";
+import { XStack, Text } from "tamagui";
-    <View display="flex" flexDirection="row" alignItems="center" justifyContent="center" gap={5}>
+    <XStack alignItems="center" justifyContent="center" gap={5}>
       <Minus size={20} color="$uiNeutralSecondary" fontWeight="bold" />
       ...
-    </View>
+    </XStack>
src/components/card/exa-card/CardContents.tsx (1)

82-86: Consider extracting the duplicated balance label.

The "Available balance" label JSX is identical in both credit (lines 82-86) and debit (lines 103-107) branches. Extracting it to a shared constant improves maintainability.

♻️ Suggested refactor

Define once before the return:

const balanceLabel = (
  <View>
    <Text color="white" emphasized caption maxFontSizeMultiplier={1} textTransform="uppercase">
      {t("Available balance")}
    </Text>
  </View>
);

Then use {balanceLabel} in both branches.

Also applies to: 103-107

src/components/card/CardPIN.tsx (1)

125-127: Move countdown into translation key for proper i18n support.

The current split between the label and countdown prevents proper localization—some languages require different word order when including the count (e.g., "5秒後に非表示" in Japanese).

🔧 Suggested approach

Use interpolation in the translation key:

-{error ? t("Retry") : displayPIN ? t("Hide PIN") : t("Show PIN")}
-{`${!error && displayPIN && countdown > 0 ? countdown : " "}`}
+{error
+  ? t("Retry")
+  : displayPIN && countdown > 0
+    ? t("Hide PIN ({{count}})", { count: countdown })
+    : displayPIN
+      ? t("Hide PIN")
+      : t("Show PIN")}

Then add the translation key to locale files:

"Hide PIN ({{count}})": "Hide PIN ({{count}})"
src/components/home/card-upgrade/Intro.tsx (1)

13-18: i18n implementation pattern looks good.

The destructuring of t and language from useTranslation() follows the established pattern. Using Intl.DateTimeFormat with the language locale ensures the date is formatted consistently with the user's selected language.

However, note that the hardcoded upgradeDeadline of June 21, 2025 is already past the current date (January 2026). This was flagged in a previous review and should be addressed separately.

src/components/home/Portfolio.tsx (1)

115-132: Consider using a stable i18nKey for maintainability.

Using the full English sentence as the i18nKey is fragile—any copy edit to the source text will break the translation lookup. Consider using a stable key like "portfolio.performanceDisclaimer" and placing the full sentence in your locale files.

Also, in React Native, use accessibilityRole="link" instead of role="link" for proper accessibility support. The role prop is a web DOM attribute that may not work correctly in React Native.

♻️ Suggested improvement
 <Trans
-  i18nKey="Performance is variable, not guaranteed, and powered by <protocol>Exactly Protocol</protocol>. Yields depend on protocol performance and network activity. Past performance does not guarantee future results."
+  i18nKey="portfolio.performanceDisclaimer"
   components={{
     protocol: (
       <Text
         cursor="pointer"
         caption2
         color="$interactiveOnDisabled"
         textDecorationLine="underline"
         aria-label="exactly protocol website"
-        role="link"
+        accessibilityRole="link"
         onPress={() => {
           openBrowser(`https://exact.ly/`).catch(reportError);
         }}
       />
     ),
   }}
 />

Then add to your locale files:

{
  "portfolio.performanceDisclaimer": "Performance is variable, not guaranteed, and powered by <protocol>Exactly Protocol</protocol>. Yields depend on protocol performance and network activity. Past performance does not guarantee future results."
}
src/components/auth/Auth.tsx (1)

144-147: Add missing translation keys to en.json.

The page titles in the pages array (lines 276-298) are used as translation keys via t(title), but they appear to be missing from en.json while present in es.json. Add these keys to ensure consistent translation behavior:

  • "Introducing the first onchain card"
  • "Pay later in installments and hold your crypto"
  • "Maximize earnings, effortlessly"
  • "In-store QR payments, with crypto"
src/components/defi/AboutDefiSheet.tsx (1)

28-30: Consider using shorter translation keys for maintainability.

Using the full sentence as the translation key works but becomes harder to maintain as translation files grow. A shorter semantic key like t("aboutDefi.description") would be more manageable.

src/components/card/CardDisclaimer.tsx (1)

80-82: Consider using a shorter semantic key for the disclaimer.

The long disclaimer text as a translation key works but is harder to maintain. A key like t("cardDisclaimer.visaNotice") would be cleaner.

src/components/card/CardDetails.tsx (1)

145-146: Consider including the NBSP in the translation.

The non-breaking space is placed outside the translation, which limits translator control over spacing. Including it in the translation key or value would be more consistent.

src/components/send-funds/Amount.tsx (1)

289-294: Consider clearer validation message key.

The validation message t("Amount cannot be {{amount}}", { amount: 0 }) always interpolates 0. A clearer key like t("Amount cannot be zero") would be more direct for translators.

src/components/loans/Review.tsx (1)

324-330: Keep date formatting ISO (avoid locale-aware toLocaleDateString here).

Based on learnings, the app intentionally uses ISO date/time formats (e.g., yyyy-MM-dd) across locales for consistency. This locale-aware date formatting deviates from that pattern.

src/components/home/card-upgrade/ActivateCard.tsx (1)

139-141: Consider using a semantic key for the long disclaimer.

This long legal text appears to be duplicated in CardDisclaimer.tsx. Using a shared semantic key (e.g., t("card.visaDisclaimer")) would improve maintainability and ensure consistency across components.

src/components/add-funds/Bridge.tsx (1)

943-943: Double translation issue with AssetSelectSheet label prop.

The label prop passes already-translated strings via t("Select asset to send"). If AssetSelectSheet internally calls t(label), this causes double translation. Verify AssetSelectSheet expects a translation key or pre-translated string.

#!/bin/bash
# Check how AssetSelectSheet uses the label prop
ast-grep --pattern 'function AssetSelectSheet($$$) {
  $$$
}'

# Also search for t(label) pattern in the file
rg -n 't\(label\)' --type=ts

Also applies to: 957-957

src/components/pay-mode/PaySelector.tsx (1)

222-228: Date formatting conflicts with ISO date decision.

Per retrieved learnings, the app intentionally uses ISO format (yyyy-MM-dd) for consistency across locales. This locale-aware toLocaleDateString call conflicts with that decision.

src/components/add-funds/AddCrypto.tsx (1)

179-180: Fragile space handling with separate JSX node.

Having {" "} as a separate JSX node before the translated text is fragile. Consider including the leading space in the translation value itself.

src/components/add-funds/SupportedAssetsSheet.tsx (1)

73-88: Use <Trans> for text with embedded interactive elements.

The warning text and "Learn more" link are two separate t() calls joined with &nbsp;. This approach doesn't give translators control over punctuation and spacing between the sentence and link. Some languages may need different spacing or word order.

This was flagged in a previous review. Using <Trans> with a component slot for the link allows translators to position the link naturally within the sentence.

src/components/shared/ProcessingBalanceBanner.tsx (1)

26-26: Make the "no banner" condition explicit.

if (!processingBalance) hides the banner when processingBalance === 0, null, or undefined. If hiding at 0 is intentional, make it explicit for clarity; otherwise, check only for null/undefined.

This was flagged in a previous review. Consider:

-  if (!processingBalance) return null;
+  if (processingBalance == null || processingBalance <= 0) return null;
src/components/card/Card.tsx (1)

456-471: Long i18nKey with <Trans> - consider a shorter key.

Using the full English sentence as i18nKey works but is verbose. A shorter key like "card.disclaimer" would be cleaner and decouple the key from the English content.

This was flagged in a previous review.

src/components/swaps/SwapDetails.tsx (1)

21-64: LGTM! Label translations are correctly implemented.

All static labels are properly wrapped with t(). The percentage values on lines 54 and 62 were already flagged in a previous review for locale-aware formatting.

src/components/activity/details/OperationDetails.tsx (1)

110-118: ISO date/time format is intentional.

The hardcoded yyyy-MM-dd and HH:mm:ss formats align with the project's deliberate choice to use ISO format for consistency across all locales, as noted in project learnings.

src/components/pay-mode/Pay.tsx (1)

608-631: Percentage formatting helper extraction already suggested.

The repeated pattern of toLocaleString(language, { style: "percent", ... }).replaceAll(/\s+/g, "") at lines 608-616 and 623-631 was flagged in a previous review for extraction into a helper function.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
src/components/shared/Failure.tsx (1)

42-44: Add accessibility label to the X button.

The icon-only button lacks an accessible name. Screen readers won't announce its purpose.

♿ Suggested fix
-          <Pressable onPress={onClose}>
+          <Pressable onPress={onClose} accessibilityLabel={t("Close")} accessibilityRole="button">
             <X size={24} color="$uiNeutralPrimary" />
           </Pressable>
src/components/activity/PendingProposals.tsx (1)

264-267: Cache renderMaturity() result to avoid redundant computation.

The renderMaturity() function is called twice: once for the conditional check and once for rendering. This performs the same computation twice unnecessarily.

Suggested fix

Compute the maturity value once before the return statement and use the cached value:

+  const maturity = renderMaturity();
+
   return (
     <XStack gap="$s4" paddingVertical="$s3">
       ...
-          {renderMaturity() && (
+          {maturity && (
             <Text footnote maxFontSizeMultiplier={1} color="$uiNeutralSecondary" numberOfLines={1}>
-              {renderMaturity()}
+              {maturity}
             </Text>
           )}
       ...
     </XStack>
   );
src/components/shared/LiquidationAlert.tsx (1)

19-35: Use design tokens for spacing values.

Hardcoded numeric values (gap={10}, padding={25}) violate the design token requirement from tamagui.config.ts. Replace these with design tokens like gap="$s3" and p="$s5". However, note that exact token matches do not exist for these specific values—the available spacing tokens are: s0 (0), s1 (2), s1_5 (3), s2 (4), s2_5 (6), s3 (8), s3_5 (12), s4 (16), s4_5 (20), s5 (24), s6 (32), s7 (40), etc. Select the closest semantically appropriate token for your design intent. Also use shorthand props (p instead of padding, f instead of flex) for consistency.

src/components/pay-mode/Pay.tsx (1)

282-286: Inconsistent locale parameter usage.

At line 284, toLocaleString(undefined, ...) is used instead of toLocaleString(language, ...) which is used elsewhere in the file. This breaks the consistent locale-aware formatting pattern.

Suggested fix
                       <Text title3 color="$uiNeutralPrimary">
                         {(Number(singleInstallment ? totalAmount : installmentsAmount) / 1e6).toLocaleString(
-                          undefined,
+                          language,
                           { minimumFractionDigits: 2, maximumFractionDigits: 2 },
                         )}
                       </Text>
src/components/loans/Review.tsx (1)

283-286: Inconsistent locale parameter usage.

At line 284, toLocaleString(undefined, ...) is used instead of toLocaleString(language, ...) which is used elsewhere in this file (e.g., lines 238, 262, 311, 448). This breaks the consistent locale-aware formatting pattern.

Suggested fix
                      <Text title3 color="$uiNeutralPrimary">
                        {(Number(singleInstallment ? totalAmount : installmentsAmount) / 1e6).toLocaleString(
-                          undefined,
+                          language,
                          { minimumFractionDigits: 2, maximumFractionDigits: 2 },
                        )}
                      </Text>
src/components/shared/AssetSelector.tsx (1)

44-47: Missing translation wrapper for user-facing string.

The string "No available assets." on line 45 is not wrapped with t(), inconsistent with the i18n pattern applied throughout this PR.

🌐 Suggested fix
     return (
       <Text textAlign="center" emphasized footnote color="$uiNeutralSecondary">
-        No available assets.
+        {t("No available assets.")}
       </Text>
     );

Also ensure this key is added to es.json:

"No available assets.": "No hay activos disponibles.",
src/components/send-funds/Amount.tsx (1)

176-200: Missing i18n for error state UI strings.

The error messages and button text in the invalid receiver/asset early return block are not localized, which is inconsistent with the rest of the component.

♻️ Proposed fix
   if (invalidReceiver || invalidAsset) {
     return (
       <SafeView fullScreen>
         <View gap="$s5" fullScreen padded justifyContent="center" alignItems="center">
           <Text body primary color="$uiNeutralPrimary">
-            {invalidReceiver ? "Invalid receiver address" : "Invalid asset address"}
+            {invalidReceiver ? t("Invalid receiver address") : t("Invalid asset address")}
           </Text>
           <Button
             dangerSecondary
             alignSelf="center"
             onPress={() => {
               if (router.canGoBack()) {
                 router.back();
               } else {
                 router.replace("/send-funds/asset");
               }
             }}
           >
-            <Button.Text>Go back</Button.Text>
+            <Button.Text>{t("Go back")}</Button.Text>
             <Button.Icon>
               <ArrowLeft size={24} color="$uiNeutralPrimary" />
             </Button.Icon>
           </Button>
         </View>
       </SafeView>
     );
   }
src/components/add-funds/Bridge.tsx (2)

504-508: Untranslated status labels in processing view.

The label variable is used to construct user-facing strings like ${label} failed and Processing ${label.toLowerCase()}, but "Transfer" and "Bridge" are not translated. This breaks i18n for non-English users.

Proposed fix
     const isTransfer = isTransferring || isTransferSuccess || isTransferError;
-    const label = isTransfer ? "Transfer" : "Bridge";
+    const label = isTransfer ? t("Transfer") : t("Bridge");
...
               <Text secondary body>
                 {isError
-                  ? `${label} failed`
+                  ? t("{{action}} failed", { action: label })
                   : isSuccess
-                    ? `${label} transaction submitted`
-                    : `Processing ${label.toLowerCase()}`}
+                    ? t("{{action}} transaction submitted", { action: label })
+                    : t("Processing {{action}}", { action: label.toLowerCase() })}
               </Text>

Alternatively, use separate translation keys for each status message for cleaner translations:

               <Text secondary body>
                 {isError
-                  ? `${label} failed`
+                  ? (isTransfer ? t("Transfer failed") : t("Bridge failed"))
                   : isSuccess
-                    ? `${label} transaction submitted`
-                    : `Processing ${label.toLowerCase()}`}
+                    ? (isTransfer ? t("Transfer transaction submitted") : t("Bridge transaction submitted"))
+                    : (isTransfer ? t("Processing transfer") : t("Processing bridge"))}
               </Text>

974-983: Toast message in handleError is not translated.

The error toast shows a hardcoded English string. For i18n consistency, this should be translated. However, since handleError is outside the component and doesn't have access to t, consider refactoring.

Proposed fix: Pass t function or move translation to caller
-function handleError(error: unknown, toast: ReturnType<typeof useToastController>, isTransfer?: boolean) {
+function handleError(error: unknown, toast: ReturnType<typeof useToastController>, message: string) {
   if (error instanceof UserRejectedRequestError) return;
   if (error instanceof TransactionExecutionError && error.shortMessage === "User rejected the request.") return;
-  toast.show(`${isTransfer ? "Transfer" : "Bridge"} failed. Please try again.`, {
+  toast.show(message, {
     native: true,
     duration: 1000,
     burntOptions: { haptic: "error", preset: "error" },
   });
   reportError(error);
 }

Then update call sites:

-    handleError(error, toast);
+    handleError(error, toast, t("Bridge failed. Please try again."));
...
-    handleError(error, toast, true);
+    handleError(error, toast, t("Transfer failed. Please try again."));
src/components/swaps/SelectorModal.tsx (1)

92-92: Default prop shadows the translation fallback.

title defaults to "Select token" (line 92), so title || t("Select Token") (line 139) never triggers the translation. The hardcoded default also remains untranslated.

🐛 Proposed fix: remove the default and rely on the translation
   title = "Select token",
+  title,
-              {title || t("Select Token")}
+              {title ?? t("Select Token")}

Or if you want to keep a default, make the default use the translation:

-  title = "Select token",
+  title,

And keep line 139 as {title ?? t("Select Token")}.

Also applies to: 139-139

🤖 Fix all issues with AI agents
In `@src/components/home/AssetList.tsx`:
- Around line 115-118: AssetList currently calls useTranslation() only to
extract t, but after the refactor you should also extract language from
useTranslation() and thread it down to child components instead of letting
AssetRow call the hook directly: update AssetList to const { t, language } =
useTranslation(), pass both t and language as props into AssetSection (and any
other intermediate components), and modify AssetSection to forward t and
language through to AssetRow so AssetRow can use the provided props and remove
its own useTranslation hook.

In `@src/components/loans/Receiver.tsx`:
- Around line 215-231: Replace the long full-sentence i18nKey in the Trans call
inside Receiver.tsx with a short descriptive key (e.g.,
"externalAddressWarning") and move the full sentence (including the <learn> tag)
into the translation JSON files under that key; keep the existing values={{
chain: chain.name }} and components.learn mapping to the Text component (with
presentArticle("9056481") and reportError) so only the key changes in the Trans
invocation and the message is maintained in the translations.

In `@src/components/pay-mode/PayMode.tsx`:
- Around line 67-93: The Trans mapping in PayMode.tsx injects link text as
children but the mapped Text components are self-closing and do not render
children, so the link labels disappear; fix by making those mapped components
render children (e.g., replace the self-closing Text entries with <Text
...>{children}</Text>) or reuse the existing DisclaimerLink from
HomeDisclaimer.tsx (extract to a shared component) and pass onPress handlers
that call openBrowser and presentCollection (with reportError.catch) so the
injected "Exactly Protocol" and "Terms and conditions" text appear.
♻️ Duplicate comments (43)
src/components/card/VerificationFailure.tsx (1)

38-42: Curly apostrophe concern already flagged.

The potential mismatch between the curly apostrophe in "We couldn't verify your identity" and the translation JSON keys has been addressed in a previous review comment.

src/components/card/exa-card/CardContents.tsx (1)

76-86: i18n implementation looks good.

The toLocaleString(language, ...) correctly uses the i18n language for locale-aware currency formatting. The translated label with textTransform="uppercase" is a clean approach.

Note: The duplicated label JSX between credit (lines 82-86) and debit (lines 103-107) branches was already flagged in a previous review—consider extracting it to a shared constant.

src/utils/useAuth.ts (1)

60-68: Translation key pattern already discussed.

The use of full English sentences as translation keys was addressed in a previous review. The current implementation is functional and consistent with the approach used elsewhere in this PR.

src/components/shared/AmountSelector.tsx (1)

169-175: Locale validation appears to be missing despite prior review.

The past review suggested normalizing the locale (replacing _ with -) and validating it with Intl.NumberFormat.supportedLocalesOf() before passing to toLocaleString. The current code still passes language directly, which can throw if the locale tag is malformed or unsupported.

Please confirm whether the locale validation fix was intentionally omitted or implemented elsewhere. If not yet addressed:

Proposed fix
  const {
    i18n: { language },
  } = useTranslation();
+ const locale = language.replaceAll("_", "-");
+ const safeLocale = Intl.NumberFormat.supportedLocalesOf([locale]).length > 0 ? locale : undefined;

Then on line 169:

- {Number(value.replaceAll(/\D/g, ".").replaceAll(/\.(?=.*\.)/g, "")).toLocaleString(language, {
+ {Number(value.replaceAll(/\D/g, ".").replaceAll(/\.(?=.*\.)/g, "")).toLocaleString(safeLocale, {
src/components/pay-mode/PaymentSheet.tsx (1)

63-63: Consider extracting locale mapping for reusability.

The language === "es" ? es : enUS pattern appears in multiple files. A shared utility would centralize this logic for maintainability when adding languages.

src/components/shared/CopyAddressSheet.tsx (1)

39-43: Natural-language translation keys are acceptable but could be improved.

The current approach works. This has already been noted in a previous review.

src/components/home/AssetChange.tsx (1)

9-9: Use XStack instead of View with flexDirection="row".

This was already flagged in a previous review. As per coding guidelines, prefer Tamagui's XStack for horizontal layout.

src/components/shared/InstallmentSelector.tsx (1)

111-111: Restructure translation to keep full phrase in one key.

The translation key "{{count}} installments of" splits the phrase, with the amount rendered separately. Per i18n best practices, keep complete grammatical phrases in a single translation key to preserve word order across languages:

t("{{count}} installments of {{amount}}", { count: installment, amount: formattedPrice })
src/components/shared/Error.tsx (1)

32-54: Add footnote prop to interpolated link components for consistent text sizing.

The parent Text at line 31 has the footnote prop, but the child Text components for X and Discord links don't include it. This causes the link text to render at default size instead of footnote size.

Proposed fix
                  x: (
                    <Text
+                     footnote
                      textDecorationLine="underline"
                      color="$interactiveBaseBrandDefault"
                      onPress={() => {
                        openBrowser("https://x.com/Exa_App").catch(reportError);
                      }}
                    />
                  ),
                  discord: (
                    <Text
+                     footnote
                      textDecorationLine="underline"
                      color="$interactiveBaseBrandDefault"
                      onPress={() => {
                        openBrowser("https://discord.gg/fBdVmbH38Y").catch(reportError);
                      }}
                    />
                  ),
src/components/send-funds/ReviewSheet.tsx (1)

44-47: LGTM!

The i18n implementation is well-structured: useTranslation correctly provides t and language, all user-facing strings are wrapped with t(), and currency formatting uses locale-aware toLocaleString(language, ...).

Note: The missing "Simulation failed" translation key (line 116) was already flagged in a previous review comment.

Also applies to: 62-62, 68-68, 83-83, 95-95, 107-107, 116-116, 123-123

src/components/activity/details/PaymentDetails.tsx (1)

56-60: Redundant condition check inside already-guarded block.

The item.mode > 0 check on line 56 is redundant since the entire block is already guarded by {item.mode > 0 && (...)} on line 50.

♻️ Suggested simplification
-              {Number(item.mode > 0 && (item as CreditActivity).borrow.rate).toLocaleString(language, {
+              {(item as CreditActivity).borrow.rate.toLocaleString(language, {
src/components/home/AssetList.tsx (1)

29-33: Redundant useTranslation hook in child component.

AssetRow is rendered for each asset in .map() (line 108), and each instance calls useTranslation(). While react-i18next caches the translation function, invoking the hook in a frequently-rendered child is unnecessary overhead. Consider passing t and language as props from AssetList.

♻️ Suggested refactor
-function AssetRow({ asset }: { asset: AssetItem }) {
-  const {
-    t,
-    i18n: { language },
-  } = useTranslation();
+function AssetRow({ asset, t, language }: { asset: AssetItem; t: (key: string) => string; language: string }) {

Update AssetList to extract both and pass them:

 export default function AssetList() {
-  const { t } = useTranslation();
+  const {
+    t,
+    i18n: { language },
+  } = useTranslation();
       {assets.map((asset) => (
-        <AssetRow key={asset.symbol} asset={asset} />
+        <AssetRow key={asset.symbol} asset={asset} t={t} language={language} />
       ))}
src/components/card/CardPIN.tsx (1)

125-127: Countdown should be part of the translated string for proper i18n support.

The current approach splits the button label and countdown into separate JSX children. This prevents proper translation for languages that require different word order or grammar when including the count.

♻️ Suggested approach

Use a single translation key with interpolation when the countdown is visible:

-{error ? t("Retry") : displayPIN ? t("Hide PIN") : t("Show PIN")}
-{`${!error && displayPIN && countdown > 0 ? countdown : " "}`}
+{error
+  ? t("Retry")
+  : displayPIN && countdown > 0
+    ? t("Hide PIN {{count}}", { count: countdown })
+    : displayPIN
+      ? t("Hide PIN")
+      : t("Show PIN")}

Then add the translation key with {{count}} placeholder to both en.json and es.json.

src/components/home/card-upgrade/Intro.tsx (1)

17-18: Hardcoded upgrade deadline is expired and inconsistent.

The deadline new Date(2025, 5, 21) (June 21, 2025) has already passed as of January 2026. Additionally, a previous review noted that Home.tsx displays a different date ("May 18th, 2025"), creating inconsistent messaging. Consider sourcing this deadline from a centralized config or API, or conditionally hiding/gating this upgrade flow if the deadline has passed.

src/components/home/HomeDisclaimer.tsx (1)

20-20: Prefer regular strings over template literals for static URLs.

Lines 20 and 27 use template literals for static strings. Use regular string literals instead.

♻️ Minor style fix
-                  openBrowser(`https://docs.exact.ly/exa-app/how-the-exa-app-works`).catch(reportError);
+                  openBrowser("https://docs.exact.ly/exa-app/how-the-exa-app-works").catch(reportError);
-                  openBrowser(`https://exact.ly/`).catch(reportError);
+                  openBrowser("https://exact.ly/").catch(reportError);

Also applies to: 27-27

src/components/card/CardDetails.tsx (1)

145-146: Consider including the NBSP in the translation key.

The non-breaking space (\u00A0) after "CVV" is placed outside the translation. For consistency and to give translators full control over spacing, consider including it in the translation key or using a different approach.

src/components/card/SpendingLimits.tsx (1)

57-61: Inline style violates coding guidelines.

The style={{ alignSelf: "center" }} on Pressable conflicts with the guideline: "Do not use inline styles with the React Native style prop using object literals." Use Tamagui's styled system props or wrap with a styled component.

Suggested fix using Tamagui layout
-              <Pressable onPress={onClose} style={{ alignSelf: "center" }} hitSlop={20}>
-                <Text emphasized footnote color="$interactiveTextBrandDefault">
-                  {t("Close")}
-                </Text>
-              </Pressable>
+              <XStack alignSelf="center">
+                <Pressable onPress={onClose} hitSlop={20}>
+                  <Text emphasized footnote color="$interactiveTextBrandDefault">
+                    {t("Close")}
+                  </Text>
+                </Pressable>
+              </XStack>

As per coding guidelines, all styling must go through Tamagui's styled system props.

src/components/home/card-upgrade/ActivateCard.tsx (1)

139-141: Consider using a shared semantic key for the disclaimer.

This long legal text is repeated in CardDisclaimer.tsx. Using a shared semantic key (e.g., t("card.visaDisclaimer")) would improve maintainability and ensure consistency across components.

src/components/pay-mode/ManualRepaymentSheet.tsx (1)

44-54: Consider extracting the penalty rate constant.

The Trans usage is correct. The hardcoded 0.0045 representing the daily penalty rate would benefit from being a named constant for clarity and maintainability.

src/components/card/CardDisclaimer.tsx (1)

80-82: Consider using semantic translation keys for long strings.

This long disclaimer string passed directly to t() is functional. Using a shorter semantic key (e.g., t("cardDisclaimer.visaNotice")) with the full text in translation files would improve readability.

src/components/pay-mode/Pay.tsx (1)

606-616: Consider extracting the percentage formatting logic.

The pattern of formatting a percentage and removing whitespace with .replaceAll(/\s+/g, "") is repeated. As noted in a previous review, consider extracting this into a helper function for DRY compliance.

Also applies to: 621-631

src/components/getting-started/GettingStarted.tsx (1)

222-222: Consider using i18next's pluralization feature.

Using separate translation keys for "One step left" and "Two steps left" doesn't scale well for languages with complex pluralization rules (e.g., Russian, Arabic).

♻️ Suggested refactor using pluralization
 <Text emphasized subHeadline color="$uiBrandTertiary">
-  {completedSteps > 1 ? t("One step left") : t("Two steps left")}
+  {t("stepsLeft", { count: 3 - completedSteps })}
 </Text>

Then in your translation files:

{
  "stepsLeft_one": "One step left",
  "stepsLeft_other": "{{count}} steps left"
}
src/components/home/GettingStarted.tsx (1)

72-73: Consider using explicit translation key mappings instead of dynamic lookup.

t(currentStep.title) assumes step titles exactly match translation keys. If step titles change without updating translations, this fails silently (displays the key as fallback). Consider using a mapping from step IDs to translation keys for more robust i18n.

♻️ Suggested improvement
+const stepTitleKeys: Record<string, string> = {
+  "add-funds": "Add funds to your account",
+  "verify-identity": "Verify your identity",
+};
+
-const activeStepTitle = currentStep ? t(currentStep.title) : "";
+const activeStepTitle = currentStep ? t(stepTitleKeys[currentStep.id] ?? currentStep.title) : "";

Or add a titleKey property to step objects in useOnboardingSteps for explicit key references.

src/components/add-funds/AddCrypto.tsx (1)

179-180: Consider moving the leading space into the translation.

Having {" "} as a separate JSX node is fragile and can be accidentally removed. Including the space in the translation value itself would be cleaner.

♻️ Suggested improvement
-              {" "}
-              {t("Learn more about adding funds.")}
+              {t(" Learn more about adding funds.")}

Or update the translation file to include the leading space in the value.

src/components/pay-mode/PaySelector.tsx (2)

222-228: Date formatting conflicts with the codebase's ISO date convention.

Based on learnings, the app intentionally uses ISO format (yyyy-MM-dd) for dates to ensure consistency across all locales. This change introduces locale-aware formatting which may display differently per user locale.

Consider reverting to ISO formatting while keeping the surrounding string translated:

♻️ Suggested change
 {t("First due date: {{date}} - then every 28 days.", {
-  date: new Date(firstMaturity * 1000).toLocaleDateString(language, {
-    year: "numeric",
-    month: "short",
-    day: "numeric",
-  }),
+  date: new Date(firstMaturity * 1000).toISOString().slice(0, 10),
 })}

124-128: Consider using a stable i18n key instead of the full sentence.

Using a long, punctuation-sensitive sentence as the i18nKey makes translations brittle. A stable key (e.g., "payMode.description") with the English string in the locale file would be more maintainable.

src/components/card/Card.tsx (1)

456-471: Missing translation key in en.json for the Trans component.

The i18nKey uses the full English sentence, but past reviews identified that this key is missing from en.json. Without the key in the English locale file, i18next will fail to resolve the <link> component placeholders, rendering them as literal text instead of clickable links for English users.

Ensure the key exists in en.json with the same <link></link> placeholder structure.

src/components/home/Home.tsx (1)

151-154: Long translation key acknowledged.

Using full English text as translation keys works and is consistent with the team's stated preference. The implementation is correct.

.changeset/better-radios-joke.md (1)

1-5: Changeset message doesn't match PR objectives.

The changeset describes "🐛 fix card 404 error handling" but this PR adds internationalization support. This mismatch will result in incorrect release notes. Based on learnings, the changeset should accurately reflect the i18n additions.

♻️ Suggested fix
 ---
 "@exactly/mobile": patch
 ---
 
-🐛 fix card 404 error handling
+✨ add internationalization support
src/components/auth/PasskeysAbout.tsx (1)

52-56: Trailing spaces embedded in template literals.

The pattern {${t("Strong credentials.")} } embeds visual spacing in JavaScript rather than using layout. Consider using margin props on the Text component or including spacing in the translation key for consistency.

♻️ Alternative using Tamagui margin
-                    <Text color="$uiNeutralSecondary" fontWeight="bold">
-                      {`${t("Strong credentials.")}   `}
-                    </Text>
+                    <Text color="$uiNeutralSecondary" fontWeight="bold" marginRight="$s1">
+                      {t("Strong credentials.")}
+                    </Text>

Apply the same pattern to lines 58-60 and 64-66.

src/components/activity/details/TransactionDetails.tsx (1)

24-27: Move useTranslation() hook before derived state.

The useTranslation() hook on line 27 is called after the non-hook assignment on line 26. Group all hooks together at the top of the component before derived state for better readability and consistency.

♻️ Suggested fix
   const toast = useToastController();
   const query = useQuery<ActivityItem>({ queryKey: ["activity", "details"] });
+  const { t } = useTranslation();
   const item = source ?? query.data;
-  const { t } = useTranslation();
src/components/add-funds/AssetSelectSheet.tsx (1)

70-70: Double translation of label prop.

Using t(label) translates the label prop, but callers may already pass a translated string (e.g., t("Select asset to send") from Bridge.tsx). This results in attempting to translate an already-translated string for non-English locales.

Either:

  1. Remove t() here and require callers to pass translated strings, or
  2. Pass translation keys (not translated strings) from callers and translate here
♻️ Option 1: Remove translation here (callers provide translated text)
         <Text fontSize={15} fontWeight="bold" textAlign="center">
-            {t(label)}
+            {label}
         </Text>
src/components/activity/details/OperationDetails.tsx (1)

110-110: Hardcoded ISO format is intentional.

The date (yyyy-MM-dd) and time (HH:mm:ss) formats remain hardcoded. Based on learnings, the Exa mobile app deliberately uses ISO format for transaction timestamps to ensure consistency across all locales.

Also applies to: 118-118

src/components/swaps/Success.tsx (1)

40-43: Use stable translation keys and add accessibility label to close button.

This was previously flagged: using English sentences as i18nKey values (e.g., "Swap request <em>sent</em>", "View pending request", "Close") is brittle for maintainability. Additionally, the close button Pressable (lines 70-72) lacks an accessibility label, which violates jsx-a11y/recommended rules.

Consider using stable keys like swap.success.requestSent with defaults prop for fallback text, and add aria-label={t("common.close")} to the close button.

Also applies to: 70-72, 80-83, 129-129, 138-138

src/components/home/Portfolio.tsx (1)

115-132: Translation key issue remains; accessibility improvements look good.

The accessibility attributes (aria-label, role="link") have been added to the protocol link, which addresses part of the previous feedback. However, using the full English sentence as i18nKey is still fragile for maintainability. Consider using a stable key like portfolio.performanceDisclaimer with the sentence in locale files.

src/components/activity/Activity.tsx (1)

59-63: Inline styles on FlatList - previously flagged.

The style={{ flex: 1 }} and contentContainerStyle with direct theme.*.val access were previously flagged. For FlatList, this may be an acceptable exception since Tamagui's styled props don't apply directly to these properties. Consider adding a comment documenting this exception.

src/components/shared/TransactionDetails.tsx (1)

86-88: Date and time formatting intentionally uses ISO format.

Based on learnings, the ISO format (yyyy-MM-dd for dates, HH:mm:ss for times) is a deliberate design choice for consistency across all locales. No changes needed here.

Also applies to: 94-96

src/components/auth/Auth.tsx (1)

276-298: Verify translation keys exist in both language files.

The page titles in this array are used as translation keys via t(title). The past review comment noted these keys were missing from en.json (while present in es.json). Please ensure these keys are added to both translation files for consistent behavior.

#!/bin/bash
# Verify that the Auth page title keys exist in both en.json and es.json
echo "=== Checking translation keys for Auth page titles ==="
for title in "Introducing the first onchain card" "Pay later in installments and hold your crypto" "Maximize earnings, effortlessly" "In-store QR payments, with crypto"; do
  echo "Key: \"$title\""
  echo -n "  en.json: "
  if rg -q "\"$title\"" src/i18n/en.json 2>/dev/null; then echo "✓ Found"; else echo "✗ Missing"; fi
  echo -n "  es.json: "
  if rg -q "\"$title\"" src/i18n/es.json 2>/dev/null; then echo "✓ Found"; else echo "✗ Missing"; fi
done
src/components/shared/ProcessingBalance.tsx (1)

26-26: Falsy check hides zero balances.

The condition if (!processingBalance) return null; treats 0 as falsy, hiding the component when processing balance is exactly zero. If displaying $0.00 is valid, use an explicit check.

Suggested fix
-  if (!processingBalance) return null;
+  if (processingBalance == null || processingBalance <= 0) return null;
src/components/add-funds/Bridge.tsx (1)

943-943: Verify double translation concern is resolved.

A past review flagged that AssetSelectSheet translates its label prop internally with {t(label)}. If that's still the case, passing t("Select asset to send") here causes double translation—the translated string is looked up as a key again.

Either pass the untranslated key and let AssetSelectSheet handle translation, or ensure AssetSelectSheet no longer calls t() on the label.

#!/bin/bash
# verify how AssetSelectSheet handles the label prop
rg -n "label" src/components/add-funds/AssetSelectSheet.tsx | head -20

Also applies to: 957-957

src/components/shared/ProcessingBalanceBanner.tsx (1)

26-26: Ambiguous falsy check hides banner when processingBalance === 0.

if (!processingBalance) return null treats 0 the same as undefined/null. If the intent is "show only when positive", make it explicit; if "hide only while loading", check for null/undefined.

🐛 Proposed fix
-  if (!processingBalance) return null;
+  if (processingBalance == null || processingBalance <= 0) return null;
src/i18n/en.json (1)

2-3: Plural keys are functionally correct but use sentence-like key names.

The _one/_other suffixes follow i18next pluralization conventions correctly. However, sentence-like keys (e.g., "Installments selected_one") are harder to maintain and prone to collisions. Structured keys like "installments.selected_one" are more scalable.

♻️ Suggested structured keys
-  "Installments selected_one": "{{count}} installment selected",
-  "Installments selected_other": "{{count}} installments selected",
+  "installments.selected_one": "{{count}} installment selected",
+  "installments.selected_other": "{{count}} installments selected",
   ...
-  "{{count}} step remaining_one": "{{count}} step remaining",
-  "{{count}} step remaining_other": "{{count}} steps remaining"
+  "steps.remaining_one": "{{count}} step remaining",
+  "steps.remaining_other": "{{count}} steps remaining"

Usage: t("installments.selected", { count }) and t("steps.remaining", { count }).

Also applies to: 10-11

src/components/loans/LoanSummary.tsx (1)

79-82: Potential NaN in amount display when borrow is undefined.

When isBorrow is true and the borrow query fails or returns no data (while isBorrowPending is false), borrow?.assets evaluates to undefined, and Number(undefined) / 1e6 yields NaN.

🐛 Proposed fix
             <Text title3>
-              {(
-                Number(!isBorrow && installments ? installments.amounts.reduce((a, b) => a + b, 0n) : borrow?.assets) /
-                1e6
-              ).toLocaleString(language, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+              {(() => {
+                const assets = !isBorrow && installments
+                  ? installments.amounts.reduce((a, b) => a + b, 0n)
+                  : borrow?.assets;
+                if (assets == null) return "N/A";
+                return (Number(assets) / 1e6).toLocaleString(language, {
+                  minimumFractionDigits: 2,
+                  maximumFractionDigits: 2,
+                });
+              })()}
             </Text>

@dieguezguille dieguezguille force-pushed the translations branch 2 times, most recently from cc8fd61 to 20d3113 Compare January 14, 2026 22:21
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/components/swaps/Success.tsx (1)

70-72: Add accessibility label to the close button.

The icon-only Pressable lacks an accessibility label, which is required for screen reader users. As per coding guidelines enforcing jsx-a11y/recommended, interactive elements should be labeled.

🔧 Proposed fix
-                <Pressable onPress={onClose}>
+                <Pressable onPress={onClose} aria-label={t("Close")}>
                   <X size={24} color="$uiNeutralPrimary" />
                 </Pressable>
src/components/loans/LoanSummary.tsx (1)

78-83: Potential NaN display when both installments and borrow are undefined.

When pending is false but both installments (for non-borrow case) and borrow?.assets are undefined/null, Number(undefined) / 1e6 evaluates to NaN, which will display in the UI.

Consider adding a guard similar to the APR calculation:

🐛 Proposed fix
             <Text title3>
-              {(
-                Number(!isBorrow && installments ? installments.amounts.reduce((a, b) => a + b, 0n) : borrow?.assets) /
-                1e6
-              ).toLocaleString(language, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+              {(() => {
+                const assets = !isBorrow && installments
+                  ? installments.amounts.reduce((a, b) => a + b, 0n)
+                  : borrow?.assets;
+                return assets != null
+                  ? (Number(assets) / 1e6).toLocaleString(language, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+                  : "N/A";
+              })()}
             </Text>
src/components/send-funds/Asset.tsx (1)

2-2: Use type import for Address.

Per coding guidelines, type-only imports should use import type { ... } syntax.

♻️ Suggested fix
-import { Address } from "@exactly/common/validation";
+import type { Address } from "@exactly/common/validation";
src/components/benefits/BenefitsSection.tsx (1)

15-49: Benefit content is not translated.

The BENEFITS array contains hardcoded English strings for title, subtitle, description, linkText, and buttonText. These user-facing strings will not be localized.

If the app supports multiple languages, wrap these strings with t() at render time or move the keys to translation files and reference them dynamically.

Example approach
 const BENEFITS = [
   {
     id: "pax",
-    partner: "Pax Assistance",
-    title: "30 days of free travel insurance",
-    subtitle: "Travel with peace of mind.",
-    description: "Copy your ID and get 30 days of travel insurance for free on Pax Assistance.",
+    partnerKey: "Pax Assistance",
+    titleKey: "30 days of free travel insurance",
+    subtitleKey: "Travel with peace of mind.",
+    descriptionKey: "Copy your ID and get 30 days of travel insurance for free on Pax Assistance.",
     logo: PaxLogo,
     url: "https://www.paxassistance.com/ar/capitas/exacardcap/",
   },
   // ...
 ];

// Then at render time:
{t(benefit.titleKey)}

Alternatively, ensure the translation JSON files contain entries for all these strings.

src/components/loans/Review.tsx (1)

283-286: Inconsistent locale parameter for number formatting.

This toLocaleString call uses undefined while all other formatting in this file uses language from i18n. This could result in inconsistent number formatting when the device locale differs from the app language.

🔧 Suggested fix
                     <Text title3 color="$uiNeutralPrimary">
                       {(Number(singleInstallment ? totalAmount : installmentsAmount) / 1e6).toLocaleString(
-                        undefined,
+                        language,
                         { minimumFractionDigits: 2, maximumFractionDigits: 2 },
                       )}
                     </Text>
🤖 Fix all issues with AI agents
In `@src/components/activity/details/ActivityDetails.tsx`:
- Around line 20-22: Reorder the React hooks in ActivityDetails.tsx so
useTranslation() is called before useQuery(): keep useRouter() first, then call
const { t } = useTranslation(), and then call const { data: item } =
useQuery<ActivityItem>({ queryKey: ["activity", "details"] }); to match the hook
order pattern used in other components like Contacts.tsx.

In `@src/components/add-funds/Bridge.tsx`:
- Around line 982-987: The handleError function is typed with a generic (key:
string) => string but should use i18next's TFunction to support interpolation
and namespaces; update the handleError signature to accept t: TFunction
(imported from 'i18next') and add the corresponding import, then adjust any
callers of handleError if needed to ensure they pass the i18next TFunction
(e.g., the same t used in components/hooks) so interpolated error messages will
type-check and work correctly.

In `@src/components/benefits/BenefitCard.tsx`:
- Line 31: The en.json localization file is missing English entries for the
benefit strings referenced by BenefitCard via t(benefit.subtitle) and other i18n
keys (e.g., the button labels and provider names used with t in BenefitCard and
related components); add explicit key/value pairs to en.json for each string
present in es.json — "30 days of free travel insurance", "Travel with peace of
mind", "20% OFF on eSims", "Stay connected everywhere", "Visa Signature
benefits", "Access exclusive discounts", "Learn more", "Get now", "Pax
Assistance", "Airalo", "Visa" — so that calling t(benefit.subtitle) and other
t(...) calls return proper English translations rather than relying on fallback
behavior.

In `@src/components/benefits/BenefitsSection.tsx`:
- Around line 76-80: The aria-label builds a translated string using a nested
t(benefit.partner) which is unnecessary and causes inconsistency; change the
binding passed into t(...) to use the raw brand name (benefit.partner) instead
of t(benefit.partner) so the call becomes aria-label={t("{{partner}}, page
{{current}} of {{total}}", { partner: benefit.partner, current: index + 1,
total: BENEFITS.length })}; update any tests or snapshots referencing the old
translated keys if needed and remove the redundant partner keys from translation
files to avoid mismatch.

In `@src/components/card/Card.tsx`:
- Around line 456-471: The i18n key used in the Trans component (the long
sentence mentioning "The Exa Card is issued by Third National Bank... Exactly
Protocol") exists only in the Spanish locale; add the same English translation
string to en.json under the corresponding key so the Trans call in Card.tsx
finds an English entry; ensure the English value matches the intended phrasing
and preserves the <link> placeholder so the components mapping (Text with
onPress opening https://exact.ly/) continues to work.

In `@src/components/swaps/SwapDetails.tsx`:
- Around line 29-31: The displayed exchange rate in SwapDetails uses
exchangeRate.toFixed(2) which isn't locale-aware; update the rendering of the
exchange rate (in the Text element referencing fromToken.symbol, exchangeRate,
and toToken.symbol) to use exchangeRate.toLocaleString with appropriate locale
and options (e.g., minimumFractionDigits/maximumFractionDigits) to match other
localized numeric formatting in the app so the value respects user locale and
formatting conventions.
♻️ Duplicate comments (45)
src/components/card/VerificationFailure.tsx (1)

38-42: Smart quote character in translation keys may cause lookup failures.

The curly apostrophe in "We couldn't verify your identity" can easily mismatch the JSON translation files if they use a straight apostrophe ('). Consider using stable key identifiers (e.g., "verificationFailure.title") or ensure exact character consistency across all locales.

src/components/shared/InstallmentSelector.tsx (1)

111-111: Phrase split across components breaks i18n word order.

The translation key "{{count}} installments of" ends with a preposition, with the amount rendered separately. This prevents translators from reordering words for languages with different grammatical structures.

Combine the full phrase into a single translation key:

-          {hasInstallments ? t("{{count}} installments of", { count: installment }) : t("Pay Now")}
-        </Text>
-      </XStack>
-      <XStack gap="$s2" alignItems="center" justifyContent="flex-end" flex={1}>
-        <AssetLogo
-          height={16}
-          source={{ uri: symbol ? assetLogos[symbol as keyof typeof assetLogos] : undefined }}
-          width={16}
-        />
-        <XStack>
-          {hasInstallments &&
-            (isInstallmentsPending || isBorrowPending ? (
-              <Skeleton height={18} />
-            ) : (
-              <Text headline numberOfLines={1} adjustsFontSizeToFit>
-                {(Number(installmentAmount) / 1e6).toLocaleString(language, {
-                  minimumFractionDigits: 2,
-                  maximumFractionDigits: 2,
-                })}
-              </Text>
-            ))}
+          {hasInstallments
+            ? isInstallmentsPending || isBorrowPending
+              ? t("{{count}} installments of", { count: installment })
+              : t("{{count}} installments of {{amount}}", {
+                  count: installment,
+                  amount: (Number(installmentAmount) / 1e6).toLocaleString(language, {
+                    minimumFractionDigits: 2,
+                    maximumFractionDigits: 2,
+                  }),
+                })
+            : t("Pay Now")}
+        </Text>
+      </XStack>
+      <XStack gap="$s2" alignItems="center" justifyContent="flex-end" flex={1}>
+        <AssetLogo
+          height={16}
+          source={{ uri: symbol ? assetLogos[symbol as keyof typeof assetLogos] : undefined }}
+          width={16}
+        />
+        <XStack>
+          {hasInstallments && (isInstallmentsPending || isBorrowPending) && <Skeleton height={18} />}
src/components/shared/AddressDialog.tsx (1)

53-53: Consider using structured translation keys.

The full-sentence keys (t("Network reminder"), t("Copy account address"), and the i18nKey in Trans) work but structured keys like t("addressDialog.title") or i18nKey="addressDialog.addFundsDisclaimer" are more maintainable and collision-resistant.

Also applies to: 71-71, 87-87

src/components/send-funds/Receiver.tsx (1)

161-161: Translation syntax is correct; hardcoded value already flagged.

The interpolation is properly implemented. The concern about the hardcoded minutes: 1 value was already raised in a previous review.

src/components/auth/PasskeysAbout.tsx (1)

52-70: Trailing spaces pattern should use layout spacing instead.

The pattern {${t("...")} } embeds visual spacing in JavaScript template literals on lines 53, 59, and 65. This should use Tamagui's margin props for consistent spacing across translations.

♻️ Suggested refactor using margin for spacing
                  <Text fontSize={16} color="$uiNeutralSecondary" textAlign="left">
-                    <Text color="$uiNeutralSecondary" fontWeight="bold">
-                      {`${t("Strong credentials.")}   `}
-                    </Text>
+                    <Text color="$uiNeutralSecondary" fontWeight="bold" marginRight="$s1">
+                      {t("Strong credentials.")}
+                    </Text>
                    {t("Every passkey is strong. They're never guessable, reused, or weak.")}
                  </Text>

Apply the same pattern to "Safe from server leaks." and "Safe from phishing." labels.

.changeset/better-radios-joke.md (1)

1-5: Changeset message doesn't reflect the PR's primary changes.

The PR is titled "✨ add i18n" and adds internationalization support, but the changeset describes "🐛 fix card 404 error handling". This will result in misleading release notes.

Based on learnings, changeset summaries should follow the format <emoji> <message> and accurately describe the shipped changes.

♻️ Suggested fix
 ---
 "@exactly/mobile": patch
 ---

-🐛 fix card 404 error handling
+✨ add internationalization support
src/components/shared/PluginUpgrade.tsx (1)

14-14: i18n integration looks correct.

The translation hook usage follows the established pattern in the codebase. Ensure the translation keys exist in both en.json and es.json files.

The missing English translations were already flagged in a previous review.

Also applies to: 24-24, 87-88

src/components/activity/details/PurchaseDetails.tsx (1)

91-99: Date/time formatting uses ISO format intentionally.

The hardcoded date ("yyyy-MM-dd") and time ("HH:mm:ss") formats are retained for consistency across all locales. Based on learnings, this is a deliberate design decision for the Exa mobile app to maintain uniform date/time display regardless of user locale.

src/components/card/CardPIN.tsx (1)

125-127: Consider using interpolation for the countdown in the translation key.

The countdown is rendered as a separate child, which prevents proper localization for languages that may need different word order (e.g., showing the countdown before "Hide PIN"). This concern was raised in a previous review iteration.

src/components/activity/details/PaymentDetails.tsx (1)

56-60: Redundant condition check inside already-guarded block.

The expression item.mode > 0 && (item as CreditActivity).borrow.rate is inside a block already guarded by {item.mode > 0 && (...)} (line 50). The inner item.mode > 0 check is redundant and causes Number() to receive a boolean (false) when the condition fails, though this path is unreachable.

♻️ Suggested simplification
-              {Number(item.mode > 0 && (item as CreditActivity).borrow.rate).toLocaleString(language, {
+              {(item as CreditActivity).borrow.rate.toLocaleString(language, {
src/components/card/CardDisclaimer.tsx (1)

80-82: Consider using a shorter semantic key for the long disclaimer.

This long string passed directly to t() was flagged in a previous review. While functional, using a semantic key like t("cardDisclaimer.visaNotice") with the full text in translation files would improve readability and make the component easier to maintain.

src/components/card/exa-card/CardContents.tsx (1)

82-86: Duplicated label JSX could be extracted.

The identical t("Available balance") label block appears in both the credit (lines 82-86) and debit (lines 103-107) branches. Extracting to a shared variable improves maintainability.

Also applies to: 103-107

src/components/card/CardDetails.tsx (1)

145-146: Non-breaking space placement outside translation key.

The \u00A0 after t("CVV") is placed outside the translation. For full translator control over spacing, consider including it in the translation value.

src/components/activity/details/OperationDetails.tsx (1)

110-110: Date/time formatting uses ISO format by design.

The hardcoded date (yyyy-MM-dd) and time (HH:mm:ss) formats are intentional. Based on learnings, the project deliberately uses ISO format for consistency across all locales rather than locale-aware formatting.

Also applies to: 118-118

src/components/shared/ProcessingBalance.tsx (1)

26-26: Ambiguous falsy check hides component when processingBalance === 0.

The condition !processingBalance returns true when processingBalance is 0, hiding the component. If displaying a zero balance is valid, use an explicit null check instead.

🐛 Proposed fix
-  if (!processingBalance) return null;
+  if (processingBalance == null || processingBalance <= 0) return null;
src/components/shared/ProcessingBalanceBanner.tsx (1)

26-26: Clarify the intent of the falsy check.

if (!processingBalance) returns null for both processingBalance === 0 and processingBalance === undefined. If the intent is "only show when > 0", make it explicit for readability.

♻️ Suggested tweak
-  if (!processingBalance) return null;
+  if (processingBalance == null || processingBalance <= 0) return null;
src/components/add-funds/AddCrypto.tsx (1)

179-181: Consider moving the leading space into the translation.

Having {" "} as a separate JSX node is fragile. Including the space in the translation key itself would be cleaner.

♻️ Suggested improvement
-              {" "}
-              {t("Learn more about adding funds.")}
+              {t(" Learn more about adding funds.")}

Or update the translation file to include the leading space in the value.

src/components/home/AssetChange.tsx (1)

9-9: Prefer XStack over View with flexDirection="row".

As per coding guidelines, use Tamagui's XStack for horizontal layout instead of View with flexDirection="row". Based on learnings.

src/components/add-funds/AssetSelectSheet.tsx (1)

70-70: Double translation of label prop.

The label prop is translated here with t(label), but callers (e.g., Bridge.tsx) may already pass a translated string like t("Select asset to send"). This results in attempting to translate an already-translated string, which is logically incorrect.

Either translate at the call site and render directly here, or pass untranslated keys and translate only here—not both.

♻️ Suggested fix (translate only at call site)
 <Text fontSize={15} fontWeight="bold" textAlign="center">
-  {t(label)}
+  {label}
 </Text>

Then ensure callers pass the translated string:

<AssetSelectSheet label={t("Select asset to send")} ... />
src/components/activity/details/TransactionDetails.tsx (1)

24-27: Move useTranslation() hook before derived state.

The useTranslation() hook on line 27 is called after the derived state assignment on line 26. Group all hooks together at the top of the component before derived state for better readability and consistency with React conventions.

♻️ Suggested fix
 export default function TransactionDetails({
   source,
 }: {
   source?: CreditActivity | DebitActivity | InstallmentsActivity;
 }) {
   const toast = useToastController();
   const query = useQuery<ActivityItem>({ queryKey: ["activity", "details"] });
+  const { t } = useTranslation();
   const item = source ?? query.data;
-  const { t } = useTranslation();
   return (
src/components/pay-mode/PayMode.tsx (1)

67-93: Trans component children not rendered - link text will be invisible.

The protocol and terms components are self-closing <Text ... /> elements. The Trans component injects the link text ("Exactly Protocol", "Terms and conditions") as children, but self-closing elements don't render children.

🐛 Proposed fix
                      protocol: (
                        <Text
                          cursor="pointer"
                          caption2
                          color="$interactiveOnDisabled"
                          textDecorationLine="underline"
                          onPress={() => {
                            openBrowser("https://exact.ly/").catch(reportError);
                          }}
-                        />
+                        >
+                          {null}
+                        </Text>
                      ),
                      terms: (
                        <Text
                          cursor="pointer"
                          caption2
                          color="$interactiveOnDisabled"
                          textDecorationLine="underline"
                          onPress={() => {
                            presentCollection("10544608").catch(reportError);
                          }}
-                        />
+                        >
+                          {null}
+                        </Text>
                      ),

Note: The {null} placeholder allows Trans to inject the actual text as children.

src/components/home/Portfolio.tsx (1)

115-132: Consider using a stable i18n key for the disclaimer text.

The Trans component correctly embeds the clickable protocol link, and the accessibility attributes (role="link", aria-label) are a good addition. However, using the full English sentence as i18nKey is fragile—any text change requires updating all locale files and the key itself.

Consider using a stable key like "portfolio.performanceDisclaimer" and storing the full sentence in the locale files.

♻️ Suggested approach
 <Trans
-  i18nKey="Performance is variable, not guaranteed, and powered by <protocol>Exactly Protocol</protocol>. Yields depend on protocol performance and network activity. Past performance does not guarantee future results."
+  i18nKey="portfolio.performanceDisclaimer"
   components={{
     protocol: (
       <Text

Then in your locale files:

{
  "portfolio": {
    "performanceDisclaimer": "Performance is variable, not guaranteed, and powered by <protocol>Exactly Protocol</protocol>. Yields depend on protocol performance and network activity. Past performance does not guarantee future results."
  }
}
src/components/home/card-upgrade/Intro.tsx (1)

17-18: Expired and potentially inconsistent upgrade deadline.

The deadline new Date(2025, 5, 21) (June 21, 2025) is hardcoded and already past as of January 2026. Per past review comments, this also conflicts with Home.tsx which displays "May 18th, 2025". Consider sourcing the deadline from a config or API to ensure consistency and allow updates without code changes.

src/components/shared/Error.tsx (1)

32-54: Missing footnote prop on interpolated link components.

The parent Text at line 31 has the footnote prop, but the child Text components for X and Discord links (lines 36-42 and 44-51) don't include it. This causes link text to render at default size instead of footnote size, creating visual inconsistency.

Proposed fix
                  x: (
                    <Text
+                     footnote
                      textDecorationLine="underline"
                      color="$interactiveBaseBrandDefault"
                      onPress={() => {
                        openBrowser("https://x.com/Exa_App").catch(reportError);
                      }}
                    />
                  ),
                  discord: (
                    <Text
+                     footnote
                      textDecorationLine="underline"
                      color="$interactiveBaseBrandDefault"
                      onPress={() => {
                        openBrowser("https://discord.gg/fBdVmbH38Y").catch(reportError);
                      }}
                    />
                  ),
src/components/home/HomeDisclaimer.tsx (1)

20-20: Use regular string literals for static URLs.

Lines 20 and 27 use template literals (backticks) for static strings that don't require interpolation. Per coding guidelines, prefer regular string literals for static values.

♻️ Minor style fix
               <DisclaimerLink
                 onPress={() => {
-                  openBrowser(`https://docs.exact.ly/exa-app/how-the-exa-app-works`).catch(reportError);
+                  openBrowser("https://docs.exact.ly/exa-app/how-the-exa-app-works").catch(reportError);
                 }}
               />
             ),
             protocol: (
               <DisclaimerLink
                 onPress={() => {
-                  openBrowser(`https://exact.ly/`).catch(reportError);
+                  openBrowser("https://exact.ly/").catch(reportError);
                 }}
               />

Also applies to: 27-27

src/components/shared/TransactionDetails.tsx (1)

86-88: Date/time formatting intentionally uses ISO format.

Based on learnings, the ISO format (yyyy-MM-dd, HH:mm:ss) is deliberately used for consistency across all locales in this codebase. No locale-aware formatting is needed here.

Also applies to: 94-96

src/utils/useAuth.ts (1)

60-64: Consider structured translation keys for maintainability.

Using full English sentences as keys (e.g., t("Authentication cancelled")) works but is fragile if source text changes. Structured keys like t("auth.cancelled") are more maintainable.

This is a nitpick — the current approach is valid and consistent.

Also applies to: 67-73

src/components/activity/Activity.tsx (1)

59-63: Inline styles on FlatList violate Tamagui guidelines.

The style={{ flex: 1 }} and contentContainerStyle with direct theme.*.val access deviate from Tamagui's styled system. For FlatList, this may be a necessary exception since Tamagui props don't apply directly to these properties.

Add a comment documenting this exception if intentional.

src/components/auth/Auth.tsx (1)

144-147: Add missing translation keys to en.json.

The t(title) call on line 146 uses the hardcoded title values from the pages array (lines 276-298) as translation keys. These keys need to exist in both translation files for proper i18n behavior. If they're missing from en.json, English users will see the raw key (which happens to be the English text), but this creates an inconsistent translation setup.

Also applies to: 276-298

src/components/home/GettingStarted.tsx (1)

72-73: Consider explicit translation key mapping for step titles.

Using t(currentStep.title) couples step titles directly to translation keys. While functional, changes to step titles in useOnboardingSteps without corresponding translation file updates could silently break translations.

src/components/card/SpendingLimits.tsx (1)

57-61: Inline style object violates coding guidelines.

The inline style={{ alignSelf: "center" }} on Pressable conflicts with the guideline: "Do not use inline styles with the React Native style prop using object literals."

Suggested fix using Tamagui layout
-              <Pressable onPress={onClose} style={{ alignSelf: "center" }} hitSlop={20}>
-                <Text emphasized footnote color="$interactiveTextBrandDefault">
-                  {t("Close")}
-                </Text>
-              </Pressable>
+              <XStack alignSelf="center">
+                <Pressable onPress={onClose} hitSlop={20}>
+                  <Text emphasized footnote color="$interactiveTextBrandDefault">
+                    {t("Close")}
+                  </Text>
+                </Pressable>
+              </XStack>
src/components/home/card-upgrade/ActivateCard.tsx (1)

139-141: Consider using a semantic key for the long disclaimer.

This long legal text appears to be duplicated with CardDisclaimer.tsx. Using a shared semantic key (e.g., t("card.visaDisclaimer")) would improve maintainability and ensure consistency.

src/components/home/AssetList.tsx (1)

29-34: Redundant useTranslation hook in child component.

AssetRow is rendered multiple times within .map() (line 108), and each instance calls useTranslation(). While react-i18next caches the translation function, consider passing t and language as props from the parent for cleaner architecture.

src/components/defi/AboutDefiSheet.tsx (1)

28-30: Consider shorter translation keys for long sentences.

Using the full sentence as the translation key works but can be harder to maintain. A shorter key like "about_defi_description" would be more manageable if translation files grow large. This is a stylistic choice; the current approach is functional.

src/components/loans/Receiver.tsx (1)

215-231: Consider simplifying the Trans i18nKey.

The translation key is the entire sentence with embedded markup indicators. This approach works but creates a very long key that's harder to manage in translation files. Consider using a shorter, descriptive key instead.

src/components/getting-started/GettingStarted.tsx (1)

220-223: Consider using i18next's pluralization feature.

Using separate translation keys for "One step left" and "Two steps left" is a common i18n anti-pattern. i18next supports pluralization natively, which handles varying grammatical rules across languages more gracefully.

♻️ Suggested refactor using pluralization
 <Text emphasized subHeadline color="$uiBrandTertiary">
-  {completedSteps > 1 ? t("One step left") : t("Two steps left")}
+  {t("stepsLeft", { count: 3 - completedSteps })}
 </Text>

Then define in translation files:

{
  "stepsLeft_one": "One step left",
  "stepsLeft_other": "{{count}} steps left"
}

This pattern scales better for languages with complex pluralization rules (e.g., Russian, Arabic).

src/components/send-funds/Amount.tsx (1)

289-294: Simplify the validation message.

The validation message always interpolates 0, making the interpolation unnecessary. A static key is clearer for translators.

Proposed fix
 check(
   (value) => {
     return value !== 0n;
   },
-  t("Amount cannot be {{amount}}", { amount: 0 }),
+  t("Amount cannot be zero"),
 ),
src/components/pay-mode/Pay.tsx (1)

608-616: Consider extracting the repeated percentage formatting logic.

The pattern of formatting a percentage and removing whitespace with .replaceAll(/\s+/g, "") is duplicated at lines 608-616 and 623-631.

Helper extraction
function formatCompactPercent(value: number, language: string): string {
  return Math.abs(value)
    .toLocaleString(language, {
      style: "percent",
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })
    .replaceAll(/\s+/g, "");
}

Then use: formatCompactPercent(discountOrPenaltyPercentage, language)

Also applies to: 623-631

src/components/pay-mode/ManualRepaymentSheet.tsx (1)

44-54: Correct Trans usage with interpolated values and styled components.

The locale-aware percentage formatting via toLocaleString(language, { style: "percent", ... }) is properly implemented.

Note: The magic number 0.0045 was already flagged in a previous review as a nitpick for extraction to a named constant.

src/i18n/en.json (1)

2-3: Plural forms follow i18next conventions.

The _one and _other suffixes are correctly applied for pluralization. The sentence-like key format concern was already raised in a previous review suggesting structured keys like "installments.selected".

Also applies to: 10-11

src/components/loans/Review.tsx (2)

325-330: Date formatting uses locale-aware format instead of ISO.

Based on learnings, the app intentionally uses ISO date/time formats across locales. This was already flagged in a previous review suggesting format(new Date(...), "yyyy-MM-dd") from date-fns.


247-253: Translation interpolations are correctly structured.

The {{variable}} syntax for interpolated values (rate, count, symbol) follows i18next conventions correctly.

Note: A previous review flagged potential missing translation keys in en.json. Ensure all keys used here are present in the English translation file.

Also applies to: 273-274, 378-382

src/components/pay-mode/PaySelector.tsx (2)

222-228: Verify: Date formatting uses locale-aware approach vs documented ISO convention.

Same concern as other files in this PR—this uses locale-aware formatting with month: "short" which conflicts with the documented ISO format convention. Confirm this is the intended direction for the i18n effort.


124-128: Consider using a stable i18n key for the Trans component.

Using the full English sentence as i18nKey makes translations brittle—any punctuation or wording change requires updating all locale files. A stable key like "payMode.description" with the English text in the locale file would be more maintainable.

src/components/add-funds/Bridge.tsx (1)

951-951: Double translation issue persists.

The AssetSelectSheet component expects an untranslated key for its label prop and translates it internally. Passing t("Select asset to send") here causes double translation. Pass the raw key instead:

 <AssetSelectSheet
-  label={t("Select asset to send")}
+  label="Select asset to send"

Same issue applies to line 965.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/components/loans/Maturity.tsx (1)

123-127: Consider using i18next pluralization for "installment(s)".

The current interpolation works but doesn't handle singular/plural forms. When count is 1, it would display "1 installments" instead of "1 installment".

♻️ Suggested pluralization approach

In your translation files, use i18next's pluralization:

{
  "Available for {{count}} installment or less": "Available for {{count}} installment or less",
  "Available for {{count}} installment or less_other": "Available for {{count}} installments or less"
}

Then use:

t("Available for {{count}} installment or less", { count: MAX_INSTALLMENTS - index })

i18next will automatically select the correct form based on count.

src/components/loans/Review.tsx (1)

283-286: Inconsistent locale parameter: use language instead of undefined.

This toLocaleString call passes undefined as the locale, while all other formatting calls in this file (Lines 238, 262, 311, 448) consistently use language. This inconsistency could lead to different number formatting between this value and others on the same screen.

Proposed fix
                      <Text title3 color="$uiNeutralPrimary">
                        {(Number(singleInstallment ? totalAmount : installmentsAmount) / 1e6).toLocaleString(
-                          undefined,
+                          language,
                          { minimumFractionDigits: 2, maximumFractionDigits: 2 },
                        )}
                      </Text>
src/components/send-funds/Receiver.tsx (1)

81-92: Avoid inline style prop with object literal.

Per coding guidelines, all styling must go through Tamagui's styled system props. The style prop on Line 89 uses an object literal, which should be converted to Tamagui props.

Proposed fix
                      <Input
                        flex={1}
                        placeholder={t("Enter {{chain}} address", { chain: chain.name })}
                        borderColor="$uiNeutralTertiary"
                        borderTopRightRadius={0}
                        borderBottomRightRadius={0}
                        value={value}
                        onChangeText={handleChange}
-                       style={{ borderColor: "$uiNeutralTertiary", borderWidth: 1, fontSize: 15 }}
+                       borderWidth={1}
+                       fontSize={15}
                        focusStyle={{ borderColor: "$borderBrandStrong", borderWidth: 1 }}
                        backgroundColor="$backgroundSoft"
                      />

Note: borderColor is already set via the prop on Line 84, so it can be removed from the inline style.

src/components/swaps/Success.tsx (1)

70-72: Add accessibility label to the close button.

The Pressable wrapping the X icon lacks an aria-label or accessibilityLabel, which violates jsx-a11y/recommended rules. Screen readers won't be able to identify this interactive element.

🔧 Proposed fix
-                <Pressable onPress={onClose}>
+                <Pressable onPress={onClose} aria-label={t("Close")}>
                   <X size={24} color="$uiNeutralPrimary" />
                 </Pressable>
src/components/loans/LoanSummary.tsx (1)

78-83: Potential NaN in amount display when data is unavailable.

If isBorrow is true but the borrow query is disabled (missing prerequisites), borrow will be undefined while isBorrowPending is false. This causes Number(borrow?.assets) to evaluate to NaN.

🐛 Proposed fix
             <Text title3>
-              {(
-                Number(!isBorrow && installments ? installments.amounts.reduce((a, b) => a + b, 0n) : borrow?.assets) /
-                1e6
-              ).toLocaleString(language, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+              {(() => {
+                const assets = !isBorrow && installments
+                  ? installments.amounts.reduce((a, b) => a + b, 0n)
+                  : borrow?.assets;
+                if (assets == null) return "N/A";
+                return (Number(assets) / 1e6).toLocaleString(language, {
+                  minimumFractionDigits: 2,
+                  maximumFractionDigits: 2,
+                });
+              })()}
             </Text>
src/components/swaps/Swaps.tsx (1)

369-375: Hardcoded gap value violates design token guidelines.

gap={10} should use a design token like gap="$s3" per coding guidelines.

🐛 Proposed fix
         <View
           padded
           flexDirection="row"
-          gap={10}
+          gap="$s3"
           paddingBottom="$s4"
           justifyContent="space-between"
           alignItems="center"

As per coding guidelines: "All styling must use predefined design tokens from tamagui.config.ts. Never use hardcoded values."

src/components/add-funds/Bridge.tsx (1)

950-965: Double translation causes incorrect i18n handling.

AssetSelectSheet expects an untranslated key for its label prop and translates it internally with {t(label)}. Passing already-translated strings like label={t("Select asset to send")} causes double translation—the second t() call attempts to look up the translated string as a key, which may work by accident but is incorrect.

🔧 Suggested fix

Pass the raw key string instead:

         <AssetSelectSheet
-          label={t("Select asset to send")}
+          label="Select asset to send"
           open={assetSheetOpen}
...
         <AssetSelectSheet
           hideBalances
-          label={t("Select asset to receive")}
+          label="Select asset to receive"
           open={destinationModalOpen}

Alternatively, update AssetSelectSheet to not translate internally if you want the caller to control translation.

🤖 Fix all issues with AI agents
In @.changeset/better-radios-joke.md:
- Around line 1-5: Update the changeset summary to end with a period: change the
first line summary from "🐛 fix card 404 error handling" to "🐛 fix card 404
error handling." to follow the lowercase imperative present-tense guideline (the
changeset documents the getCard() 404 handling, but only the trailing period
needs to be added).

In `@src/components/home/card-upgrade/VerifyIdentity.tsx`:
- Around line 69-71: Replace the inline full-sentence translation key in the
VerifyIdentity component (the t(...) call that currently uses the long sentence)
with a semantic key such as t("verifyIdentity.description"), update the
corresponding translation entries in your i18n resource files (all locales) to
include that key with the sentence as the value, and run a quick lint/i18n
extraction to ensure no missing keys remain; keep the component's t call and
surrounding JSX unchanged except for the key string.

In `@src/components/home/HomeActions.tsx`:
- Around line 26-29: The actions array in HomeActions (const actions = [...]) is
recreated on every render; wrap its creation with React's useMemo to memoize it
so it only recomputes when dependencies change (e.g., the translation function t
or any other props/state used). Import useMemo, replace the direct const actions
with useMemo(() => [...], [t]) (including ArrowDownToLine and ArrowUpRight in
the returned objects) so the array identity is stable across renders.

In `@src/components/home/VisaSignatureSheet.tsx`:
- Around line 138-140: Replace the long inline translation string in
VisaSignatureSheet.tsx with a structured translation key (e.g.,
"upgrade.deactivation_warning") and update the call that uses the i18n function
t to reference that key instead of the full sentence; then add the full sentence
as the value for "upgrade.deactivation_warning" in en.json (and mirror in other
locale files), and run your i18n extraction/validation so the new key is picked
up in translation tooling.

In `@src/components/loans/Installments.tsx`:
- Line 67: The English translation file is missing keys used in Installments.tsx
("Select your funding installment plan" and "Continue"); add these exact
key-value pairs to en.json so the i18n lookup for Installments (and any
component using those strings) returns the English text: add "Select your
funding installment plan": "Select your funding installment plan" and
"Continue": "Continue" to en.json.

In `@src/components/loans/LoanSummary.tsx`:
- Line 93: In LoanSummary replace the inline translation key that embeds
interpolation (the t("{{rate}} FIXED APR", { rate: apr }) call) with a
structured key (e.g., t("loan.fixed_apr", { rate: apr })) and add/modify the
corresponding entry in your i18n resource files (e.g., "loan.fixed_apr":
"{{rate}} FIXED APR" or localized variants) so the interpolation continues to
work; ensure you update any tests or usages that expect the old string and keep
the interpolation variable name (rate) consistent with the t(...) call.

In `@src/components/swaps/Swaps.tsx`:
- Around line 480-483: Replace the long inline translation strings in the Swaps
component (the t(...) calls that show the liquidation warning and collateral
risk acknowledgment) with structured i18n keys like "swap.liquidation_warning"
and "swap.collateral_risk_acknowledgment"; update the component to call
t("swap.liquidation_warning") and t("swap.collateral_risk_acknowledgment")
instead, and add the corresponding entries and English text to your translation
resource files so existing UI copy is preserved.

In `@src/utils/useAuth.ts`:
- Around line 42-47: The helper function handleError currently accepts t:
TFunction which couples callers to pass the translation function; instead import
i18next (or the i18n instance) into the module and replace uses of t(...) inside
handleError with i18next.t(...), remove the t parameter from handleError's
signature, and update all call sites that pass t (e.g., where handleError(...)
is invoked from useAuth hooks) to stop passing the extra arg; ensure
useToastController and onDomainError parameters remain unchanged and run
tests/build to verify no remaining references to the removed parameter.
♻️ Duplicate comments (46)
src/components/shared/AddressDialog.tsx (1)

53-53: Consider using structured translation keys.

Using full sentence strings as keys (e.g., t("Network reminder")) works, but structured keys like t("addressDialog.title") are easier to maintain and avoid collisions.

Also applies to: 87-87

src/components/loans/Receiver.tsx (1)

215-231: Long i18nKey in Trans component.

The concern about using the full sentence as the i18nKey was already raised in a previous review. Consider using a shorter, descriptive key (e.g., "externalAddressWarning") and defining the full sentence in the translation JSON files for better maintainability.

src/components/send-funds/Amount.tsx (1)

289-294: Consider simplifying the validation message key.

The interpolation t("Amount cannot be {{amount}}", { amount: 0 }) always passes 0. A direct key like t("Amount cannot be zero") would be clearer for translators and simpler to maintain.

Proposed simplification
                   onChange: pipe(
                     bigint(),
                     check(
                       (value) => {
                         return value !== 0n;
                       },
-                      t("Amount cannot be {{amount}}", { amount: 0 }),
+                      t("Amount cannot be zero"),
                     ),
src/components/auth/PasskeysAbout.tsx (1)

52-56: Trailing spaces embedded in template literals.

The pattern {${t("Strong credentials.")} } embeds visual spacing in JavaScript rather than the translation. This appears at lines 53, 59, and 65. Consider using Tamagui margins instead for consistent cross-language spacing.

♻️ Suggested approach using margins
-                    <Text color="$uiNeutralSecondary" fontWeight="bold">
-                      {`${t("Strong credentials.")}   `}
-                    </Text>
+                    <Text color="$uiNeutralSecondary" fontWeight="bold" marginRight="$s2">
+                      {t("Strong credentials.")}
+                    </Text>

Apply similarly to lines 59 and 65.

src/components/shared/InstallmentSelector.tsx (1)

111-111: Phrase splitting issue persists.

The translation key "{{count}} installments of" still ends with "of", with the amount rendered separately in lines 125-130. This breaks i18n best practices since the full phrase should be in a single translation key to allow proper word order in different languages.

src/components/home/AssetChange.tsx (1)

9-9: Previously suggested: Use XStack instead of View with flexDirection="row".

Per coding guidelines, Tamagui's XStack should be used for horizontal layouts instead of View with flexDirection="row".

src/components/activity/details/TransactionDetails.tsx (1)

24-27: Move useTranslation() hook before derived state.

The useTranslation() hook on line 27 is called after the derived state assignment on line 26. Group all hooks together before derived state for consistency with React conventions.

♻️ Suggested fix
 export default function TransactionDetails({
   source,
 }: {
   source?: CreditActivity | DebitActivity | InstallmentsActivity;
 }) {
   const toast = useToastController();
   const query = useQuery<ActivityItem>({ queryKey: ["activity", "details"] });
+  const { t } = useTranslation();
   const item = source ?? query.data;
-  const { t } = useTranslation();
   return (
src/components/card/SpendingLimits.tsx (1)

57-61: Inline style violates coding guidelines.

The style={{ alignSelf: "center" }} on Pressable conflicts with the guideline prohibiting inline styles with React Native's style prop. Wrap with a Tamagui layout component instead.

♻️ Suggested fix using Tamagui layout
-              <Pressable onPress={onClose} style={{ alignSelf: "center" }} hitSlop={20}>
-                <Text emphasized footnote color="$interactiveTextBrandDefault">
-                  {t("Close")}
-                </Text>
-              </Pressable>
+              <XStack alignSelf="center">
+                <Pressable onPress={onClose} hitSlop={20}>
+                  <Text emphasized footnote color="$interactiveTextBrandDefault">
+                    {t("Close")}
+                  </Text>
+                </Pressable>
+              </XStack>

As per coding guidelines, all styling must go through Tamagui's styled system props.

src/components/home/card-upgrade/ActivateCard.tsx (1)

139-141: Consider using a semantic key for the duplicated disclaimer.

This long legal disclaimer is repeated verbatim in CardDisclaimer.tsx. Using a shared semantic key (e.g., t("card.visaDisclaimer")) would improve maintainability and ensure consistency across both locations.

src/utils/useAuth.ts (1)

60-68: Consider using structured translation keys for maintainability.

Using full English sentences as translation keys works but can be fragile if the source text changes. Structured keys like t("auth.cancelled") or t("auth.passkeyBackupRequired") are more maintainable.

src/components/shared/Error.tsx (1)

32-54: Missing footnote prop on interpolated link components.

The parent Text at line 31 has the footnote prop, but the child Text components for the X and Discord links don't include it. This causes visual inconsistency with different text sizes.

src/components/shared/ProcessingBalance.tsx (1)

26-26: Ambiguous falsy check hides zero balance.

The condition !processingBalance returns null when processingBalance === 0, which may not be the intended behavior if you want to distinguish between "no data" and "zero balance".

src/components/home/HomeDisclaimer.tsx (1)

20-20: Prefer regular strings over template literals for static URLs.

Lines 20 and 27 use template literals for static strings. Per coding guidelines, use regular string literals when there's no interpolation.

♻️ Suggested fix
             app: (
               <DisclaimerLink
                 onPress={() => {
-                  openBrowser(`https://docs.exact.ly/exa-app/how-the-exa-app-works`).catch(reportError);
+                  openBrowser("https://docs.exact.ly/exa-app/how-the-exa-app-works").catch(reportError);
                 }}
               />
             ),
             protocol: (
               <DisclaimerLink
                 onPress={() => {
-                  openBrowser(`https://exact.ly/`).catch(reportError);
+                  openBrowser("https://exact.ly/").catch(reportError);
                 }}
               />
             ),

Also applies to: 27-27

src/components/home/AssetList.tsx (1)

29-33: Redundant useTranslation hook in child component.

AssetRow is rendered for each asset in the list (lines 108-110), and each instance calls useTranslation(). While react-i18next caches the translation function, this is unnecessarily repeated work. Consider lifting t and language to AssetList and passing them as props through AssetSection to AssetRow.

src/components/loans/Review.tsx (1)

325-329: Keep date formatting ISO (avoid locale-aware toLocaleDateString here).

Based on learnings, the app intentionally uses ISO date/time formats (yyyy-MM-dd) across all locales for consistency. This locale-aware date formatting deviates from that pattern.

src/components/shared/ProcessingBalanceBanner.tsx (1)

26-26: Make the "no banner" condition explicit (0 vs not loaded).

The current check if (!processingBalance) hides the banner for both processingBalance === 0 and processingBalance === undefined. If the intent is "only show when > 0", make that explicit; if the intent is "hide only while loading", check for null/undefined instead.

src/components/shared/PluginUpgrade.tsx (1)

87-88: Missing English translation keys in en.json.

As flagged in a previous review, ensure these translation keys are added to src/i18n/en.json:

  • "An account upgrade is required to access the latest features."
  • "Upgrade account now"

Without these entries, English users will see the raw keys or fallback behavior depending on i18n configuration.

#!/bin/bash
# Verify if the translation keys exist in en.json
echo "Checking en.json for required translation keys..."
rg -n "An account upgrade is required" src/i18n/en.json || echo "Key 'An account upgrade...' NOT FOUND in en.json"
rg -n "Upgrade account now" src/i18n/en.json || echo "Key 'Upgrade account now' NOT FOUND in en.json"

echo ""
echo "Checking es.json for comparison..."
rg -n "An account upgrade is required" src/i18n/es.json || echo "Key NOT FOUND in es.json"
rg -n "Upgrade account now" src/i18n/es.json || echo "Key NOT FOUND in es.json"
src/components/activity/details/PurchaseDetails.tsx (1)

91-91: Intentional ISO date/time format - no change needed.

The hardcoded "yyyy-MM-dd" and "HH:mm:ss" patterns are intentional per project convention to maintain consistent date/time display across all locales. Based on learnings, this is a deliberate design decision.

Also applies to: 99-99

src/components/benefits/BenefitCard.tsx (1)

16-16: Verify that all dynamic benefit keys exist in both translation files.

Using t(benefit.subtitle), t(benefit.title), etc. with dynamic values requires every possible string value to exist as a key in both en.json and es.json. Missing keys will cause the raw string to display instead of a translation.

#!/bin/bash
# Description: Check if benefit-related translation keys exist in both en.json and es.json

echo "=== Checking BenefitsSection for BENEFITS data ==="
rg -n "subtitle:|title:|partner:|linkText:" src/components/benefits/BenefitsSection.tsx

echo -e "\n=== Checking en.json for benefit keys ==="
for key in "30 days of free travel insurance" "Travel with peace of mind" "Pax Assistance" \
           "20% OFF on eSims" "Stay connected everywhere" "Airalo" \
           "Visa Signature benefits" "Access exclusive discounts" "Visa" \
           "Get now" "Learn more"; do
  if rg -q "\"$key\"" src/i18n/en.json 2>/dev/null; then
    echo "✓ en.json: $key"
  else
    echo "✗ en.json MISSING: $key"
  fi
done

echo -e "\n=== Checking es.json for benefit keys ==="
for key in "30 days of free travel insurance" "Travel with peace of mind" "Pax Assistance" \
           "20% OFF on eSims" "Stay connected everywhere" "Airalo" \
           "Visa Signature benefits" "Access exclusive discounts" "Visa" \
           "Get now" "Learn more"; do
  if rg -q "\"$key\"" src/i18n/es.json 2>/dev/null; then
    echo "✓ es.json: $key"
  else
    echo "✗ es.json MISSING: $key"
  fi
done

Also applies to: 31-31, 34-34, 41-41, 46-46

src/components/swaps/SwapDetails.tsx (1)

54-54: Consider locale-aware number formatting for consistency.

Other components in this PR extract language from useTranslation and use it with toLocaleString for numeric values. The hardcoded 0.025% and slippage percentage could benefit from the same treatment for consistency across the app.

Also applies to: 62-62

src/components/add-funds/AssetSelectSheet.tsx (1)

70-70: Double translation of label prop.

Using t(label) here will attempt to translate a string that may already be translated by the caller. If Bridge.tsx passes t("Select asset to send"), this results in translating an already-translated string. The lookup will fail and fall back to the translated string, which happens to work but is logically incorrect.

Consider either:

  1. Remove t() here and require callers to pass pre-translated strings, or
  2. Keep t() here and require callers to pass translation keys (not translated strings)

Pick one convention and apply it consistently.

src/components/activity/details/PaymentDetails.tsx (1)

56-60: Redundant condition inside guarded block.

This block is already guarded by item.mode > 0 (line 50), making the inner item.mode > 0 && check redundant. The Number() wrapper on a logical AND expression is also a code smell.

♻️ Suggested fix
-              {Number(item.mode > 0 && (item as CreditActivity).borrow.rate).toLocaleString(language, {
+              {(item as CreditActivity).borrow.rate.toLocaleString(language, {
src/components/card/CardPIN.tsx (1)

125-126: Countdown should use i18n interpolation for proper localization.

This pattern was flagged in a previous review. Splitting the button label and countdown into separate JSX children prevents proper translation of the full phrase—some languages need different word order or grammar when the count is included.

src/components/card/VerificationFailure.tsx (1)

38-41: Curly apostrophe in translation key may cause mismatch.

This was flagged in a previous review. The curly apostrophe (') in "We couldn't verify your identity" can cause key lookup failures if the JSON file uses a straight apostrophe ('). Consider using stable key identifiers or ensure consistent apostrophe usage.

src/components/home/Portfolio.tsx (1)

115-132: Accessibility attributes added to the embedded link.

The role="link" and aria-label attributes properly communicate link semantics to assistive technologies. The Trans component correctly handles the embedded interactive element.

The i18nKey still uses the full English sentence rather than a stable key (e.g., portfolio.performanceDisclaimer), which was noted in a previous review as a minor improvement for maintainability.

src/components/home/card-upgrade/Intro.tsx (1)

17-18: Hardcoded deadline date is already past (June 21, 2025).

The upgradeDeadline is hardcoded to June 21, 2025, which has already passed as of January 2026. Users will see an expired deadline in the UI. Source this date from configuration or an API, and consider gating the upgrade flow if the deadline has passed.

src/components/auth/Auth.tsx (1)

144-147: Add page title translation keys to en.json for consistency.

The page titles are used as translation keys via t(title) (line 146), but they may be missing from en.json while present in es.json. This creates an asymmetric translation setup where English falls back to the raw key string. Add the four page title keys to src/i18n/en.json to ensure explicit translation behavior.

Also applies to: 276-298

src/components/activity/Activity.tsx (1)

59-63: Inline styles and direct theme value access for FlatList.

The style={{ flex: 1 }} and contentContainerStyle with theme.backgroundMild.val access are exceptions to the Tamagui styling guidelines. For FlatList's contentContainerStyle, direct theme value access may be necessary since Tamagui props don't apply directly. Consider adding a comment documenting this exception, and move flex: 1 into the StyledFlatList definition if possible.

♻️ Suggested improvement
-const StyledFlatList = styled(FlatList<ActivityItemType>);
+const StyledFlatList = styled(FlatList<ActivityItemType>, {
+  flex: 1,
+});

And add a comment for the contentContainerStyle:

           ref={activityScrollReference}
-          style={{ flex: 1 }}
+          // flatlist contentContainerStyle requires direct theme value access
           contentContainerStyle={{
src/components/pay-mode/PayMode.tsx (1)

67-93: Critical: Self-closing Text components won't render Trans-injected children.

The Trans component injects the link text ("Exactly Protocol", "Terms and conditions") as children into the mapped components. The Text elements at lines 79 and 90 are self-closing (<Text ... />), which means they don't render children and the link text will be invisible.

🐛 Proposed fix: Render children in Text components
                      protocol: (
                        <Text
                          cursor="pointer"
                          caption2
                          color="$interactiveOnDisabled"
                          textDecorationLine="underline"
                          onPress={() => {
                            openBrowser("https://exact.ly/").catch(reportError);
                          }}
-                        />
+                        >
+                          {null}
+                        </Text>
                      ),
                      terms: (
                        <Text
                          cursor="pointer"
                          caption2
                          color="$interactiveOnDisabled"
                          textDecorationLine="underline"
                          onPress={() => {
                            presentCollection("10544608").catch(reportError);
                          }}
-                        />
+                        >
+                          {null}
+                        </Text>
                      ),

Note: The {null} placeholder allows Trans to inject the actual children. Alternatively, use an explicit children prop pattern if your Text component supports it.

src/components/home/GettingStarted.tsx (1)

72-73: Dynamic translation key coupling.

Using t(currentStep.title) couples step data directly to translation keys. If step titles change without updating translation files, translations will silently fail. Consider using explicit translation key mappings for better maintainability.

src/components/defi/AboutDefiSheet.tsx (1)

28-30: Consider shorter translation keys for long sentences.

Using full sentences as translation keys works but is harder to maintain as translation files grow. A shorter key like "about_defi_description" would be more manageable.

src/components/card/CardDetails.tsx (1)

145-146: Consider including the NBSP in the translation key.

The non-breaking space (\u00A0) after "CVV" is placed outside the translation. For consistency and to give translators full control over spacing, consider including it in the translation value.

src/components/pay-mode/ManualRepaymentSheet.tsx (1)

47-47: Consider extracting the penalty rate constant.

The 0.0045 daily penalty rate is an important business value. Extracting it to a named constant would improve readability.

src/components/add-funds/SupportedAssetsSheet.tsx (1)

73-88: Consider using <Trans> for the warning with embedded link.

The current approach uses two separate t() calls with &nbsp; for spacing. Using <Trans> would give translators full control over the placement and punctuation of the "Learn more" link within the warning text.

♻️ Suggested refactor
+import { Trans, useTranslation } from "react-i18next";
-import { useTranslation } from "react-i18next";

// In translation file:
// "addFunds.networkWarning": "Only send assets on {{chain}}. Sending funds from other networks may cause permanent loss.<learn> Learn more about adding funds.</learn>"

                <Text emphasized caption2 color="$uiNeutralPlaceholder" textAlign="justify">
-                  {t("Only send assets on {{chain}}. Sending funds from other networks may cause permanent loss.", {
-                    chain: chain.name,
-                  })}
-                  <Text ... >
-                    &nbsp;{t("Learn more about adding funds.")}
-                  </Text>
+                  <Trans
+                    i18nKey="Only send assets on {{chain}}. Sending funds from other networks may cause permanent loss.<learn> Learn more about adding funds.</learn>"
+                    values={{ chain: chain.name }}
+                    components={{
+                      learn: (
+                        <Text
+                          cursor="pointer"
+                          emphasized
+                          caption2
+                          color="$uiBrandSecondary"
+                          onPress={() => {
+                            presentArticle("8950801").catch(reportError);
+                          }}
+                        />
+                      ),
+                    }}
+                  />
                </Text>
src/components/card/CardDisclaimer.tsx (1)

79-83: Consider using a semantic key for the long disclaimer.

The full disclaimer text is used as the translation key. While functional, a shorter semantic key like t("cardDisclaimer.visaNotice") with the full text in translation files would improve readability and reduce risk of typos breaking the lookup.

src/components/pay-mode/Pay.tsx (1)

604-634: Consider extracting the repeated percentage formatting.

The percentage formatting with .replaceAll(/\s+/g, "") appears at lines 608-616 and 623-631. A helper like formatCompactPercent(value, language) would reduce duplication.

src/i18n/en.json (1)

2-3: Sentence-based keys may complicate maintenance.

As noted in previous reviews, using sentence fragments as translation keys (e.g., "Installments selected_one") can lead to collisions and makes refactoring harder. Structured keys like "installments.selected_one" are more maintainable. However, this is a team style preference.

Also applies to: 10-11

src/components/pay-mode/PaySelector.tsx (3)

222-228: Date formatting conflicts with repo's ISO date convention.

Based on learnings, the app intentionally uses ISO format (yyyy-MM-dd) for consistency across all locales. This locale-aware formatting may conflict with that decision.


124-128: Consider using a stable i18n key instead of the full sentence.

Using the full English sentence as the i18nKey makes translations brittle and harder to manage. A stable key like "payMode.description" with the English string in the locale file would be more maintainable.


278-288: Consider extracting APR calculation to a named helper function.

The APR calculation logic with nested ternaries could be more readable and testable as a separate function.

src/components/getting-started/GettingStarted.tsx (1)

220-227: Consider using i18next's pluralization feature for step count.

Using separate translation keys for "One step left" and "Two steps left" is a common i18n anti-pattern. i18next supports pluralization natively with _one and _other suffixes, which handles varying grammatical rules across languages more gracefully.

♻️ Suggested refactor
 <Text emphasized subHeadline color="$uiBrandTertiary">
-  {completedSteps > 1 ? t("One step left") : t("Two steps left")}
+  {t("stepsLeft", { count: 3 - completedSteps })}
 </Text>

Then in translation files:

{
  "stepsLeft_one": "One step left",
  "stepsLeft_other": "{{count}} steps left"
}
src/components/roll-debt/RollDebt.tsx (2)

154-161: Same interpolation issue for the second "due" occurrence.

Apply the same pattern here for consistency and translator flexibility.

♻️ Suggested fix
                  <Text secondary footnote textAlign="left">
-                    {t("due")}{" "}
-                    {new Date(borrowMaturity * 1000).toLocaleDateString(language, {
-                      year: "numeric",
-                      month: "short",
-                      day: "numeric",
-                    })}
+                    {t("due {{date}}", {
+                      date: new Date(borrowMaturity * 1000).toLocaleDateString(language, {
+                        year: "numeric",
+                        month: "short",
+                        day: "numeric",
+                      }),
+                    })}
                  </Text>

98-105: Use interpolation for better translator flexibility.

Separating t("due") from the date limits translators' ability to adjust word order. Some languages require the date before the label or use different grammatical structures.

♻️ Suggested fix
                  <Text secondary footnote textAlign="left">
-                    {t("due")}{" "}
-                    {new Date(Number(repayMaturity) * 1000).toLocaleDateString(language, {
-                      year: "numeric",
-                      month: "short",
-                      day: "numeric",
-                    })}
+                    {t("due {{date}}", {
+                      date: new Date(Number(repayMaturity) * 1000).toLocaleDateString(language, {
+                        year: "numeric",
+                        month: "short",
+                        day: "numeric",
+                      }),
+                    })}
                  </Text>

This allows translators to use patterns like "vence el {{date}}" or "{{date}} 到期".

src/components/add-funds/AddCrypto.tsx (1)

167-180: Translation integration looks good; space pattern is a known nitpick.

The warning text and learn more link are properly translated with interpolation. The {" "} space pattern between Text nodes was previously flagged as fragile but is a trivial nitpick.

src/components/card/exa-card/CardContents.tsx (1)

97-107: Duplicate label JSX pattern noted.

This is identical to lines 82-86. Extracting to a shared variable would improve maintainability but is a minor refactor.

src/components/pay-mode/PaymentSheet.tsx (1)

192-199: Date format conflicts with app's ISO-format convention.

Based on learnings, the app deliberately uses ISO format (yyyy-MM-dd) for date consistency across all locales. Using toLocaleDateString with month: "short" renders locale-dependent month names (e.g., "Jan" vs "ene."), which deviates from this pattern.

🔧 Suggested fix to use ISO format
+import { format } from "date-fns";
...
-                    {dueDate.toLocaleDateString(language, {
-                      year: "numeric",
-                      month: "short",
-                      day: "numeric",
-                    })}
+                    {format(dueDate, "yyyy-MM-dd")}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/components/swaps/TokenInput.tsx (1)

177-183: Inline style object violates coding guidelines.

As per coding guidelines, inline styles with the React Native style prop using object literals should be avoided. All styling must go through Tamagui's styled system props.

♻️ Suggested approach

Consider creating a styled Input component or moving these styles to Tamagui props:

-                      <Input
-                        value={value}
-                        onChangeText={handleAmountChange}
-                        onFocus={onFocus}
-                        disabled={disabled}
-                        cursor={disabled ? undefined : "pointer"}
-                        placeholder={token ? formatUnits(amount, token.decimals) : String(amount)}
-                        color={
-                          isDanger ? "$uiErrorSecondary" : isActive ? "$uiNeutralPrimary" : "$uiNeutralPlaceholder"
-                        }
-                        style={{
-                          fontFamily: "BDOGrotesk-Regular",
-                          fontSize: 28,
-                          fontWeight: "bold",
-                          lineHeight: 34,
-                          letterSpacing: -0.2,
-                        }}
-                        textAlign="left"
-                        inputMode="decimal"
-                        borderColor="transparent"
-                        numberOfLines={1}
-                        flex={1}
-                        width="100%"
-                      />
+                      <Input
+                        value={value}
+                        onChangeText={handleAmountChange}
+                        onFocus={onFocus}
+                        disabled={disabled}
+                        cursor={disabled ? undefined : "pointer"}
+                        placeholder={token ? formatUnits(amount, token.decimals) : String(amount)}
+                        color={
+                          isDanger ? "$uiErrorSecondary" : isActive ? "$uiNeutralPrimary" : "$uiNeutralPlaceholder"
+                        }
+                        fontFamily="BDOGrotesk-Regular"
+                        fontSize={28}
+                        fontWeight="bold"
+                        lineHeight={34}
+                        letterSpacing={-0.2}
+                        textAlign="left"
+                        inputMode="decimal"
+                        borderColor="transparent"
+                        numberOfLines={1}
+                        flex={1}
+                        width="100%"
+                      />
src/components/swaps/Success.tsx (1)

70-72: Missing accessibility label on close button.

The Pressable wrapping the X icon lacks an accessibility label, which violates jsx-a11y/recommended rules per coding guidelines.

♿ Suggested fix
                <Pressable onPress={onClose}>
-                  <X size={24} color="$uiNeutralPrimary" />
+                  <X size={24} color="$uiNeutralPrimary" aria-label={t("Close")} />
                </Pressable>
src/app/_layout.tsx (1)

57-88: Consider translating Sentry feedback integration strings.

The user feedback form strings (lines 61-65) are hardcoded in English:

  • "Send report error"
  • "Describe the issue"
  • "Send report"
  • "Cancel"

Since this configuration runs at module level before the React tree mounts, useTranslation() cannot be used directly. You could consider dynamically updating these strings after i18n initializes, or accept this as a known limitation.

src/components/benefits/BenefitsSection.tsx (1)

15-49: Untranslated user-facing strings in BENEFITS array.

The BENEFITS constant contains English strings for title, subtitle, and description that are rendered directly in the UI but are not wrapped with t(). These should be translated for full i18n support.

♻️ Suggested approach

Wrap the benefit content with translation calls when rendering:

// In BenefitCard or wherever these are rendered:
-<Text>{benefit.title}</Text>
+<Text>{t(benefit.title)}</Text>

And ensure these keys are added to the translation files (en.json, es.json).

src/components/send-funds/Receiver.tsx (1)

89-89: Avoid inline style objects.

Per coding guidelines, all styling should go through Tamagui's styled system props. The style prop with an object literal should be replaced with Tamagui props.

♻️ Proposed fix
                       <Input
                         flex={1}
                         placeholder={t("Enter {{chain}} address", { chain: chain.name })}
                         borderColor="$uiNeutralTertiary"
                         borderTopRightRadius={0}
                         borderBottomRightRadius={0}
                         value={value}
                         onChangeText={handleChange}
-                        style={{ borderColor: "$uiNeutralTertiary", borderWidth: 1, fontSize: 15 }}
+                        borderWidth={1}
+                        fontSize={15}
                         focusStyle={{ borderColor: "$borderBrandStrong", borderWidth: 1 }}
                         backgroundColor="$backgroundSoft"
                       />
src/components/loans/Review.tsx (1)

283-286: Inconsistent locale: use language instead of undefined.

Other toLocaleString calls in this file use the language variable, but this one passes undefined. For consistency across the app, use language here as well.

🐛 Proposed fix
                       <Text title3 color="$uiNeutralPrimary">
                         {(Number(singleInstallment ? totalAmount : installmentsAmount) / 1e6).toLocaleString(
-                          undefined,
+                          language,
                           { minimumFractionDigits: 2, maximumFractionDigits: 2 },
                         )}
                       </Text>
src/components/add-funds/Bridge.tsx (1)

951-966: Pass untranslated keys to AssetSelectSheet's label prop; the component handles translation internally.

AssetSelectSheet internally calls t(label) (line 70 of AssetSelectSheet.tsx), so passing label={t("Select asset to send")} causes double translation. The first t() call returns the translated string, and AssetSelectSheet then attempts to translate that string again, which fails.

Pass the raw translation keys instead:

label="Select asset to send"
label="Select asset to receive"
🤖 Fix all issues with AI agents
In `@src/app/_layout.tsx`:
- Around line 50-53: The custom language detector passed to configI18n does not
match i18next's detector interface; replace the inline object with a properly
typed LanguageDetectorModule (or annotate it) named e.g. languageDetector and
implement the required method signature (use lookup or detect with parameters)
such as detect: (detectionOrder?: string[], options?: Record<string, unknown>)
=> getLocales()[0]?.languageCode, then pass languageDetector to
.use(initReactI18next) before .init and keep reportError handling the same so
types align with i18next and the detector signature is correct.

In `@src/components/auth/PasskeysAbout.tsx`:
- Around line 42-44: The long translation passed to t(...) inside the
PasskeysAbout component should be split into shorter, manageable translation
keys or multiple t(...) calls (e.g., t('passkeys.intro.part1') and
t('passkeys.intro.part2')) so translators get clear, smaller strings; update the
JSX to use the new keys or concatenated t calls for the same output, and do the
same for the similar long string at the other occurrence (lines 67-69) so all
long sentences are replaced with compact translation keys.

In `@src/components/benefits/BenefitSheet.tsx`:
- Line 64: The EN translation file is missing keys for benefit texts referenced
by the BENEFITS constant used in BenefitSheet (e.g., benefit.partner,
benefit.title, benefit.longTitle, benefit.description); update en.json to
include the same translation keys/values present in es.json for all benefits
(Pax Assistance, 30 days of free travel insurance, Copy your ID..., Airalo, 20%
OFF on eSims, Stay connected around the world..., Visa, Visa Signature benefits,
Visa Signature Exa Card benefits, A world of benefits...) so that
t(benefit.partner)/t(benefit.title)/t(benefit.longTitle)/t(benefit.description)
resolve correctly for English.

In `@src/components/home/Portfolio.tsx`:
- Line 116: Replace the inline English sentence used as the i18n key with a
stable, descriptive key (e.g., "portfolio.performanceDisclaimer") in the i18n
prop of the component where i18nKey currently equals the full sentence; update
the locale message files to add the new key mapping to the existing English
string (and any other locales), and remove or alias the old sentence-based key
if present so translations remain consistent; locate the usage in Portfolio.tsx
(the element with i18nKey="Performance is variable, not guaranteed, and powered
by <protocol>Exactly Protocol</protocol>...") and change only the key string
while ensuring the translated content is preserved in your i18n resource files.
- Around line 115-132: The issue is that the protocol Text is self-closing so
Trans cannot inject the "Exactly Protocol" child; change the Text component used
in the Trans components prop "protocol" from self-closing to an opening and
closing tag and include the link text as its child (i.e., render "Exactly
Protocol" between <Text>...</Text>), keeping the existing props (cursor,
caption2, color, textDecorationLine, aria-label, role, onPress calling
openBrowser(...).catch(reportError)) intact so the link and accessibility
remain.

In `@src/components/swaps/Swaps.tsx`:
- Around line 491-506: The Trans component uses a missing i18nKey string for
English; add an entry to the English translations mapping for the exact i18nKey
string used in the Trans (the long sentence containing
"<link>LI.FI</link>...third-party protocols.") in the en.json translations
object so English users receive the same message (key and value should be
identical to the i18nKey string).
♻️ Duplicate comments (50)
src/utils/useAuth.ts (2)

42-47: Acknowledged: Past feedback on dependency injection approach.

The signature update to pass t: TFunction as a parameter is valid. The alternative approach of using i18next.t directly was suggested in a prior review.


60-68: Acknowledged: Past feedback on translation key style.

Using full English sentences as translation keys works with react-i18next. The suggestion to use structured keys like t("auth.cancelled") was already covered in a prior review.

src/components/home/HomeDisclaimer.tsx (1)

20-27: Use regular strings instead of template literals for static URLs.

Lines 20 and 27 use template literals (backticks) for static strings without interpolation.

♻️ Suggested fix
               <DisclaimerLink
                 onPress={() => {
-                  openBrowser(`https://docs.exact.ly/exa-app/how-the-exa-app-works`).catch(reportError);
+                  openBrowser("https://docs.exact.ly/exa-app/how-the-exa-app-works").catch(reportError);
                 }}
               />
             ),
             protocol: (
               <DisclaimerLink
                 onPress={() => {
-                  openBrowser(`https://exact.ly/`).catch(reportError);
+                  openBrowser("https://exact.ly/").catch(reportError);
                 }}
               />
src/components/send-funds/Amount.tsx (1)

289-294: Simplify the validation message key.

The interpolation { amount: 0 } always passes the same value, making the translation key unnecessarily complex for translators. A direct key would be clearer.

Proposed simplification
                    check(
                      (value) => {
                        return value !== 0n;
                      },
-                      t("Amount cannot be {{amount}}", { amount: 0 }),
+                      t("Amount cannot be zero"),
                    ),
.changeset/better-radios-joke.md (1)

1-5: Mismatch between changeset and PR scope already flagged.

Previous reviews noted that this changeset describes "fix card 404 error handling" while the PR is focused on i18n. This appears to be a separate fix bundled into the PR. Ensure release notes accurately reflect what's shipping.

src/components/auth/PasskeysAbout.tsx (1)

52-56: Trailing spaces in template literals should use layout-based spacing instead.

The pattern {`${t("Strong credentials.")} `} embeds visual spacing in JavaScript. This applies to all three bold label patterns in this file. Use margin or separate Text components for consistent cross-locale spacing.

Also applies to: 58-61, 63-69

src/components/home/AssetChange.tsx (1)

9-9: Prefer XStack over View with flexDirection="row".

As per coding guidelines, use Tamagui's XStack for horizontal layout instead of View with flexDirection="row".

src/components/benefits/BenefitCard.tsx (1)

3-3: LGTM! Translation hook properly integrated.

The i18n wiring is correct. The dynamic key pattern t(benefit.subtitle) works with i18next's fallback behavior. The conditional benefit.linkText ? t(benefit.linkText) : t("Get now") correctly handles the optional prop.

Note: The missing en.json entries for benefit strings was flagged in a previous review.

Also applies to: 16-16, 31-31, 34-34, 41-41, 46-46

src/components/activity/details/PurchaseDetails.tsx (1)

91-91: ISO date/time format is intentional — no change needed.

Based on learnings, the Exa mobile app deliberately uses ISO format (yyyy-MM-dd for dates, HH:mm:ss for times) for consistency across all locales rather than locale-aware formatting. The previous review suggestion for locale-aware dates can be disregarded.

Also applies to: 99-99

src/components/activity/details/PaymentDetails.tsx (1)

56-60: Redundant condition check inside already-guarded block.

The expression item.mode > 0 && (item as CreditActivity).borrow.rate is inside a block already guarded by {item.mode > 0 && (...)} (line 50). The inner item.mode > 0 check is redundant and evaluating a boolean in Number() is confusing.

♻️ Suggested simplification
-              {Number(item.mode > 0 && (item as CreditActivity).borrow.rate).toLocaleString(language, {
+              {(item as CreditActivity).borrow.rate.toLocaleString(language, {
src/components/activity/details/TransactionDetails.tsx (1)

24-27: Move useTranslation() hook before derived state.

All hooks should be grouped at the top of the component before derived state for consistency and to follow React hooks conventions. Move line 27 before line 26.

♻️ Suggested fix
   const toast = useToastController();
   const query = useQuery<ActivityItem>({ queryKey: ["activity", "details"] });
+  const { t } = useTranslation();
   const item = source ?? query.data;
-  const { t } = useTranslation();
src/components/card/VerificationFailure.tsx (1)

38-42: Curly apostrophe in translation key already flagged.

The use of curly quotes (') in the key "We couldn't verify your identity" can cause mismatches with JSON files. This concern was raised in a previous review.

src/components/home/GettingStarted.tsx (1)

72-73: Dynamic translation key coupling already flagged.

Using t(currentStep.title) couples step titles directly to translation keys, which can break silently if titles change without updating translations. This was raised in a previous review.

src/components/activity/Activity.tsx (1)

59-63: Inline styles and direct theme access already flagged.

Using style={{ flex: 1 }} and accessing theme.backgroundMild.val directly in contentContainerStyle violates Tamagui styling guidelines. This was raised in a previous review.

src/components/shared/AssetSelector.tsx (1)

89-90: aria-describedby should reference an element ID, not a string.

aria-describedby expects a reference to an element's id attribute, not a translated string. For providing a hint to screen readers in React Native, use accessibilityHint instead.

🔧 Suggested fix
 <ToggleGroup.Item
   aria-label={t("{{symbol}}, {{balance}} available", { symbol, balance })}
-  aria-describedby={t("Tap to select")}
+  accessibilityHint={t("Tap to select")}
   unstyled
src/components/getting-started/GettingStarted.tsx (1)

222-222: Consider i18next's pluralization for step counts.

Using separate keys for "One step left" and "Two steps left" works but doesn't scale well for languages with complex pluralization rules. i18next supports _one, _other suffixes for pluralized keys.

♻️ Suggested pluralization approach
 <Text emphasized subHeadline color="$uiBrandTertiary">
-  {completedSteps > 1 ? t("One step left") : t("Two steps left")}
+  {t("stepsLeft", { count: 3 - completedSteps })}
 </Text>

In translation files:

{
  "stepsLeft_one": "One step left",
  "stepsLeft_other": "{{count}} steps left"
}
src/i18n/en.json (1)

2-3: Pluralization keys are functional but could use structured naming.

The plural forms follow react-i18next conventions and will work correctly. However, as noted in a previous review, using structured keys like "installments.selected_one" and "steps.remaining_one" would be more maintainable than sentence-based keys.

Also applies to: 10-11

src/components/home/card-upgrade/Progression.tsx (1)

26-28: Pluralization key pattern previously discussed.

The use of {{count}} in the key for pluralization was raised in prior reviews. For proper i18n plural handling across languages, i18next typically expects static base keys with _one/_other suffixes (e.g., "step_remaining_one", "step_remaining_other"). This has been discussed with the team already.

src/components/activity/details/ActivityDetails.tsx (1)

20-22: Hook ordering inconsistency persists.

useTranslation (line 22) is still placed after useQuery (line 21). For consistency with the pattern established in other components like Contacts.tsx, consider placing useTranslation before useQuery.

♻️ Suggested reorder
 export default function ActivityDetails() {
   const router = useRouter();
+  const { t } = useTranslation();
   const { data: item } = useQuery<ActivityItem>({ queryKey: ["activity", "details"] });
-  const { t } = useTranslation();
   if (!item) return null;
src/components/shared/InstallmentSelector.tsx (1)

111-111: Avoid splitting grammatical phrases across components.

The translation key "{{count}} installments of" ends with "of", which means the amount rendered separately breaks the phrase. This prevents translators from reordering words as needed for different languages (e.g., some languages place the amount before "installments").

Include the full phrase with the amount interpolated:

- {hasInstallments ? t("{{count}} installments of", { count: installment }) : t("Pay Now")}
+ {hasInstallments
+   ? t("{{count}} installments of {{amount}}", {
+       count: installment,
+       amount: (Number(installmentAmount) / 1e6).toLocaleString(language, {
+         minimumFractionDigits: 2,
+         maximumFractionDigits: 2,
+       }),
+     })
+   : t("Pay Now")}

This requires adjusting the JSX structure to avoid rendering the amount separately in lines 125-130.

src/components/add-funds/AssetSelectSheet.tsx (1)

70-70: Double translation risk with t(label).

The label prop may already be translated at the call site (e.g., t("Select asset to send") in Bridge.tsx), and calling t(label) here attempts to translate an already-translated string. This is inconsistent with how other components handle label props.

Either:

  1. Receive untranslated keys and translate here with t(label), updating all callers to pass raw keys
  2. Receive pre-translated strings and render directly as {label}, keeping current caller behavior

Option 2 is simpler and consistent with the default value pattern:

- {t(label)}
+ {label}

Then ensure callers always pass t("...") when needed, or document that the default "Select asset" should be added to translation files and translated at the call site.

src/components/shared/Error.tsx (1)

32-54: Add footnote prop to interpolated link components for consistent styling.

The parent Text at line 31 has the footnote prop, but the child Text components for the X and Discord links don't include it. This causes the link text to render at default size instead of footnote size.

Proposed fix
                  x: (
                    <Text
+                     footnote
                      textDecorationLine="underline"
                      color="$interactiveBaseBrandDefault"
                      onPress={() => {
                        openBrowser("https://x.com/Exa_App").catch(reportError);
                      }}
                    />
                  ),
                  discord: (
                    <Text
+                     footnote
                      textDecorationLine="underline"
                      color="$interactiveBaseBrandDefault"
                      onPress={() => {
                        openBrowser("https://discord.gg/fBdVmbH38Y").catch(reportError);
                      }}
                    />
                  ),
src/components/card/SpendingLimits.tsx (1)

57-61: Inline style object violates coding guidelines.

The inline style={{ alignSelf: "center" }} on Pressable conflicts with the guideline against inline styles. Use Tamagui's styled system props or wrap with a layout component.

Suggested fix using Tamagui layout
-              <Pressable onPress={onClose} style={{ alignSelf: "center" }} hitSlop={20}>
-                <Text emphasized footnote color="$interactiveTextBrandDefault">
-                  {t("Close")}
-                </Text>
-              </Pressable>
+              <XStack alignSelf="center">
+                <Pressable onPress={onClose} hitSlop={20}>
+                  <Text emphasized footnote color="$interactiveTextBrandDefault">
+                    {t("Close")}
+                  </Text>
+                </Pressable>
+              </XStack>

As per coding guidelines, all styling must go through Tamagui's styled system props.

src/components/pay-mode/PayMode.tsx (1)

67-93: Critical: Self-closing Text components cannot render Trans-injected children.

The Trans component extracts text from the i18nKey placeholders ("Exactly Protocol" from <protocol>Exactly Protocol</protocol> and "Terms and conditions" from <terms>Terms and conditions</terms>) and attempts to inject it as children into the mapped components. Both protocol and terms are self-closing <Text ... /> elements that cannot accept children, so the link text will not render.

🐛 Proposed fix: Render children in Text components
                      protocol: (
                        <Text
                          cursor="pointer"
                          caption2
                          color="$interactiveOnDisabled"
                          textDecorationLine="underline"
                          onPress={() => {
                            openBrowser("https://exact.ly/").catch(reportError);
                          }}
-                        />
+                        >
+                          {children}
+                        </Text>
                      ),
                      terms: (
                        <Text
                          cursor="pointer"
                          caption2
                          color="$interactiveOnDisabled"
                          textDecorationLine="underline"
                          onPress={() => {
                            presentCollection("10544608").catch(reportError);
                          }}
-                        />
+                        >
+                          {children}
+                        </Text>
                      ),
src/components/card/CardPIN.tsx (1)

125-127: i18n pattern concern: countdown should be part of translated string.

The countdown is rendered as a separate element, preventing proper i18n for languages where the number might need different placement. This was flagged in a previous review.

src/components/home/card-upgrade/ActivateCard.tsx (1)

139-141: Consider using a shared semantic key for the legal disclaimer.

This long legal disclaimer text is duplicated between this file and CardDisclaimer.tsx. A shared translation key would improve maintainability.

src/components/swaps/SwapDetails.tsx (1)

58-68: Consider locale-aware formatting for swap fee and slippage percentages.

The swap fee 0.025% (line 59) and slippage calculation (line 67) use hardcoded/raw number formatting while the exchange rate uses toLocaleString. For consistency, these could also use locale-aware formatting.

src/components/benefits/BenefitsSection.tsx (1)

76-80: Verify the aria-label template key exists in en.json.

The translation template "{{partner}}, page {{current}} of {{total}}" must exist in both locale files. A previous review flagged this key as potentially missing from en.json.

src/components/auth/Auth.tsx (1)

144-147: Verify translation keys exist in en.json.

The pages array (lines 276-298) uses hardcoded English strings as translation keys (e.g., "Introducing the first onchain card"). Ensure these keys exist in en.json with their English values to maintain symmetric translation behavior, rather than relying on key fallback.

#!/bin/bash
# Verify translation keys exist in both language files
echo "=== Checking en.json for page titles ==="
for title in "Introducing the first onchain card" "Pay later in installments and hold your crypto" "Maximize earnings, effortlessly" "In-store QR payments, with crypto"; do
  if rg -q "\"$title\"" src/i18n/en.json 2>/dev/null; then
    echo "✓ Found in en.json: $title"
  else
    echo "✗ Missing in en.json: $title"
  fi
done

echo -e "\n=== Checking es.json for page titles ==="
for title in "Introducing the first onchain card" "Pay later in installments and hold your crypto" "Maximize earnings, effortlessly" "In-store QR payments, with crypto"; do
  if rg -q "\"$title\"" src/i18n/es.json 2>/dev/null; then
    echo "✓ Found in es.json: $title"
  else
    echo "✗ Missing in es.json: $title"
  fi
done
src/components/card/exa-card/CardContents.tsx (1)

82-86: Extract duplicated label to reduce repetition.

The "Available balance" label JSX is identical in both credit (lines 82-86) and debit (lines 103-107) branches. Extract it to a shared variable for maintainability.

♻️ Suggested refactor
const balanceLabel = (
  <View>
    <Text color="white" emphasized caption maxFontSizeMultiplier={1} textTransform="uppercase">
      {t("Available balance")}
    </Text>
  </View>
);

Then use {balanceLabel} in both branches.

Also applies to: 103-107

src/components/shared/AddressDialog.tsx (1)

53-53: Consider using structured translation keys.

Using full sentence strings as keys (e.g., t("Network reminder")) works, but structured keys like t("addressDialog.title") are easier to maintain and avoid collisions.

src/components/pay-mode/ManualRepaymentSheet.tsx (1)

44-54: Extract the penalty rate as a named constant.

The hardcoded 0.0045 is a magic number representing the daily penalty rate. Extracting it to a named constant improves readability and maintainability.

💡 Suggested improvement
+const DAILY_PENALTY_RATE = 0.0045;
+
 export default function ManualRepaymentSheet({

Then use:

-                        rate: (0.0045).toLocaleString(language, {
+                        rate: DAILY_PENALTY_RATE.toLocaleString(language, {
src/components/home/AssetList.tsx (1)

29-33: Redundant useTranslation hook in child component.

AssetRow is rendered multiple times within .map() (Line 108), and each instance calls useTranslation(). While react-i18next caches the translation function, invoking the hook in a frequently-rendered child is unnecessary when the parent (AssetList) already has access to t and language. Consider passing these as props.

♻️ Suggested refactor
-function AssetRow({ asset }: { asset: AssetItem }) {
-  const {
-    t,
-    i18n: { language },
-  } = useTranslation();
+function AssetRow({ asset, t, language }: { asset: AssetItem; t: (key: string) => string; language: string }) {

Update usage in AssetList:

+  const {
+    t,
+    i18n: { language },
+  } = useTranslation();
   // ...
   {assets.map((asset) => (
-    <AssetRow key={asset.symbol} asset={asset} />
+    <AssetRow key={asset.symbol} asset={asset} t={t} language={language} />
   ))}
src/components/card/CardDetails.tsx (1)

145-146: Consider including the NBSP in the translation key.

The non-breaking space (\u00A0) after "CVV" is placed outside the translation. For consistency and to give translators full control over spacing, consider including it in the translation key. Alternatively, keep the current approach if the NBSP serves a specific layout purpose that should remain consistent across all locales.

src/components/card/CardDisclaimer.tsx (1)

80-82: Consider using a semantic translation key for the long disclaimer.

This long string passed directly to t() reduces maintainability. A semantic key like t("cardDisclaimer.visaNotice") with the full text in translation files would improve readability.

src/components/add-funds/SupportedAssetsSheet.tsx (1)

73-88: Consider using <Trans> for the warning + link pattern.

The &nbsp; between the warning text and "Learn more" link is hardcoded outside the translation. A previous review suggested using <Trans> to give translators control over spacing and punctuation between these elements.

src/components/home/VisaSignatureSheet.tsx (1)

138-140: Long translation key pattern.

This lengthy sentence as a translation key was flagged in a previous review. The suggestion to use a structured key like "upgrade.deactivation_warning" remains valid for maintainability.

src/components/pay-mode/Pay.tsx (1)

608-616: Consider extracting repeated percentage formatting logic.

This pattern of formatting a percentage and removing whitespace with .replaceAll(/\s+/g, "") is repeated. A previous review suggested extracting this into a helper function for DRY compliance.

Also applies to: 623-631

src/components/loans/Review.tsx (2)

378-382: Verify English translation keys exist.

Based on past review comments, these translation keys may be missing from en.json. Since English is the fallback language, users will see raw key strings if the translations aren't present.

#!/bin/bash
# Check if the translation keys exist in en.json
echo "=== Checking en.json for Review.tsx translation keys ==="
rg -F "Confirm and receive" src/i18n/en.json || echo "Key 'Confirm and receive' not found"
rg -F "Confirm and borrow" src/i18n/en.json || echo "Key 'Confirm and borrow' not found"
rg -F "Fixed {{rate}} APR" src/i18n/en.json || echo "Key 'Fixed {{rate}} APR' not found"

325-329: Keep date formatting ISO per project convention.

Based on learnings, the Exa mobile app uses ISO format (yyyy-MM-dd) for dates for consistency across all locales, rather than locale-aware formatting.

🐛 Proposed fix using date-fns
+import { format } from "date-fns";
 // ...
                 <Text headline color="$uiNeutralPrimary">
-                  {new Date(Number(maturity) * 1000).toLocaleDateString(language, {
-                    year: "numeric",
-                    month: "short",
-                    day: "numeric",
-                  })}
+                  {format(new Date(Number(maturity) * 1000), "yyyy-MM-dd")}
                 </Text>
src/components/card/Card.tsx (1)

456-471: Verify English translation key exists for Trans component.

This Trans component uses an i18nKey that may be missing from en.json. Since English is the fallback language, the <link> placeholder tags will render as literal text instead of interactive components if the key isn't present.

#!/bin/bash
# Check if the Trans i18nKey exists in en.json
echo "=== Checking en.json for Card.tsx Trans key ==="
rg -F "The Exa Card is issued by Third National Bank" src/i18n/en.json || echo "Trans key not found in en.json"
rg -F "The Exa Card is issued by Third National Bank" src/i18n/es.json || echo "Trans key not found in es.json"
src/components/loans/LoanSummary.tsx (2)

93-93: Consider using a structured translation key for APR display.

The key "{{rate}} FIXED APR" embeds interpolation syntax in the key itself. A structured key like "loan.fixed_apr" would be more maintainable and easier to manage in translation files.


79-82: Guard against NaN when borrow?.assets is undefined.

When pending is false but borrow is undefined (e.g., for non-single-installment loans without installment data), Number(borrow?.assets) yields NaN, which will display as "NaN" in the UI.

🐛 Proposed fix
             <Text title3>
-              {(
-                Number(!isBorrow && installments ? installments.amounts.reduce((a, b) => a + b, 0n) : borrow?.assets) /
-                1e6
-              ).toLocaleString(language, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+              {(() => {
+                const assets =
+                  !isBorrow && installments
+                    ? installments.amounts.reduce((a, b) => a + b, 0n)
+                    : borrow?.assets;
+                if (assets == null) return "—";
+                return (Number(assets) / 1e6).toLocaleString(language, {
+                  minimumFractionDigits: 2,
+                  maximumFractionDigits: 2,
+                });
+              })()}
             </Text>
src/components/send-funds/Receiver.tsx (1)

161-161: Consider whether the arrival time should be dynamic.

The minutes interpolation value is hardcoded to 1. If the arrival time could vary based on network conditions, consider making this dynamic.

src/components/swaps/Swaps.tsx (1)

480-483: Consider structured translation keys for warning messages.

These lengthy sentences as translation keys can be harder to maintain. Consider structured keys like "swap.liquidation_warning" and "swap.collateral_risk_acknowledgment".

src/components/shared/ProcessingBalanceBanner.tsx (1)

26-26: Clarify the intent of the falsy check for processingBalance.

The check if (!processingBalance) return null; hides the banner both when loading (undefined) and when the balance is 0. If the intent is "show only when > 0", make it explicit; if it's "hide only while loading", use a nullish check.

🔧 Suggested fix if intent is to show only for positive balances
-  if (!processingBalance) return null;
+  if (processingBalance == null || processingBalance <= 0) return null;
src/components/pay-mode/PaySelector.tsx (2)

222-228: Date formatting conflicts with the repo's ISO date policy.

Based on learnings, the Exa mobile app deliberately uses ISO format (yyyy-MM-dd) for consistency across all locales. This locale-aware toLocaleDateString call conflicts with that decision.

🔧 Proposed fix to use ISO format
 {t("First due date: {{date}} - then every 28 days.", {
-  date: new Date(firstMaturity * 1000).toLocaleDateString(language, {
-    year: "numeric",
-    month: "short",
-    day: "numeric",
-  }),
+  date: new Date(firstMaturity * 1000).toISOString().slice(0, 10),
 })}

124-128: Consider using a stable i18n key instead of the full sentence.

Using the entire English sentence as the i18nKey makes translations brittle and harder to manage. A stable key like "payMode.description" would be cleaner.

src/components/loans/Receiver.tsx (1)

215-231: Consider using a stable i18n key for the Trans component.

Using the entire English sentence (including markup indicators) as the i18nKey is fragile. A shorter key like "externalAddressWarning" would be cleaner and easier to manage in translation files.

src/components/add-funds/AddCrypto.tsx (1)

179-180: Consider moving the leading space into the translation.

Having {" "} as a separate JSX node is fragile. Including the space in the translation string itself would be cleaner.

♻️ Suggested improvement
-              {" "}
-              {t("Learn more about adding funds.")}
+              {t(" Learn more about adding funds.")}

Or update the translation file to include the leading space in the value.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/components/shared/TransactionDetails.tsx (1)

68-72: Add explicit type guard for hash inside callback.

Per project conventions, conditional rendering ({hash && ...}) doesn't guarantee type narrowing inside callback handlers. Add an explicit guard to ensure type safety. Based on learnings, this pattern prevents reliance on render-time narrowing.

♻️ Proposed fix
 onPress={(event) => {
   event.stopPropagation();
+  if (!hash) return;
   const explorerUrl = chain.blockExplorers?.default.url;
   if (!explorerUrl) return;
   openBrowser(`${explorerUrl}/tx/${hash}`).catch(reportError);
 }}
src/components/home/SpendingLimitsSheet.tsx (1)

45-72: Consider using a single translation key for the conditional phrase.

The phrase "WHEN [badge] IS ENABLED" is split across multiple t() calls (t("WHEN"), t("IS ENABLED")), which can cause word order issues in some languages.

Consider using Trans component with placeholders for the badge:

<Trans i18nKey="When {{mode}} is enabled" components={{ badge: <Badge>...</Badge> }} />

This allows translators to reorder the sentence structure as needed for their language.

Also applies to: 82-109

src/components/swaps/Swaps.tsx (1)

283-286: Inconsistent locale parameter - uses undefined instead of language.

This toLocaleString call uses undefined for the locale while other numeric formatting in this file (and SwapDetails.tsx) uses language. For consistency across the app, pass the language variable here.

🔧 Suggested fix
                      <Text title3 color="$uiNeutralPrimary">
                        {(Number(singleInstallment ? totalAmount : installmentsAmount) / 1e6).toLocaleString(
-                          undefined,
+                          language,
                          { minimumFractionDigits: 2, maximumFractionDigits: 2 },
                        )}
                      </Text>

Note: This requires extracting language from i18n at line 70:

-const { t } = useTranslation();
+const {
+  t,
+  i18n: { language },
+} = useTranslation();
src/components/loans/Review.tsx (1)

283-286: Inconsistent locale parameter - uses undefined instead of language.

This toLocaleString call uses undefined while other calls in this file (lines 238, 262, 311, 448) use language. For consistency, pass language here.

🔧 Suggested fix
                      <Text title3 color="$uiNeutralPrimary">
                        {(Number(singleInstallment ? totalAmount : installmentsAmount) / 1e6).toLocaleString(
-                          undefined,
+                          language,
                          { minimumFractionDigits: 2, maximumFractionDigits: 2 },
                        )}
                      </Text>
src/components/add-funds/AssetSelectSheet.tsx (1)

151-162: Consider simplifying the fraction digits calculation.

The dynamic maximumFractionDigits calculation is complex. While functional, it could benefit from extraction into a helper function for readability and reusability.

♻️ Optional: Extract formatting logic
// In a utils file
function getOptimalFractionDigits(balance: bigint, decimals: number): number {
  return Math.min(
    8,
    Math.max(0, decimals - Math.ceil(Math.log10(Math.max(1, Number(balance) / 1e18))))
  );
}

Then use:

 {`${Number(formatUnits(balance, token.decimals)).toLocaleString(language, {
   minimumFractionDigits: 0,
-  maximumFractionDigits: Math.min(
-    8,
-    Math.max(
-      0,
-      token.decimals - Math.ceil(Math.log10(Math.max(1, Number(balance) / 1e18))),
-    ),
-  ),
+  maximumFractionDigits: getOptimalFractionDigits(balance, token.decimals),
   useGrouping: false,
 })} ${token.symbol}`}
src/components/activity/PendingProposals.tsx (1)

264-267: Store renderMaturity() result to avoid duplicate computation.

renderMaturity() is called twice—once for the conditional check and once to render—which unnecessarily duplicates the date formatting logic.

♻️ Suggested refactor
+  const maturityDisplay = renderMaturity();
+
   return (
     <XStack gap="$s4" paddingVertical="$s3">
       ...
-          {renderMaturity() && (
+          {maturityDisplay && (
             <Text footnote maxFontSizeMultiplier={1} color="$uiNeutralSecondary" numberOfLines={1}>
-              {renderMaturity()}
+              {maturityDisplay}
             </Text>
           )}
🤖 Fix all issues with AI agents
In `@src/components/benefits/BenefitsSection.tsx`:
- Around line 76-80: The aria-label key used in BenefitsSection.tsx
(t("{{partner}}, page {{current}} of {{total}}", {...})) is missing from
en.json; add that translation entry to en.json with the same interpolation
placeholders (e.g. key: "{{partner}}, page {{current}} of {{total}}" and value:
"{{partner}}, page {{current}} of {{total}}") so English users see a proper
localized label and the placeholders partner, current, and total match exactly.

In `@src/components/loans/Installments.tsx`:
- Line 67: Add the missing English translations for the keys used in
Installments.tsx by adding entries for "Select your funding installment plan"
and "Continue" to src/i18n/en.json with their English strings (e.g., "Select
your funding installment plan": "Select your funding installment plan",
"Continue": "Continue") so Installments.tsx and other components using those
keys display proper English text.

In `@src/components/loans/LoanSummary.tsx`:
- Line 29: The timestamp is only computed once on mount because timestamp is
created with useMemo(..., []), so it won't reflect time passing across a loan
maturity boundary; either compute Date.now()/1000 on each render by removing
useMemo or, if you need periodic updates for long-lived views, replace the
static memo with a stateful timestamp in LoanSummary updated via a
useEffect/setInterval (e.g., refresh every minute) and clear the interval on
unmount so the UI updates correctly across boundaries.

In `@src/components/pay-mode/ManualRepaymentSheet.tsx`:
- Around line 44-54: Extract the hard-coded penalty magic number 0.0045 into a
named constant (e.g., DAILY_PENALTY_RATE or PENALTY_RATE) and use that constant
in the Trans component's values calculation instead of the literal; update the
file-level or module-level scope in ManualRepaymentSheet.tsx so the constant is
easily discoverable and (optionally) exported, and keep the existing
toLocaleString(language, { style: "percent", minimumFractionDigits: 2,
maximumFractionDigits: 2 }) call unchanged while replacing the literal with the
new constant.

In `@src/components/shared/Success.tsx`:
- Around line 81-87: Replace the locale-dependent date formatting in the Success
component where t("Due {{date}}", { date: new Date(Number(maturity) *
1000).toLocaleDateString(...) }) is used with an ISO yyyy-MM-dd string; compute
the date as new Date(Number(maturity) * 1000).toISOString().slice(0,10) (or a
small helper that returns yyyy-MM-dd) and pass that ISO string into the t(...)
call so the displayed date matches the project's ISO format convention.
♻️ Duplicate comments (31)
.changeset/better-radios-joke.md (1)

1-5: Previous feedback still applies.

The past review comments already flagged that this changeset message may not fully represent the PR's i18n scope and that the summary should end with a period. Please address those comments.

src/components/shared/AssetSelector.tsx (1)

89-90: Incorrect usage of aria-describedby.

The aria-label on line 89 is correct. However, aria-describedby expects an element ID reference, not a translated string. For React Native components, use accessibilityHint to describe the result of performing an action.

🔧 Suggested fix
-              aria-label={t("{{symbol}}, {{balance}} available", { symbol, balance })}
-              aria-describedby={t("Tap to select")}
+              aria-label={t("{{symbol}}, {{balance}} available", { symbol, balance })}
+              accessibilityHint={t("Tap to select")}
src/components/shared/PluginUpgrade.tsx (1)

14-14: The i18n wiring is correct, but ensure the English translations are added.

The useTranslation implementation follows project conventions with full-sentence keys. The past review already flagged that these two keys are missing from src/i18n/en.json — ensure they are added to complete the localization setup.

Also applies to: 24-24, 87-88

src/components/shared/AmountSelector.tsx (1)

19-22: Verify locale validation is applied before toLocaleString.

The past review flagged that toLocaleString(language, …) can throw if language isn't a valid BCP 47 locale tag. The comment was marked as addressed, but the current code still passes language directly without normalization (e.g., replacing _ with -) or fallback validation.

Please confirm whether the locale validation fix was applied elsewhere (e.g., in i18n initialization) or if it needs to be added here.

#!/bin/bash
# Check if locale normalization/validation is handled elsewhere in the codebase
rg -n "supportedLocalesOf|replaceAll.*_.*-" --type=ts --type=tsx -C2

Also applies to: 170-170

src/components/shared/InstallmentSelector.tsx (1)

111-111: Translation key splits phrase across components.

The translation key "{{count}} installments of" ends with a trailing "of", relying on the amount rendered separately. Per i18n best practices, this should be a single translatable unit to allow proper word order in different languages.

Consider restructuring to include the amount in the translation:

-{hasInstallments ? t("{{count}} installments of", { count: installment }) : t("Pay Now")}
+{hasInstallments ? t("{{count}} installments of {{amount}}", { count: installment, amount: formattedAmount }) : t("Pay Now")}
src/components/home/HomeDisclaimer.tsx (1)

20-27: Use regular strings instead of template literals for static URLs.

Lines 20 and 27 use template literals for static strings. Prefer regular string literals for clarity.

♻️ Suggested fix
               <DisclaimerLink
                 onPress={() => {
-                  openBrowser(`https://docs.exact.ly/exa-app/how-the-exa-app-works`).catch(reportError);
+                  openBrowser("https://docs.exact.ly/exa-app/how-the-exa-app-works").catch(reportError);
                 }}
               />
             ),
             protocol: (
               <DisclaimerLink
                 onPress={() => {
-                  openBrowser(`https://exact.ly/`).catch(reportError);
+                  openBrowser("https://exact.ly/").catch(reportError);
                 }}
               />
src/components/card/CardPIN.tsx (1)

125-127: Countdown display pattern may cause i18n issues in some languages.

The button label and countdown are rendered as separate JSX children. While visually this works with the spaced variant, some languages require different word order when a count is included (e.g., "5 seconds remaining" vs "Hide PIN 5").

Consider using interpolation for better i18n support:

-{error ? t("Retry") : displayPIN ? t("Hide PIN") : t("Show PIN")}
-{`${!error && displayPIN && countdown > 0 ? countdown : " "}`}
+{error ? t("Retry") : displayPIN && countdown > 0 ? t("Hide PIN {{count}}", { count: countdown }) : displayPIN ? t("Hide PIN") : t("Show PIN")}
src/components/home/card-upgrade/Intro.tsx (1)

17-18: The upgrade deadline (June 21, 2025) is expired and remains inconsistent with other components.

This was flagged in a previous review and remains unaddressed. The hardcoded date is now ~7 months in the past (current date: January 2026), and reportedly conflicts with Home.tsx which displays a different deadline. Consider:

  1. Sourcing the deadline from a centralized config or API
  2. Adding conditional logic to hide/adjust the upgrade flow if the deadline has passed
  3. Ensuring all upgrade-related components reference the same deadline value
src/components/home/card-upgrade/ActivateCard.tsx (1)

139-141: Consider centralizing the Visa disclaimer for maintainability.

This exact disclaimer text appears in multiple components (e.g., CardDisclaimer.tsx). While full-sentence keys are the project convention, having the same long string duplicated increases the risk of translation drift. Consider extracting to a shared constant that both files reference:

// e.g., in a shared constants file
export const VISA_DISCLAIMER_KEY = "*The Exa Card is issued by Third National pursuant to a license from Visa...";

This keeps the full-sentence key while ensuring consistency.

src/components/home/AssetList.tsx (1)

29-33: Consider passing t and language as props from parent.

AssetRow is rendered within .map() at line 108, causing each instance to invoke useTranslation(). While react-i18next caches the translation function, passing t and language as props from AssetList would be cleaner and avoid redundant hook calls.

src/components/loans/Review.tsx (1)

325-329: Date formatting should use ISO format per project conventions.

Based on learnings, this app deliberately uses ISO format (e.g., yyyy-MM-dd) for dates across all locales for consistency, rather than locale-aware formatting. This toLocaleDateString call deviates from that pattern.

🔧 Suggested fix using date-fns
+import { format } from "date-fns";
                <Text headline color="$uiNeutralPrimary">
-                  {new Date(Number(maturity) * 1000).toLocaleDateString(language, {
-                    year: "numeric",
-                    month: "short",
-                    day: "numeric",
-                  })}
+                  {format(new Date(Number(maturity) * 1000), "yyyy-MM-dd")}
                </Text>
src/components/add-funds/AddCrypto.tsx (1)

179-180: Consider moving the leading space into the translation.

Having {" "} as a separate JSX node before the translated text is fragile and can break with certain text rendering. Include the leading space in the translation value instead.

♻️ Suggested improvement
-              {" "}
-              {t("Learn more about adding funds.")}
+              {t(" Learn more about adding funds.")}

Or update the translation file to include the leading space in the value.

src/components/shared/ProcessingBalanceBanner.tsx (1)

26-26: Clarify the zero-balance condition.

The falsy check !processingBalance also returns null when processingBalance === 0. If the intent is to hide when there's no processing balance (including zero), this is correct but implicit. If the intent is to hide only while loading, check for null/undefined explicitly.

♻️ Suggested fix if hiding at zero is intended
-  if (!processingBalance) return null;
+  if (processingBalance == null || processingBalance <= 0) return null;
src/components/auth/PasskeysAbout.tsx (1)

52-56: Trailing spaces in template literals remain.

The pattern {${t("Strong credentials.")} } embeds visual spacing in JavaScript. This was previously flagged - consider using marginRight on the Text component instead.

♻️ Suggested fix using margin instead of spaces
-                    <Text color="$uiNeutralSecondary" fontWeight="bold">
-                      {`${t("Strong credentials.")}   `}
-                    </Text>
+                    <Text color="$uiNeutralSecondary" fontWeight="bold" marginRight="$s1">
+                      {t("Strong credentials.")}
+                    </Text>

Apply the same pattern to lines 59 and 65.

src/components/card/SpendingLimits.tsx (1)

57-61: Inline style object violates coding guidelines.

The style={{ alignSelf: "center" }} on Pressable conflicts with the guideline: "Do not use inline styles with the React Native style prop using object literals." Wrap with a Tamagui layout component instead.

♻️ Suggested fix using Tamagui layout
-              <Pressable onPress={onClose} style={{ alignSelf: "center" }} hitSlop={20}>
-                <Text emphasized footnote color="$interactiveTextBrandDefault">
-                  {t("Close")}
-                </Text>
-              </Pressable>
+              <XStack alignSelf="center">
+                <Pressable onPress={onClose} hitSlop={20}>
+                  <Text emphasized footnote color="$interactiveTextBrandDefault">
+                    {t("Close")}
+                  </Text>
+                </Pressable>
+              </XStack>

As per coding guidelines, all styling must go through Tamagui's styled system props.

src/app/_layout.tsx (1)

50-53: Add explicit type annotation for the language detector.

The inline language detector object works functionally but lacks proper TypeScript typing. Adding an explicit type improves maintainability and catches interface mismatches at compile time.

♻️ Suggested fix with proper typing
+import type { LanguageDetectorModule } from "i18next";
+
+const languageDetector: LanguageDetectorModule = {
+  type: "languageDetector",
+  detect: () => getLocales()[0]?.languageCode,
+};
+
 configI18n(initReactI18next)
-  .use({ type: "languageDetector", detect: () => getLocales()[0]?.languageCode })
+  .use(languageDetector)
   .init({ fallbackLng: "en", resources: { en: { translation: en }, es: { translation: es } } })
   .catch(reportError);
src/components/pay-mode/PayMode.tsx (1)

67-93: LGTM! Trans component correctly handles link interpolation.

The self-closing Text components within the components prop work correctly with Trans—react-i18next automatically injects the translated text segments as children at runtime. This pattern was confirmed working in the Portfolio.tsx discussion.

src/components/card/VerificationFailure.tsx (1)

3-3: i18n integration looks correct; verify apostrophe consistency in translation files.

The translation hook usage follows project patterns. As noted in past review, ensure the apostrophe character in "We couldn't verify your identity" exactly matches the corresponding key in your JSON translation files (curly ' vs straight ' can cause silent lookup failures).

Also applies to: 17-17, 38-38, 41-41, 54-54

src/components/activity/Activity.tsx (1)

59-63: Inline styles on FlatList require direct theme access.

This was previously flagged. For FlatList props like contentContainerStyle, Tamagui's styled props don't apply directly, making this a necessary exception for dynamic background based on data length.

src/components/card/exa-card/CardContents.tsx (1)

82-86: Consider extracting the duplicated label to reduce repetition.

The "Available balance" label JSX is identical in both the credit (lines 82-86) and debit (lines 103-107) branches.

Also applies to: 103-107

src/components/home/GettingStarted.tsx (1)

72-73: Dynamic translation key coupling creates maintainability risk.

t(currentStep.title) assumes step titles exactly match translation keys. Changes to step titles without corresponding translation file updates will silently fail. Consider using explicit key mappings.

♻️ Suggested approach using explicit mapping
+const stepTitleKeys: Record<string, string> = {
+  "add-funds": "Add funds",
+  "verify-identity": "Verify identity",
+  "create-account": "Create account",
+};
+
-  const activeStepTitle = currentStep ? t(currentStep.title) : "";
+  const activeStepTitle = currentStep ? t(stepTitleKeys[currentStep.id] ?? currentStep.title) : "";

Run the following script to identify step titles used in the codebase:

#!/bin/bash
# Find step definitions in useOnboardingSteps to verify translation key coverage
ast-grep --pattern $'const $_ = [
  $$$
]'
rg -n "title:" src/utils/useOnboardingSteps.ts
src/components/pay-mode/Pay.tsx (1)

608-631: Percentage formatting pattern is repeated.

The same formatting logic with .replaceAll(/\s+/g, "") appears twice for discount and penalty percentages. Consider extracting to a helper as previously suggested.

src/components/auth/Auth.tsx (1)

144-147: Add missing translation keys to en.json.

The page titles used as translation keys (e.g., "Introducing the first onchain card") need to be added to src/i18n/en.json for symmetric translation behavior across locales.

src/components/home/VisaSignatureSheet.tsx (1)

138-140: Full-sentence translation key is correct per project guidelines.

The long translation string used here aligns with the project's architectural decision to use full-sentence i18n keys rather than structured keys like "upgrade.deactivation_warning". Based on learnings, this is the intended pattern.

src/components/getting-started/GettingStarted.tsx (1)

205-224: Pluralization implementation looks correct.

The StepCounter component uses t("{{count}} step remaining", { count: remainingSteps }) which leverages i18next's built-in pluralization. Ensure the translation files include the plural forms (e.g., "{{count}} step remaining_one" and "{{count}} step remaining_other" keys) for proper pluralization across languages.

#!/bin/bash
# Description: Verify pluralization keys exist in translation files
# Expected: Find plural form keys for step remaining message

rg -n "step remaining" --type=json
src/components/add-funds/SupportedAssetsSheet.tsx (1)

74-90: LGTM! Trans component implementation for embedded link.

The Trans component is correctly used to embed an interactive "Learn more" link within the translated warning message. This approach gives translators control over the full sentence structure, including spacing and punctuation around the embedded component.

src/components/shared/ProcessingBalance.tsx (1)

26-26: Ambiguous falsy check on processingBalance.

The condition !processingBalance will hide the component when processingBalance === 0. If this is intentional (zero balance shouldn't be displayed), consider making the intent explicit.

♻️ Suggested clarification
-  if (!processingBalance) return null;
+  if (processingBalance == null || processingBalance <= 0) return null;
src/components/loans/LoanSummary.tsx (1)

79-82: Potential NaN display when borrow?.assets is undefined.

When pending is false but borrow hasn't loaded yet (or has no data), borrow?.assets evaluates to undefined, resulting in NaN being displayed. Consider adding a guard or fallback.

🐛 Proposed fix
             <Text title3>
-              {(
-                Number(!isBorrow && installments ? installments.amounts.reduce((a, b) => a + b, 0n) : borrow?.assets) /
-                1e6
-              ).toLocaleString(language, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+              {(() => {
+                const assets = !isBorrow && installments
+                  ? installments.amounts.reduce((a, b) => a + b, 0n)
+                  : borrow?.assets;
+                return assets != null
+                  ? (Number(assets) / 1e6).toLocaleString(language, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+                  : "—";
+              })()}
             </Text>
src/components/send-funds/Receiver.tsx (1)

161-161: Consider whether the arrival time should be dynamic.

The minutes interpolation value is hardcoded to 1. If arrival time varies based on network conditions, consider making this dynamic.

src/components/pay-mode/PaySelector.tsx (1)

222-228: Date formatting conflicts with the app's ISO-format convention.

Per learnings, the app deliberately uses ISO format (yyyy-MM-dd) for dates across all locales for consistency. The toLocaleDateString with month: "short" renders locale-dependent month names which conflicts with this convention.

src/components/pay-mode/PaymentSheet.tsx (1)

194-198: Date format may conflict with ISO-format convention.

Per learnings, the app deliberately uses ISO format for dates across all locales. The toLocaleDateString with month: "short" renders locale-dependent month names. Consider using ISO format (yyyy-MM-dd) for consistency.

let eventPosition = 0;

for (const event of activity) {
const date = format(event.timestamp, "yyyy-MM-dd");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Date displayed in header rows is not locale-formatted.

The date passed to HeaderRow is in "yyyy-MM-dd" format (line 38), which is then rendered directly to users (line 111). This doesn't match the locale-aware formatting applied elsewhere in this PR.

Consider formatting the display date using toLocaleDateString with the current language, while keeping the internal key in a consistent format.

♻️ Suggested approach
 const HeaderRow = memo(function HeaderRow({ date }: { date: string }) {
+  const { i18n: { language } } = useTranslation();
+  const formattedDate = new Date(date).toLocaleDateString(language, {
+    year: "numeric",
+    month: "long", 
+    day: "numeric",
+  });
   return (
     <View paddingHorizontal="$s4" paddingVertical="$s3" backgroundColor="$backgroundSoft">
       <Text subHeadline color="$uiNeutralSecondary">
-        {date}
+        {formattedDate}
       </Text>
     </View>
   );
 });

Also applies to: 107-115

Comment on lines +76 to +80
aria-label={t("{{partner}}, page {{current}} of {{total}}", {
partner: benefit.partner,
current: index + 1,
total: BENEFITS.length,
})}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the aria-label translation key exists in both locale files
echo "=== Checking en.json for aria-label template ==="
grep -F "page {{current}} of {{total}}" src/i18n/en.json || echo "NOT FOUND in en.json"

echo -e "\n=== Checking es.json for aria-label template ==="
grep -F "page {{current}} of {{total}}" src/i18n/es.json || echo "NOT FOUND in es.json"

Repository: exactly/exa

Length of output: 270


Add missing translation key to en.json.

The aria-label translation key "{{partner}}, page {{current}} of {{total}}" is missing from en.json but exists in es.json. English users will see the raw key string instead of a translated label. Add the key to src/i18n/en.json with an appropriate English translation.

🤖 Prompt for AI Agents
In `@src/components/benefits/BenefitsSection.tsx` around lines 76 - 80, The
aria-label key used in BenefitsSection.tsx (t("{{partner}}, page {{current}} of
{{total}}", {...})) is missing from en.json; add that translation entry to
en.json with the same interpolation placeholders (e.g. key: "{{partner}}, page
{{current}} of {{total}}" and value: "{{partner}}, page {{current}} of
{{total}}") so English users see a proper localized label and the placeholders
partner, current, and total match exactly.

<YStack gap="$s4_5">
<Text primary emphasized body>
Select your funding installment plan
{t("Select your funding installment plan")}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the i18n translation files
fd -e json . | grep -i i18n

Repository: exactly/exa

Length of output: 88


🏁 Script executed:

# Also search for en.json and es.json files
fd "en.json|es.json" --type f

Repository: exactly/exa

Length of output: 88


🏁 Script executed:

# Check if i18n directory exists and show its structure
find . -type d -name i18n 2>/dev/null

Repository: exactly/exa

Length of output: 65


🏁 Script executed:

# Check for "Select your funding installment plan" in en.json and es.json
echo "=== Checking for 'Select your funding installment plan' ==="
echo "en.json:"
rg "Select your funding installment plan" src/i18n/en.json
echo "es.json:"
rg "Select your funding installment plan" src/i18n/es.json

echo ""
echo "=== Checking for 'Continue' ==="
echo "en.json:"
rg '"Continue"' src/i18n/en.json
echo "es.json:"
rg '"Continue"' src/i18n/es.json

Repository: exactly/exa

Length of output: 302


Add missing translation keys to en.json.

The keys "Select your funding installment plan" and "Continue" are still missing from src/i18n/en.json while present in src/i18n/es.json. Without these entries, English users will see translation keys instead of translated text. Add both keys with their English translations to en.json.

🤖 Prompt for AI Agents
In `@src/components/loans/Installments.tsx` at line 67, Add the missing English
translations for the keys used in Installments.tsx by adding entries for "Select
your funding installment plan" and "Continue" to src/i18n/en.json with their
English strings (e.g., "Select your funding installment plan": "Select your
funding installment plan", "Continue": "Continue") so Installments.tsx and other
components using those keys display proper English text.

const symbol = market?.symbol.slice(3) === "WETH" ? "ETH" : market?.symbol.slice(3);
const isBorrow = loan.installments === 1;
const timestamp = Math.floor(Date.now() / 1000);
const timestamp = useMemo(() => Math.floor(Date.now() / 1000), []);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Timestamp is computed once per mount.

The empty dependency array means timestamp is calculated only when the component mounts and won't update if the user keeps the view open across a maturity boundary. This is likely acceptable for a loan summary, but worth noting if long-lived sessions are expected.

🤖 Prompt for AI Agents
In `@src/components/loans/LoanSummary.tsx` at line 29, The timestamp is only
computed once on mount because timestamp is created with useMemo(..., []), so it
won't reflect time passing across a loan maturity boundary; either compute
Date.now()/1000 on each render by removing useMemo or, if you need periodic
updates for long-lived views, replace the static memo with a stateful timestamp
in LoanSummary updated via a useEffect/setInterval (e.g., refresh every minute)
and clear the interval on unmount so the UI updates correctly across boundaries.

Comment on lines +44 to +54
<Trans
i18nKey="When you make a purchase using an installment plan, <highlight>you must pay each installment manually before the due date.</highlight> Otherwise, a daily penalty of {{rate}} is added while the payment is late."
values={{
rate: (0.0045).toLocaleString(language, {
style: "percent",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}),
}}
components={{ highlight: <Text color="$uiInfoSecondary" /> }}
/>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Trans component usage is correct for JSX interpolation.

The Trans component properly handles the highlighted text segment and the locale-aware rate formatting. The i18nKey follows the full-sentence convention.

Consider extracting the penalty rate 0.0045 to a named constant for improved maintainability, as noted in a previous review.

🤖 Prompt for AI Agents
In `@src/components/pay-mode/ManualRepaymentSheet.tsx` around lines 44 - 54,
Extract the hard-coded penalty magic number 0.0045 into a named constant (e.g.,
DAILY_PENALTY_RATE or PENALTY_RATE) and use that constant in the Trans
component's values calculation instead of the literal; update the file-level or
module-level scope in ManualRepaymentSheet.tsx so the constant is easily
discoverable and (optionally) exported, and keep the existing
toLocaleString(language, { style: "percent", minimumFractionDigits: 2,
maximumFractionDigits: 2 }) call unchanged while replacing the literal with the
new constant.

Comment on lines +81 to +87
{t("Due {{date}}", {
date: new Date(Number(maturity) * 1000).toLocaleDateString(language, {
year: "numeric",
month: "short",
day: "numeric",
}),
})}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Date formatting should use ISO format for consistency.

Based on learnings, this codebase uses ISO format (yyyy-MM-dd) for date display to ensure consistency across all locales. The current toLocaleDateString usage contradicts this architectural decision established in TransactionDetails.tsx.

🛠️ Suggested fix using ISO format
+import { format, isAfter } from "date-fns";
-import { isAfter } from "date-fns";
 
-                  {t("Due {{date}}", {
-                    date: new Date(Number(maturity) * 1000).toLocaleDateString(language, {
-                      year: "numeric",
-                      month: "short",
-                      day: "numeric",
-                    }),
-                  })}
+                  {t("Due {{date}}", {
+                    date: format(new Date(Number(maturity) * 1000), "yyyy-MM-dd"),
+                  })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{t("Due {{date}}", {
date: new Date(Number(maturity) * 1000).toLocaleDateString(language, {
year: "numeric",
month: "short",
day: "numeric",
}),
})}
{t("Due {{date}}", {
date: format(new Date(Number(maturity) * 1000), "yyyy-MM-dd"),
})}
🤖 Prompt for AI Agents
In `@src/components/shared/Success.tsx` around lines 81 - 87, Replace the
locale-dependent date formatting in the Success component where t("Due
{{date}}", { date: new Date(Number(maturity) * 1000).toLocaleDateString(...) })
is used with an ISO yyyy-MM-dd string; compute the date as new
Date(Number(maturity) * 1000).toISOString().slice(0,10) (or a small helper that
returns yyyy-MM-dd) and pass that ISO string into the t(...) call so the
displayed date matches the project's ISO format convention.

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.

app: apply i18n

3 participants