Interview Guide · 15 min read · Updated

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…

Featured image: Part 11, Internationalization and Localization

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-ES and es-MX share a language but differ in vocabulary, currency, and formats; en-US and en-GB differ 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:

  1. Externalizes all user-facing strings into resource files.
  2. Formats numbers, currencies, dates, times, lists, and units with locale-aware APIs.
  3. Handles plurals, gender, and grammatical variations through a message format, not string concatenation.
  4. Supports text expansion (German text can be 30 to 40 percent longer than English) and different scripts.
  5. Supports right-to-left layouts.
  6. Uses Unicode (UTF-8) everywhere and handles input in any script.
  7. Sorts and compares strings with locale-aware collation.
  8. Stores data in locale-neutral forms (ISO dates in UTC, decimal strings or minor units for money) and formats only at the edges.
  9. Detects, stores, and lets users change their locale preference.
  10. 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.NumberFormat knows the defaults (resolvedOptions().maximumFractionDigits).
  • Create formatters once and reuse them; constructing Intl objects is relatively expensive.
  • formatToParts returns 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-DD strings. Converting "2026-03-14" with new 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, and Temporal.Instant) fixes these modeling problems and is shipping in browsers; until it is universally available, use a polyfill or libraries like date-fns with date-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" and lang on <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:

  1. An explicit user choice saved in their profile (server-side) or a cookie.
  2. A locale segment in the URL (/es/accounts), which is good for public, indexable pages.
  3. The Accept-Language header (server) or navigator.languages (client).
  4. 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:

  1. Developers write source strings (usually English) with keys and descriptions.
  2. CI extracts new or changed strings and pushes them to a translation management system (TMS) such as Phrase, Lokalise, Crowdin, Smartling, or Transifex.
  3. Translators work with context (screenshots, descriptions, character limits). Banking content often requires legal and compliance review per locale.
  4. Completed translations are pulled back into the repository or loaded from a CDN at runtime.
  5. Missing translations fall back to the default language and are reported (logged in development, monitored in production).
  6. 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 Intl formatter 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_Angeles and TZ=Asia/Tokyo to 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-js to 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 Intl for 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 →

Senior Frontend Interview Guide (full PDF)Every question and answer from this series in one printable guide.PDF · 12.5 MB
Free download: unlock it in one step

Share this page

or

Mailchimp opens in a new tab to confirm. Unsubscribe any time.

Filed underInterview Guide
Rupak Nepali
Written byRupak Nepali

Author of four Opencart book.
The recent are Opencart 4 developer book and Opencart 4 user manual

Leave a Reply

Your email address will not be published. Required fields are marked *