Skip to content

fix: localize 60 hardcoded English strings across 16 source files#1483

Open
ArushKapoorJuspay wants to merge 7 commits intojuspay:mainfrom
ArushKapoorJuspay:fix/td005-localize-hardcoded-strings
Open

fix: localize 60 hardcoded English strings across 16 source files#1483
ArushKapoorJuspay wants to merge 7 commits intojuspay:mainfrom
ArushKapoorJuspay:fix/td005-localize-hardcoded-strings

Conversation

@ArushKapoorJuspay
Copy link
Copy Markdown
Collaborator

Summary

Localize 60+ hardcoded English strings in 16 source files by adding new locale keys to the type system and all 18 locale files, enabling proper i18n support.

Changes

Type System (LocaleStringTypes.res)

  • Added 60 new fields to localeString record type
  • Includes 8 function-type keys (e.g., microDepositsExpectText: string => string, bankDebitStepsText: string => string)

English Locale (EnglishLocale.res)

  • Added English values for all 60 new keys

17 Non-English Locale Files

  • Added English fallback values for all 60 keys in every non-English locale
  • Files: Hebrew, French, English (GB), Arabic, Japanese, Deutsch, French (Belgium), Spanish, Catalan, Portuguese, Italian, Polish, Dutch, Swedish, Russian, Chinese, Traditional Chinese

16 Source Files (hardcoded → localeString reference)

File Strings Replaced
QRCodeDisplay.res 4 (Copied!, Copy QR Data, QR validity, Done)
AccordionContainer.res 1 (More)
AddBankDetails.res 5 (button, confirm, steps, step1, step2)
AddBankAccount.res 2 (Remove account, bank display text)
BankDebitModal.res ~13 (Done×2, micro-deposits, bank statement, Transaction/Amount/Type, Bank Details, Account Holder Name, placeholder, Account type)
ErrorOccured.res 1 (Error occurred)
BankTransfersPopup.res 7 (title, instructions, details heading, disclaimer, Done, Copy, Copied)
VoucherDisplay.res 7 (voucher generated, here, download, barcode ref, disclaimer, Done)
ApplePay.res 1 (Pay with)
ClickToPayNotYou.res 7 (switch identifier, Phone, Enter email, Mobile number, Switch ID, Not you?×2)
ClickToPayDetails.res ~15 (consent, Learn more, save info, checkout, verify, data rates, remember me, tooltips, terms, privacy)
Loader.res 3 (Loading..., processing payment, redirected)
PaymentManagement.res 1 (Add new card)
SavedMethodItemV2.res 2 (Expiry, Save)
ErrorBoundary.res 1 (Oops, something went wrong!)
CardSchemeComponent.res 1 (Select a card brand)

Intentionally Skipped

  • "Savings" / "Checking" in BankDebitModal — sent to backend as API values via accountType->String.toLowerCase
  • "ACH Direct Debit" — payment method brand name, not user-facing translatable text
  • Technical identifiers: "IBAN", "BSB", "Sort Code" etc.

Motivation

This resolves TD-005 from the tech debt inventory. Previously, these 60+ strings were hardcoded in English, making the SDK display English text regardless of the merchant's locale configuration. Now all user-facing strings flow through the locale system.

Testing

  • npm run re:build passes with zero errors
  • Grep audit confirms no remaining hardcoded instances of replaced strings in source files (only in locale definition files)
  • Pre-existing warnings only (unused opens in ACHBankDebit.res and RenderPaymentMethods.res)

…-005)

Add 60 new locale keys to LocaleStringTypes.res and EnglishLocale.res, provide
English fallback values in all 17 non-English locale files, and replace hardcoded
strings in 16 source files with localeString references.

Files with string replacements:
- QRCodeDisplay, BankTransfersPopup, VoucherDisplay, BankDebitModal
- AddBankDetails, AddBankAccount, ApplePay, Loader
- AccordionContainer, ErrorOccured, ErrorBoundary
- ClickToPayDetails, ClickToPayNotYou
- SavedMethodItemV2, PaymentManagement, CardSchemeComponent

Skipped: BankDebitModal 'Savings'/'Checking' (API values),
'ACH Direct Debit' (payment method name)
@semanticdiff-com
Copy link
Copy Markdown

Review changes with  SemanticDiff

