fix: localize 60 hardcoded English strings across 16 source files#1483
fix: localize 60 hardcoded English strings across 16 source files#1483ArushKapoorJuspay wants to merge 7 commits intojuspay:mainfrom
Conversation
…-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)
…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")} |
There was a problem hiding this comment.
Localization is missing for "Click to Pay".
There was a problem hiding this comment.
""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."
src/PaymentManagement.res
Outdated
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
"Already addressed in commit ed4a0b8 — added saveCardText locale key and wired it to the button label in PaymentManagement.res."
| | Top => "We'll be back with you shortly :)" | ||
| | _ => "Try another payment method :)" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
"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 `, |
There was a problem hiding this comment.
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}
There was a problem hiding this comment.
"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)."
src/LocaleStrings/FrenchLocale.res
Outdated
| 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 `, |
There was a problem hiding this comment.
same happened in other locales too, please check.
There was a problem hiding this comment.
"Fixed across all 7 affected locales in ea835b0. Each now includes ${cardBrand} in the translated ctpTermsConsentText string."
AbhishekChorotiya
left a comment
There was a problem hiding this comment.
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:
ctpTermsConsentTextdrops brand from legal consent in 7 locales;postFailedSubmitResponsesends locale-dependent string over postMessage API - 3× MEDIUM:
module Button/MicroDepositScreenre-subscribe to full atom;value="$0.01"hardcoded;"Click to Pay"inline with RTL bidi risk - 4× LOW: Stale closure;
phoneLabelnaming; sentence fragmented across 3 keys;$0.01/1-2 business daysbaked into locale function
src/Payments/AddBankDetails.res
Outdated
| postFailedSubmitResponse( | ||
| ~errortype="validation_error", | ||
| ~message="Please add Bank Details and then confirm payment with the added payment methods.", | ||
| ~message=localeString.addBankDetailsConfirmText, |
There was a problem hiding this comment.
[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.",There was a problem hiding this comment.
"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."
src/Payments/BankDebitModal.res
Outdated
| module Button = { | ||
| @react.component | ||
| let make = (~active=true, ~onclick) => { | ||
| let {localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom) |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
"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) |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
"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" /> |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
"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))} |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
"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, |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
"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)} |
There was a problem hiding this comment.
[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> elementThis gives translators control over word order while keeping the link interactive.
There was a problem hiding this comment.
"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.`, |
There was a problem hiding this comment.
[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.`,There was a problem hiding this comment.
"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
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)localeStringrecord typemicroDepositsExpectText: string => string,bankDebitStepsText: string => string)English Locale (
EnglishLocale.res)17 Non-English Locale Files
16 Source Files (hardcoded →
localeStringreference)QRCodeDisplay.resAccordionContainer.resAddBankDetails.resAddBankAccount.resBankDebitModal.resErrorOccured.resBankTransfersPopup.resVoucherDisplay.resApplePay.resClickToPayNotYou.resClickToPayDetails.resLoader.resPaymentManagement.resSavedMethodItemV2.resErrorBoundary.resCardSchemeComponent.resIntentionally Skipped
"Savings"/"Checking"in BankDebitModal — sent to backend as API values viaaccountType->String.toLowerCase"ACH Direct Debit"— payment method brand name, not user-facing translatable text"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:buildpasses with zero errorsACHBankDebit.resandRenderPaymentMethods.res)