Senior Frontend Interview Guide, Part 11: Internationalization and Localization
Global banks and fintech companies serve customers in many languages, regions, and currencies. Even a United States-only bank typically supports Spanish and must format dates, numbers, and currencies correctly. Internationalization (i18n) is engineering the product so it can be adapted to any…

This article is Part 11 of 18 in The Senior Frontend Engineer Interview Guide, and includes 20 interview questions with model answers.
Global banks and fintech companies serve customers in many languages, regions, and currencies. Even a United States-only bank typically supports Spanish and must format dates, numbers, and currencies correctly. Internationalization (i18n) is engineering the product so it can be adapted to any language and region without code changes. Localization (l10n) is the adaptation itself: translating text, formatting data, and adjusting content for a specific locale. This chapter covers locales, the Intl APIs, message formatting with plurals and ICU syntax, react-i18next and FormatJS, right-to-left support, translation workflows, and the subtle bugs interviewers love to ask about.
Key Concepts
- Locale: a language plus optional region, script, and extensions, identified by a BCP 47 language tag:
en-US,es-MX,fr-CA,zh-Hans-CN,ar-EG,de-DE-u-nu-latn(with a numbering system extension). - Language is not the same as region:
es-ESandes-MXshare a language but differ in vocabulary, currency, and formats;en-USanden-GBdiffer in date order and spelling. - Globalization (g11n) is the umbrella term covering both i18n and l10n.
- Translation is only one part of localization. Formats, currencies, units, time zones, legal text, imagery, colors, and even product features (payment rails available per country) vary.
Internationalization Checklist
An internationalized application:
- Externalizes all user-facing strings into resource files.
- Formats numbers, currencies, dates, times, lists, and units with locale-aware APIs.
- Handles plurals, gender, and grammatical variations through a message format, not string concatenation.
- Supports text expansion (German text can be 30 to 40 percent longer than English) and different scripts.
- Supports right-to-left layouts.
- Uses Unicode (UTF-8) everywhere and handles input in any script.
- Sorts and compares strings with locale-aware collation.
- Stores data in locale-neutral forms (ISO dates in UTC, decimal strings or minor units for money) and formats only at the edges.
- Detects, stores, and lets users change their locale preference.
- Loads translations efficiently and falls back gracefully when a translation is missing.
The Intl APIs
Modern JavaScript includes the ECMAScript Internationalization API (Intl), backed by the Unicode CLDR data in browsers. Use it instead of hand-written formatting.
Numbers and Currency
const amount = 1234567.891;
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
// "$1,234,567.89"
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(amount);
// "1.234.567,89 €"
new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' }).format(amount);
// "₹12,34,567.89" (Indian digit grouping)
new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(amount);
// "¥1,234,568" (yen has no minor units)
new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(0.0425);
// "4.25%"
new Intl.NumberFormat('en-US', { notation: 'compact' }).format(2_500_000);
// "2.5M"
new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD', currencySign: 'accounting',
}).format(-45);
// "($45.00)"
Notes:
- Currency and locale are independent. A user in France viewing a USD account sees
1 234,56 $US. The account’s currency comes from data; the format comes from the user’s locale. - Minor units differ: USD has 2, JPY has 0, KWD and BHD have 3. Do not assume two decimals;
Intl.NumberFormatknows the defaults (resolvedOptions().maximumFractionDigits). - Create formatters once and reuse them; constructing
Intlobjects is relatively expensive. formatToPartsreturns typed parts (currency symbol, integer, decimal, fraction) for custom styling, such as a smaller font for cents.
Parsing Localized Numbers
Intl formats but does not parse. To parse user input like 1.234,56 in a German locale, derive the group and decimal separators from formatToParts:
function parseLocaleNumber(input, locale) {
const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);
const group = parts.find(p => p.type === 'group')?.value ?? ',';
const decimal = parts.find(p => p.type === 'decimal')?.value ?? '.';
const normalized = input
.replace(new RegExp(`[\\s${group === '.' ? '\\.' : group} ]`, 'g'), '')
.replace(decimal, '.');
return normalized; // keep as a string; convert to minor units with a decimal library
}
For money, convert the normalized string to minor units with a decimal library rather than parseFloat (Chapter 13).
Dates and Times
const date = new Date('2026-03-14T15:09:26Z');
new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'short', timeZone: 'America/New_York' }).format(date);
// "Mar 14, 2026, 11:09 AM"
new Intl.DateTimeFormat('en-GB', { dateStyle: 'short' }).format(date);
// "14/03/2026"
new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long' }).format(date);
// "2026年3月14日"
new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-1, 'day');
// "yesterday"
Time zone rules:
- Store and transmit instants in UTC (ISO 8601 with
Z). - Display in the user’s time zone (
Intl.DateTimeFormat().resolvedOptions().timeZone) or the business-relevant time zone. Payment cutoff times are usually expressed in the bank’s time zone (“Wires submitted after 5:00 PM ET are processed the next business day”), so display them explicitly with the zone. - Business dates (statement date, settlement date, date of birth) are calendar dates, not instants. Represent them as
YYYY-MM-DDstrings. Converting"2026-03-14"withnew Date()treats it as UTC midnight, which displays as March 13 in American time zones, a classic bug. - The Temporal API (with
Temporal.PlainDate,Temporal.ZonedDateTime, andTemporal.Instant) fixes these modeling problems and is shipping in browsers; until it is universally available, use a polyfill or libraries likedate-fnswithdate-fns-tz.
Other Intl Formatters
new Intl.ListFormat('en', { type: 'conjunction' }).format(['Checking', 'Savings', 'Credit']);
// "Checking, Savings, and Credit"
new Intl.PluralRules('en-US').select(1); // "one"
new Intl.PluralRules('en-US', { type: 'ordinal' }).select(2); // "two" -> "2nd"
new Intl.DisplayNames(['en'], { type: 'currency' }).of('EUR'); // "Euro"
new Intl.DisplayNames(['fr'], { type: 'region' }).of('US'); // "États-Unis"
new Intl.Collator('de').compare('ä', 'z'); // negative: ä sorts before z in German
new Intl.Segmenter('ja', { granularity: 'word' }); // word boundaries for languages without spaces
Intl.Collator matters when sorting payee names: Array.prototype.sort() without a comparator sorts by UTF-16 code units, which puts “Zoë” and “Émile” in the wrong places. Use names.sort(new Intl.Collator(locale, { sensitivity: 'base' }).compare).
Message Formatting: Plurals, Gender, and Variables
String concatenation breaks translation:
// Wrong: word order and plural rules differ across languages
`You have ${count} new message` + (count === 1 ? '' : 's');
Languages have different plural categories (CLDR defines zero, one, two, few, many, other). English uses one and other; Arabic uses all six; Polish distinguishes one, few, and many; Japanese uses only other.
ICU MessageFormat handles this:
{count, plural,
=0 {You have no pending transfers}
one {You have # pending transfer}
other {You have # pending transfers}
}
{payeeGender, select,
female {She will receive the funds by {date}}
male {He will receive the funds by {date}}
other {They will receive the funds by {date}}
}
Your balance is {balance, number, ::currency/USD} as of {asOf, date, medium}.
Rules for translatable messages:
- Use whole sentences with placeholders, never fragments joined in code, because word order varies.
- Give translators context: a description, screenshots, and maximum length.
- Use named placeholders (
{payeeName}), not positional ones. - Do not reuse the same key for different contexts, even if the English text is identical (“Close” as a verb versus “Close” as an adjective may translate differently).
- Keep markup out of strings when possible, or use rich-text placeholders (
<link>terms</link>) that the library maps to components.
React i18n Libraries
react-i18next
The most widely used option, built on i18next.
// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import ICU from 'i18next-icu';
i18n
.use(ICU)
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en-US',
supportedLngs: ['en-US', 'es-US', 'fr-CA'],
ns: ['common', 'payments'],
defaultNS: 'common',
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
interpolation: { escapeValue: false }, // React already escapes
detection: { order: ['querystring', 'cookie', 'localStorage', 'navigator'] },
});
export default i18n;
// /locales/en-US/payments.json
{
"transfer.title": "Make a transfer",
"transfer.pending": "{count, plural, one {# transfer is pending} other {# transfers are pending}}",
"transfer.confirm": "Send {amount} to {payee} on {date}?",
"transfer.terms": "By continuing you agree to the <terms>transfer terms</terms>."
}
import { useTranslation, Trans } from 'react-i18next';
function TransferHeader({ pendingCount }) {
const { t } = useTranslation('payments');
return (
<>
<h1>{t('transfer.title')}</h1>
<p>{t('transfer.pending', { count: pendingCount })}</p>
<Trans i18nKey="transfer.terms" ns="payments"
components={{ terms: <a href="/legal/transfers" /> }} />
</>
);
}
i18next has its own plural suffix convention (key_one, key_other) when not using the ICU plugin. Namespaces split translations by feature so each micro frontend or route loads only what it needs. With Suspense enabled (the default), useTranslation suspends until the namespace loads.
FormatJS (react-intl)
FormatJS uses ICU MessageFormat natively and exposes components and an imperative API:
import { FormattedMessage, FormattedNumber, useIntl } from 'react-intl';
<FormattedMessage
id="transfer.confirm"
defaultMessage="Send {amount} to {payee} on {date, date, medium}?"
description="Confirmation question on the review step"
values={{ amount: <FormattedNumber value={12.5} style="currency" currency="USD" />, payee, date }}
/>
const intl = useIntl();
const label = intl.formatMessage({ id: 'transfer.amountLabel', defaultMessage: 'Amount' });
FormatJS’s CLI extracts messages from source code (including description for translator context) and can precompile them to reduce runtime parsing cost.
Choosing
Both are mature. react-i18next offers flexible loading, namespaces, and a large plugin ecosystem; FormatJS offers standards-based ICU syntax and message extraction with descriptions. Other options include Lingui (compile-time extraction with small runtime) and the Next.js-specific next-intl.
Right-to-Left (RTL) Languages
Arabic, Hebrew, Persian, and Urdu are written right to left. Supporting them means more than mirroring text:
- Set
dir="rtl"andlangon<html>(or on a container for mixed content). - Use CSS logical properties (
margin-inline-start,padding-inline-end,inset-inline-start,border-start-start-radius,text-align: start) so layouts mirror automatically. - Flexbox and Grid follow the writing direction automatically.
- Mirror directional icons (back/forward arrows, progress chevrons) but not icons that represent real-world objects or time (a clock, a media play button usually stays) and not logos.
- Numbers and phone numbers are written left to right even in RTL text; use Unicode isolation (
<bdi>,dir="auto", or the...isolate characters) for user-generated content of unknown direction, such as payee names, to avoid garbled display. - Test with real translations, not pseudo-mirroring.
.list-item {
padding-inline-start: 1rem;
border-inline-start: 3px solid var(--color-accent);
}
[dir='rtl'] .icon-chevron { transform: scaleX(-1); }
Locale Detection and Persistence
Determine the locale in priority order:
- An explicit user choice saved in their profile (server-side) or a cookie.
- A locale segment in the URL (
/es/accounts), which is good for public, indexable pages. - The
Accept-Languageheader (server) ornavigator.languages(client). - The default locale.
Never infer language from IP geolocation alone: a Spanish speaker in the United States or an English speaker traveling in France should not be forced into a language. Allow the user to change language easily, with the language names written in their own language (“Español”, not “Spanish”).
Separate language from region-dependent formatting when needed. Some users want English text with European date formats. The Intl APIs accept extensions like en-US-u-hc-h23 (24-hour clock) or explicit options.
Translation Workflow
A scalable workflow for an enterprise:
- Developers write source strings (usually English) with keys and descriptions.
- CI extracts new or changed strings and pushes them to a translation management system (TMS) such as Phrase, Lokalise, Crowdin, Smartling, or Transifex.
- Translators work with context (screenshots, descriptions, character limits). Banking content often requires legal and compliance review per locale.
- Completed translations are pulled back into the repository or loaded from a CDN at runtime.
- Missing translations fall back to the default language and are reported (logged in development, monitored in production).
- Releases may be gated on translation completeness for critical locales.
Pseudo-Localization
Pseudo-localization transforms source strings to simulate translation problems before real translations exist: accented characters, extra length, and brackets to reveal truncation and concatenation ("Make a transfer" becomes "[Ḿàķé à ţŕàñšƒéŕ one two]"). It exposes hard-coded strings (which remain un-pseudo-localized), layouts that cannot grow, and font glyph problems. Run it in a dedicated environment or as a development locale.
Performance
- Load only the active locale and the namespaces needed for the current route.
- Lazy-load locale data for libraries that need it (date libraries, phone number formatting).
- Cache translation files with content hashes and long cache lifetimes.
- Precompile ICU messages when using FormatJS.
- Memoize
Intlformatter instances per locale and options. - Avoid flashes of untranslated content by loading the translation before rendering the route (Suspense or route loaders), and render server-side in the correct locale when doing SSR.
Testing i18n
- Unit tests that render components with different locales and assert formatting (but avoid snapshotting exact strings that depend on CLDR versions; Node and browsers can differ slightly, for example in spacing characters like
U+202F). - Lint rules to forbid hard-coded user-facing strings in JSX (
i18next/no-literal-string,formatjs/enforce-description). - Visual testing in long-text locales (German), RTL locales (Arabic), and CJK locales.
- Tests for plural forms with 0, 1, 2, 5, and 21 (some languages treat 21 as
one). - Time zone tests: run tests with
TZ=America/Los_AngelesandTZ=Asia/Tokyoto catch business-date bugs.
Localization Beyond Text in Financial Apps
- Currencies and rails: available payment types differ by country (ACH and RTP in the United States, SEPA in Europe, Faster Payments in the UK, UPI in India).
- Address formats differ: postal code position, state/province requirements. Use libraries like Google’s libaddressinput data rather than a US-only form.
- Phone numbers: use
libphonenumber-jsto validate and format in E.164. - Names: not everyone has a first and last name; avoid forcing two fields, and allow characters beyond ASCII.
- Legal and regulatory content: disclosures, fee schedules, and terms are often jurisdiction-specific and need legal review.
- Calendars and holidays: business day calculations for settlement depend on the payment system’s holiday calendar, not the user’s locale.
Interview Questions and Answers
Q1. What is the difference between internationalization and localization?
Internationalization is engineering the product so it can support any locale without code changes: externalized strings, locale-aware formatting, plural support, RTL-ready layouts, Unicode. Localization is adapting it to a specific locale: translating, formatting, and adjusting content, legal text, and features.
Q2. Why shouldn’t you concatenate translated strings?
Word order, grammar, and plural rules differ across languages, so fragments joined in code cannot be translated correctly. Use complete messages with named placeholders and ICU plural and select syntax.
Q3. How do plurals work across languages?
CLDR defines plural categories (zero, one, two, few, many, other), and each language uses a subset with its own rules. English uses one and other; Arabic uses all six; Russian and Polish use one, few, many, and other. ICU MessageFormat and Intl.PluralRules select the correct category for a number.
Q4. How do you format currency correctly for different users?
Use Intl.NumberFormat(userLocale, { style: 'currency', currency: accountCurrency }). The locale controls separators and symbol placement; the currency comes from the data. Respect currency minor units and keep amounts as decimal strings or minor-unit integers until formatting.
Q5. What bug occurs with new Date('2026-03-14'), and how do you avoid it?
Date-only ISO strings are parsed as UTC midnight, so in time zones west of UTC the displayed local date is the previous day. Treat business dates as plain date strings and format them without time zone conversion (parse into components or use Temporal.PlainDate or date-fns parseISO with local interpretation), and format instants with an explicit time zone.
Q6. How do you support right-to-left languages?
Set dir and lang on the document, use CSS logical properties and flex/grid so layouts mirror automatically, mirror directional icons, isolate bidirectional user content with <bdi> or dir="auto", and test with real RTL translations and native reviewers.
Q7. How do you detect a user’s locale?
Prefer the user’s saved preference, then a URL locale segment, then Accept-Language or navigator.languages, then a default. Do not rely on IP geolocation. Always provide an easy, persistent language switcher.
Q8. react-i18next or react-intl?
Both are solid. react-i18next offers flexible resource loading, namespaces, detection plugins, and a large ecosystem; react-intl is built on standard ICU MessageFormat with good extraction tooling and message descriptions. I would choose based on existing ecosystem, need for ICU syntax, and how translations are managed. With i18next, I would add the ICU plugin if translators work with ICU.
Q9. What is pseudo-localization?
Automatically transforming source strings with accents, padding, and markers to simulate translation. It reveals hard-coded strings, truncation and layout problems from text expansion, and encoding issues early, before real translations exist.
Q10. How do you handle text expansion?
Design flexible layouts without fixed widths or heights for text, allow wrapping, test with long locales like German and with pseudo-localization, avoid putting text in images, and give translators maximum lengths only when truly necessary.
Q11. How do you sort names correctly in multiple languages?
Use Intl.Collator with the user’s locale and appropriate sensitivity, rather than the default sort(), which compares UTF-16 code units. Collation rules differ by language, for example where accented characters and digraphs sort.
Q12. How would you organize translations in a micro frontend architecture?
Each micro frontend owns its namespaces and translation files, loaded on demand, while the shell owns the locale state, the i18n instance or configuration, and shared common strings. Locale changes propagate from the shell via props, context, or events, and each remote reacts by loading its namespaces for the new locale.
Q13. How do you avoid hard-coded strings slipping into the code base?
Lint rules that flag literal strings in JSX, code review checklists, pseudo-localization builds in QA, and extraction scripts in CI that fail when components reference missing keys.
Q14. What performance concerns come with i18n?
Large translation bundles, locale data for libraries, runtime message parsing, and repeated creation of Intl objects. Mitigate by loading only active locale namespaces per route, precompiling messages, caching files, and memoizing formatters.
Q15. How do time zones affect financial applications?
Transaction timestamps are instants and should be stored in UTC and displayed in the user’s zone. Cutoff times and business days are defined in the bank’s or payment network’s time zone and calendar, so they must be displayed explicitly with that zone. Statement and settlement dates are calendar dates without time zones.
Q16. What is BCP 47?
The IETF standard for language tags, combining language, optional script, region, variants, and extensions, such as en-US, zh-Hant-TW, or ar-EG-u-nu-arab. Intl APIs and the HTML lang attribute use these tags.
Q17. How do you localize content that contains links or formatting?
Use rich-text placeholders in the message (By continuing you agree to the <terms>terms</terms>) and map them to React components with Trans (react-i18next) or rich-text values (react-intl), so translators can move the link text while the code controls the element.
Q18. How do you test plural handling?
Render messages with counts that exercise each plural category for the target languages, such as 0, 1, 2, 3, 5, 11, 21, and 101, and verify output against expected translations. Automated checks in the TMS can verify all required plural forms are present.
Q19. How do you handle a missing translation in production?
Fall back to the default locale’s string (or a parent locale, like es for es-MX), never show raw keys to users, log the missing key with locale and route for monitoring, and treat it as a defect for the translation workflow.
Q20. What is Intl.Segmenter used for?
Locale-aware text segmentation into graphemes, words, or sentences. It is essential for correctly counting user-perceived characters (emoji and combined characters), truncating text safely, and finding word boundaries in languages without spaces such as Japanese and Thai.
Key Takeaways
- Internationalize once; localize many times. Externalize strings and use whole messages with ICU syntax.
- Use
Intlfor numbers, currencies, dates, lists, plurals, collation, and segmentation. - Separate the user’s locale from the data’s currency and the business time zone.
- Support RTL with logical properties and bidirectional isolation.
- Automate extraction, pseudo-localization, and translation management.
← Part 10: Accessibility and WCAG Compliance · All chapters · Part 12: Testing with Jest, React Testing Library, and Playwright →