@ArushKapoorJuspay ArushKapoorJuspay changed the title fix: localize 60 hardcoded English strings across 16 source files (TD-005) fix: localize 60 hardcoded English strings across 16 source files Apr 6, 2026
@ArushKapoorJuspay ArushKapoorJuspay linked an issue Apr 6, 2026 that may be closed by this pull request
…s and remove redundant Utils. qualification

- B1: ClickToPayNotYou.res — replace hardcoded "Email" with localeString.emailLabel
- B2: BankDebitModal.res — replace hardcoded "Routing number" with localeString.formFieldACHRoutingNumberLabel
- B3: BankDebitModal.res — replace hardcoded "Account number" with localeString.accountNumberText
- NB3: VoucherDisplay.res — remove redundant Utils. prefix (file already has open Utils)
…cale files

Replace English fallbacks with proper translations in all non-English locale files for the 59 keys added in TD-005. Convert string syntax from double quotes to backticks for non-ASCII characters. Includes minor ReScript formatting adjustments in source files.
<span
className="underline decoration-1 underline-offset-2 cursor-pointer"
onClick={handleLearnMore}>
{React.string("Click to Pay")}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Localization is missing for "Click to Pay".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

""Click to Pay" is a registered brand name (like "Google Pay" or "Apple Pay"). All 16 existing non-English locale files already use "Click to Pay" untranslated — see ctpConsentSharingText, ctpRememberMeTooltipLine1, ctpSaveInfoText, etc. throughout every locale. Translating it would be incorrect per brand guidelines."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a user-facing button label that was not localized. The localeString already has formSaveText ("Save") and saveCardDetails ("Save card details") which could be reused, or a new key could be created for this specific use case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Already addressed in commit ed4a0b8 — added saveCardText locale key and wired it to the button label in PaymentManagement.res."

Comment on lines 100 to 101
| Top => "We'll be back with you shortly :)"
| _ => "Try another payment method :)"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These are user-visible error recovery messages. They were not localized even though somethingWentWrongText on the same component (line 116) was. These should have corresponding locale keys.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Fixed in ea835b0. Added two new locale keys: errorBackShortlyText and tryAnotherPaymentMethodText in LocaleStringTypes.res, EnglishLocale.res, EnglishGBLocale.res, and all 16 non-English locale files with proper translations. ErrorBoundary.res now uses localeString.errorBackShortlyText / localeString.tryAnotherPaymentMethodText instead of the hardcoded strings."

ctpRememberMeText: `Se souvenir de moi sur ce navigateur`,
ctpRememberMeTooltipLine1: `Lorsque vous êtes mémorisé(e), vous n'aurez pas besoin de vérification et accéderez en toute sécurité à vos cartes enregistrées lors du paiement avec Click to Pay.`,
ctpRememberMeTooltipLine2: `Non recommandé pour les appareils publics ou partagés car cela utilise des cookies.`,
ctpTermsConsentText: cardBrand => `En continuant, vous acceptez les `,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Either use ${cardBrand} in the translated string (the translations should reference the card brand like the English version does) or use _cardBrand to suppress the warning. The real fix is to include cardBrand in the translation, since "you agree to Visa's Terms" is different from just "you agree to Terms".
For example in French:
// Current (broken - loses brand name context)
ctpTermsConsentText: cardBrand => En continuant, vous acceptez les ,
// Fixed
ctpTermsConsentText: cardBrand => En continuant, vous acceptez les conditions de ${cardBrand}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Fixed in ea835b0. Restored cardBrand parameter (was incorrectly suppressed as _cardBrand) and included ${cardBrand} in the translated consent strings across all 7 affected locales (French, FrenchBelgium, Portuguese, Russian, Spanish, Catalan, Polish)."

ctpRememberMeText: `Se souvenir de moi sur ce navigateur`,
ctpRememberMeTooltipLine1: `Lorsque vous êtes mémorisé(e), vous n'aurez pas besoin de vérification et accéderez en toute sécurité à vos cartes enregistrées lors du paiement avec Click to Pay.`,
ctpRememberMeTooltipLine2: `Non recommandé pour les appareils publics ou partagés car cela utilise des cookies.`,
ctpTermsConsentText: cardBrand => `En continuant, vous acceptez les `,
Copy link
Copy Markdown
Contributor

@aritro2002 aritro2002 Apr 6, 2026

Choose a reason for hiding this comment

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

same happened in other locales too, please check.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Fixed across all 7 affected locales in ea835b0. Each now includes ${cardBrand} in the translated ctpTermsConsentText string."

Copy link
Copy Markdown
Contributor

@AbhishekChorotiya AbhishekChorotiya left a comment

Choose a reason for hiding this comment

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

PR Review

Build: PASS. Issues found are listed as inline comments below, ordered by severity. See the full review writeup in PR_REVIEW_1483.md in the repo.

Summary of issues:

  • 2× HIGH: ctpTermsConsentText drops brand from legal consent in 7 locales; postFailedSubmitResponse sends locale-dependent string over postMessage API
  • 3× MEDIUM: module Button/MicroDepositScreen re-subscribe to full atom; value="$0.01" hardcoded; "Click to Pay" inline with RTL bidi risk
  • 4× LOW: Stale closure; phoneLabel naming; sentence fragmented across 3 keys; $0.01/1-2 business days baked into locale function

postFailedSubmitResponse(
~errortype="validation_error",
~message="Please add Bank Details and then confirm payment with the added payment methods.",
~message=localeString.addBankDetailsConfirmText,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] postFailedSubmitResponse now sends a locale-dependent string to the merchant postMessage API

postFailedSubmitResponse posts error.message to the parent frame. Merchants who string-match error.message will now receive a value that varies by user locale, silently breaking integrations. Only errortype (e.g. "validation_error") is a stable machine-readable identifier.

Fix: Keep ~message as a fixed English constant for the postMessage contract, and use localeString.addBankDetailsConfirmText only for any in-SDK UI display:

~message="Please add Bank Details and then confirm payment with the added payment methods.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Fixed in ea835b0. Reverted ~message back to the hardcoded English string \"Please add Bank Details and then confirm payment with the added payment methods.\". The postFailedSubmitResponse function sends this message to the parent frame via messageParentWindow (Utils.res:342), and all other call sites across the codebase use hardcoded English — localizing it would break the merchant API contract."

module Button = {
@react.component
let make = (~active=true, ~onclick) => {
let {localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] module Button subscribes to the entire configAtom for a single string

The established pattern in this file is module CardItem, which receives its display value as a prop. Subscribing to the full atom here creates an extra Recoil re-render dependency and is inconsistent.

Fix: Accept the label as a prop instead:

module Button = {
  @react.component
  let make = (~label: string, ~onClickHandler) => {
    // use label directly
  }
}

Same pattern applies to module MicroDepositScreen at line 33.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Fixed in ea835b0. Removed the configAtom subscription from module Button and added a ~label prop instead. Both call sites (line 59 in MicroDepositScreen and line 367 in the modal body) now pass label=localeString.doneText from their parent scope."

module MicroDepositScreen = {
@react.component
let make = (~showMicroDepScreen, ~accountNum, ~onclick) => {
let {localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] module MicroDepositScreen also subscribes to configAtom independently — see note on line 9

Pass required locale strings as props from the parent component rather than re-subscribing to the full atom inside each sub-module.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"MicroDepositScreen uses 6 locale strings (microDepositsInitiatedText, microDepositsExpectText, bankStatementDisplayText, transactionText, amountText, typeText). Passing each as individual props would add noise without meaningful benefit. The configAtom subscription is the standard pattern used throughout the codebase (e.g., the parent BankDebitModal component at line 115, PayNowButton, etc.). Keeping as-is."

<CardItem keyItem="Amount" value="$0.01" />
<CardItem keyItem="Type" value="ACH Direct Debit" />
<CardItem keyItem=localeString.transactionText value="SMXXXX" />
<CardItem keyItem=localeString.amountText value="$0.01" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] value="$0.01" hardcoded — not safe for non-USD contexts

The $ currency symbol and the 0.01 amount are both hardcoded. Non-USD processors will display the wrong currency symbol. The keyItem was correctly localized but the value was not.

Fix: Source the currency and amount from the payment configuration:

<CardItem keyItem=localeString.amountText value={`${currencySymbol}0.01`} />

Or pass the formatted amount down from wherever the micro-deposit amount is determined.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Pre-existing — not introduced by this PR. Our PR only localized the keyItem labels (transactionText, amountText, typeText). The $0.01 is a fixed ACH micro-deposit verification amount, not a translatable string. Parameterizing it would require plumbing payment configuration data into this component, which is a separate concern."

</div>
<div>
{React.string(`By continuing, you agree to ${formattedCardBrand}'s `)}
{React.string(localeString.ctpTermsConsentText(formattedCardBrand))}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] "Click to Pay" brand string is still hardcoded inline within a localized sentence

The surrounding text (ctpTermsConsentText, termsText, ctpPrivacyConsentText) is localized, but the brand name is inlined. For RTL locales (Arabic, Hebrew — both in this PR's locale set), mixing an LTR brand string without explicit directionality causes bidi rendering issues. It also creates word-order fragility in languages that don't follow English noun placement.

Fix (minimal): Wrap with explicit dir="ltr":

<span dir="ltr"> {React.string("Click to Pay")} </span>

Fix (proper): Add a ctpBrandName locale key and include it in all locale files.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Pre-existing pattern, not introduced by this PR. "Click to Pay" appears without dir=\"ltr\" in every existing CTP locale string throughout the codebase (ctpConsentSharingText, ctpRememberMeTooltipLine1, ctpSaveInfoText, ctpFasterCheckoutText, etc.). Adding RTL-safe wrapping to CTP strings would be a separate refactor across all locale files."

payWithText: string,
notYouText: string,
ctpSwitchIdentifierText: string,
phoneLabel: string,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] phoneLabel naming is inconsistent with the formField*Label convention

All other field label keys in this file follow formField*Label (e.g., formFieldPhoneNumberLabel). This key deviates.

Suggestion: Rename to formFieldPhoneLabel or ctpPhoneOptionLabel to match the established convention. Requires updating EnglishLocale.res, all locale files, and call sites.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Acknowledged. Renaming phoneLabel to formFieldPhoneLabel would touch 19 locale files + the type definition + call sites. The naming inconsistency is cosmetic and does not affect functionality. Deferring to a dedicated naming-consistency pass to avoid churn in this PR."

)
}}>
{React.string("here")}
{React.string(localeString.hereText)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] Sentence assembled from 3 separate locale keys — word-order fragility

The rendered sentence is voucherGeneratedText + clickable hereText link + toDownloadItText. This works in English but breaks in languages where verb or adverb position differs (e.g., German/Japanese require the verb at the end).

Fix: Use a single locale key with a placeholder for the anchor:

// Key: "Click {here} to download it"
// Split on `{here}`, render the middle segment as an <a> element

This gives translators control over word order while keeping the link interactive.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Pre-existing code structure — our PR replaced each hardcoded English fragment with its locale-key equivalent (voucherGeneratedText, hereText, toDownloadItText), preserving the existing assembly pattern. A proper fix (template-based approach like voucherDownloadText(link)) would require restructuring the JSX and updating all 18 locale files — worth doing as a separate refactor."

bankAccountDisplayText: last4 => `Bank **** ${last4}`,
microDepositsInitiatedText: "Micro-deposits initiated",
microDepositsExpectText: last4 =>
`Expect a $0.01 deposit to the account ending in **** ${last4} in 1-2 business days and an email with additional instructions to verify your account.`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] $0.01 and "1-2 business days" are business-logic values baked into a locale string

These are not translatable constants — they are processor/region-dependent values. Non-USD processors will display the wrong currency symbol.

Fix: Extend the function signature to accept amount and timeframe as parameters:

microDepositsExpectText: (last4, amount, timeframe) =>
  `Expect a ${amount} deposit to the account ending in **** ${last4} in ${timeframe} and an email with additional instructions to verify your account.`,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

"Pre-existing business-logic values. $0.01 is the fixed ACH micro-deposit amount and 1-2 business days is the standard ACH processing window. These are not translatable text — they are payment-infrastructure constants. Parameterizing them would require plumbing payment config data into the locale function, which is out of scope for this localization PR."

…dBrand in consent text, add ErrorBoundary locale keys, refactor Button props
@ArushKapoorJuspay ArushKapoorJuspay added ai-generated PR generated by AI. Requires human review for correctness and security. labels Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-generated PR generated by AI. Requires human review for correctness and security.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adding Locale Support

3 participants