SevyDevy

Guide · Frontend

Frontend Accessibility and i18n: Design for Every User

25 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • Frontend accessibility means building a web app people can use with a keyboard, a screen reader (software that reads the page aloud), zoomed text or shaky hands.
  • i18n, short for internationalisation, means building it so every word, number and date can change with the user's language. Plan both from the first screen, not after launch.
  • Start from native HTML elements, visible labels and a clear focus outline. Aim for WCAG 2.2 level AA, the middle of its three levels. WCAG is the checklist of testable accessibility rules from the W3C, the web's standards body.
  • For languages, keep words in per-language files, set the page's lang (language) and dir (reading direction) attributes, and format money, dates and plurals with JavaScript's built-in Intl tools.
  • Beginners can stop after the testing section. Engineers get tested gotchas. Chromium 141, the open-source base of Chrome, lacks Odia number data and silently uses another locale, Hindi treats zero as singular, and cutting text can split a Hindi letter.

What goes wrong when a web app ignores accessibility and language?

When a web app skips frontend accessibility and language support, people who cannot see well, cannot use a mouse or do not read English get stuck. They rarely file a bug report. They phone the helpline or give up.

Picture a common setup. A state pension office runs a web portal where retirees check their pension status and file their yearly life certificate, the proof that a pensioner is alive. It works in 12 Indian languages, including Urdu, which is written right to left. The setup is made up, but the problems are real.

The first version was built in English and translated in a hurry. About 2.1 lakh (210,000) retirees started using it, and the helpline began getting 1,800 calls a week. The team spent two weeks watching retirees use the portal at a pension camp. They found six problems.

  • A retired clerk who uses a screen reader heard only "button, button" in the header. The icon buttons had no names.
  • Pale grey hint text was almost invisible to people with cataracts.
  • Submit was a styled div, not a button, so keyboard users could not reach it. Cancel was a 16 by 16 pixel square that shaky hands kept missing.
  • The Odia screen cut words off, because the layout was sized for shorter English labels.
  • The Urdu screen showed right-to-left text inside a left-to-right layout, so the back arrow pointed the wrong way.
  • The pension amount showed as 32500.5 instead of a clear rupee figure, and dates appeared in the American month-first order.

None of these is a hard bug. Each came from a choice nobody made on purpose. The rest of this guide fixes them one by one, and every fix was run in a real browser.

What is frontend accessibility?

Frontend accessibility is the work of making a website usable by people with disabilities, including people who are blind, deaf, older, or have trouble moving their hands. It covers what the screen shows, how it responds and what it tells assistive technology.

Assistive technology is software or hardware that helps a person use a computer. A screen reader reads the page aloud. A screen magnifier enlarges part of it. Some people use only a keyboard, a switch or their voice.

Think of a ramp at a government office. A ramp drawn into the plan costs little. A ramp added after the stairs are built is costly and often too steep. Accessibility in a web app works the same way, and the picture maps part by part.

  • The ramp is native HTML: a real button already works with a keyboard, the way a ramp already works with a wheelchair.
  • The signboard with room for every language is the message catalog, the file that holds each language's words.
  • The raised floor path that a visually impaired visitor follows with a cane is the focus order, the path the Tab key takes.
  • Where the picture breaks: a building changes once in years, but a web app ships every week. So the checks must run on every change, not once at opening.

What is the difference between i18n and l10n?

i18n is engineering work done once so the app can support any language, while l10n is the work done for each language, such as translating text and checking local formats. You cannot localise well if you skipped i18n.

Internationalisation is written i18n because 18 letters sit between the i and the n. Localisation is written l10n for the same reason. The portal does i18n once and l10n twelve times.

A locale is a code for a language plus a region, such as hi-IN for Hindi in India or ur-IN for Urdu in India. These codes are BCP 47 language tags, the standard format JavaScript's Intl tools accept. The locale decides the words, the digits, the date order and sometimes the reading direction.

  • i18n work: move every string out of components, support right-to-left layouts, pick fonts for every script, and format numbers through the locale.
  • l10n work: translate each string, check it with native speakers, adapt examples and help text, and confirm that dates and money read naturally.
  • Shared work: decide which languages you support, how a user picks one, and what happens when a string is missing.

What does WCAG 2.2 ask a web app to do?

WCAG 2.2 asks that people can perceive, operate and understand a web app, and that it works with assistive technology. It turns those ideas into testable rules called success criteria, each with a level: A, AA or AAA.

WCAG stands for Web Content Accessibility Guidelines, published by the W3C, the body that writes web standards. Version 2.2 became a W3C Recommendation on 5 October 2023, and its current edition is dated 12 December 2024.

As of 19 October 2026, WCAG 3.0 is still a W3C Working Draft, dated 10 September 2026. So 2.2 is the version to build against today, and level AA is the usual target.

WCAG 2.2 added nine success criteria and removed one old rule, 4.1.1 Parsing. The table shows the rules that fixed the portal's problems.

WCAG 2.2 rules that fixed the pension portal
RuleLevelWhat it asksThe portal's fix
1.4.3 Contrast (Minimum)AAText contrast of at least 4.5:1; large text (18 point, or 14 point bold) needs 3:1Hint grey #aaaaaa (2.32:1) became #595959 (7.00:1)
1.4.4 Resize TextAAText can grow to 200 percent without losing content or functionSizes in relative units, tested at 200 percent zoom
2.1.1 KeyboardAEverything works through a keyboardThe Submit div became a real button
2.4.7 Focus VisibleAAKeyboard focus can be seenA 3 pixel dark outline on every focused control
2.4.11 Focus Not Obscured (Minimum)AA, new in 2.2A focused control is not entirely hidden by the page's own contentThe sticky header no longer covers focused fields
2.5.8 Target Size (Minimum)AA, new in 2.2Pointer targets at least 24 by 24 CSS pixels, the browser's layout unit, unless spacing or another exception appliesSubmit and Cancel grew to 44 pixels tall
3.1.1 and 3.1.2 LanguageA and AAThe page language, and any passage in another language, can be determined by softwarelang set on the page and on mixed-language parts
3.3.1 Error IdentificationAWhen an input error is detected automatically, it is identified and described to the user in textErrors name the field and say how to fix it
3.3.8 Accessible Authentication (Minimum)AA, new in 2.2No memory test or puzzle to log in, unless an alternative or a helping mechanism existsLogin by one-time code, and pasting the code is allowed
4.1.3 Status MessagesAAStatus messages reach assistive technology without moving focusThe Certificate filed banner is a status live region

The login fix needs care. The W3C's explanation of 3.3.8 says a service that requires manual transcription of a verification code is not compliant. So never block paste on the code field.

How do you make a web app work with a keyboard and a screen reader?

Use native HTML elements, give every control a name, keep a logical focus order, and show where focus is. Add ARIA only where HTML has no element for the job.

A screen reader normally works from the page's structure, not its pixels. The browser turns your HTML into an accessibility tree, a simpler copy of the page that lists each element's role, name and state. The operating system's accessibility interface then hands that tree to the screen reader.

Role means what something is, like button. Name is how it is referred to, like File life certificate. State is something like checked or expanded. Follow the diagram from top to bottom.

How a screen reader sees the pension portal

Notice that the screen reader gets only what the accessibility tree holds, so wrong markup means wrong speech.

Diagram connections:

  • Your HTML and ARIA to Browser
  • Browser to Accessibility tree (Builds)
  • Accessibility tree to OS accessibility API
  • OS accessibility API to Screen reader
  • Screen reader to Retiree hears the page (Speech or braille)
  • Your HTML and ARIA: button, label, lang, aria-live
  • Browser: Builds the page and its tree
  • Accessibility tree: Role, name and state of each element
  • OS accessibility API: Shares the tree with assistive tools
  • Screen reader: Uses lang to pick pronunciation rules
  • Retiree hears the page: File life certificate, button

We loaded the old screen in Chromium 141 and printed its accessibility tree. The header held two entries that said only button, with no name. That is all a screen reader can say. The styled Submit div showed up as plain text, not as a control.

Use native elements first

A real button element can take focus, activates with Enter and Space, and announces itself as a button. In the same Chromium test, pressing Enter and Space on the Submit div did nothing, and Tab skipped it. After it became a button, the same two key presses clicked it twice.

ARIA, short for Accessible Rich Internet Applications, is a set of HTML attributes that add roles, names and states to the accessibility tree. The W3C's ARIA Authoring Practices Guide says ARIA roles do not cause browsers to provide keyboard behaviours. Writing role=button on a div is a promise that you wrote that keyboard code yourself.

Give every control a name

Every form field needs a visible label tied to it with the label element. Every icon-only button needs a text name, from hidden text or an aria-label attribute. The portal's bell now announces Notifications, 2 new.

Keep focus visible and in order

Focus is the marker that shows which element will react to the keyboard. Focus order is the path the Tab key takes, and it should match the order people see.

  • Keep the order of elements in the HTML the same as the order on screen. In our Chromium 141 test, flex-direction row-reverse flipped the buttons on screen but not the Tab order.
  • Never remove the focus outline without adding a stronger one.
  • When a dialog opens, move focus into it. When it closes, send focus back to the button that opened it.
  • Add a Skip to main content link at the top, so keyboard users do not tab through the whole menu every time.

Announce changes that happen without a page load

When a retiree files a certificate, a green banner appears. A screen reader user hears nothing unless the banner sits in a live region, an element marked with role=status or aria-live that reads new text aloud without moving focus.

Put the empty live region in the page's first HTML, then change its text later. MDN says screen readers generally announce only changes to a region that already exists. A banner inserted along with its text may stay silent.

How do you design a frontend for many languages and scripts?

Keep words in message files, let layouts grow, set the page's lang and dir attributes, and load fonts per script. Never build sentences by gluing strings together in code.

Keep words out of components

Each piece of text gets a key, such as status.pending, and the component asks for the key. A message catalog is the file that maps keys to text for one locale. Translators edit the catalog, never the code.

Write whole sentences with placeholders, like Your pension of {amount} is due on {date}. Word order changes between languages. In Hindi the verb usually comes at the end, so gluing English-ordered parts produces broken grammar.

Let the layout grow

Translated text is usually longer than English. The W3C's article on text size in translation gives average expected figures: English strings of up to 10 characters expand by 200 to 300 percent when translated into European languages.

Avoid fixed widths and heights on anything holding text. Indic scripts also place vowel signs above and below letters, so a tight height can clip them. Let buttons wrap to two lines rather than cut a word.

Set lang and dir

The lang attribute tells browsers and assistive technology which language the page is in. The W3C advises putting it on the html element, not the body. WCAG's explanation of rule 3.1.1 says screen readers can then load the correct pronunciation rules.

The dir attribute sets reading direction, so the Urdu screen needs dir=rtl. Use CSS logical properties, such as margin-inline-start instead of margin-left, so spacing follows the direction. In Chromium 141, a 24 pixel margin-inline-start became margin-right when dir changed to rtl.

When a user arrives, the portal picks a locale in a fixed order.

How the portal picks a language

Notice that every path ends at Set lang and dir, so no screen can render without a language. A saved choice wins, then the browser list, then the regional default.

Diagram connections:

  • Retiree opens the app to Saved language choice?
  • Saved language choice? to Load that language bundle (Yes)
  • Saved language choice? to Browser language supported? (No)
  • Browser language supported? to Load that language bundle (Yes)
  • Browser language supported? to Use Odia (No)
  • Use Odia to Load that language bundle
  • Load that language bundle to Set lang and dir
  • Retiree opens the app: Phone or shared computer at a camp
  • Saved language choice?: From the account or this device
  • Browser language supported?: Checks each language in the browser list
  • Use Odia: Default for the portal's home state
  • Load that language bundle: Words, fonts and formats for one locale
  • Set lang and dir: dir is rtl only for Urdu

Here is that logic in TypeScript, cut to four locales. The saved choice comes from the account, and the browser list comes from navigator.languages in real use.

typescript
type Locale = "en-IN" | "hi-IN" | "or-IN" | "ur-IN";
const SUPPORTED: readonly Locale[] = ["en-IN", "hi-IN", "or-IN", "ur-IN"];
const RTL = new Set<Locale>(["ur-IN"]);

export function pickLocale(saved: string | null, browser: readonly string[]): Locale {
  for (const tag of [saved, ...browser]) {
    if (!tag) continue;
    const lang = tag.toLowerCase().split("-")[0];
    const match = SUPPORTED.find((l) => l.split("-")[0] === lang);
    if (match) return match;
  }
  return "or-IN";
}

export function applyLocale(locale: Locale): void {
  document.documentElement.lang = locale;
  document.documentElement.dir = RTL.has(locale) ? "rtl" : "ltr";
}

We compiled this with TypeScript 5.9.3 in strict mode and ran it in Chromium 141. A browser list of hi-US gave hi-IN, because the match compares only the language part. A list of fr-FR and de fell back to or-IN.

Load fonts only for scripts on the page

The portal's 12 languages use several scripts, and each script needs its own font file. The CSS unicode-range descriptor tells the browser which characters a font file covers. MDN says the browser downloads a file only if the page uses a character in its range.

We tested four font files, for Latin, Devanagari, Odia and Arabic script, on a page with only Hindi text. Chromium 141 fetched just two: Devanagari, and Latin because the spaces between words fall in its range.

How do you format money, dates and plurals in every language?

Use JavaScript's built-in Intl tools and pass the user's locale every time. Never write your own comma or plural rules, because they differ from language to language.

Intl is a set of formatting tools built into every modern browser. Intl.NumberFormat formats numbers and money. With en-IN it groups digits the Indian way, in lakhs and crores, so 123456 becomes 1,23,456. Intl.DateTimeFormat does the same job for dates.

Plurals are harder than they look. English has two forms, one payment and two payments. Japanese has one form and Arabic has six. Intl.PluralRules tells you which form a number needs, returning a tag such as one, few or other.

typescript
type Messages = Partial<Record<Intl.LDMLPluralRule, string>> & { other: string };

// locales: the user's locale first, then a fallback, e.g. ["or-IN", "en-IN"]
export function formatRupees(amount: number, locales: readonly string[]): string {
  return new Intl.NumberFormat(locales, { style: "currency", currency: "INR" }).format(amount);
}

export function pendingText(count: number, locales: readonly string[], msgs: Messages): string {
  const form = new Intl.PluralRules(locales).select(count);
  const template = msgs[form] ?? msgs.other;
  return template.replace("{count}", new Intl.NumberFormat(locales).format(count));
}

Each function takes a list of locales, with the user's locale first and Indian English after it. Each Intl tool picks the best locale in the list that it has data for. The ?? line falls back to the other form when a translator left one out.

Dates follow the same idea. For 19 October 2026, en-IN with dateStyle medium gave 19 Oct 2026, while en-US gave Oct 19, 2026. Passing the user's locale is what fixes the month-first dates retirees misread.

You can check the Indian grouping from any terminal with Node installed. On Node 22.22.0 this printed ₹32,50,000.00 [ 'or-IN', 'hi-IN' ]. The second part lists which of the two locales this runtime has data for.

bash
node -e 'const f = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" }); console.log(f.format(3250000), Intl.NumberFormat.supportedLocalesOf(["or-IN", "hi-IN"]))'

How do you ship translations without slowing the app down?

Split the words into one bundle per language and load only the one the user needs. Serve it from a CDN, a network of servers near users that keep copies of files. A user reading Telugu never downloads Urdu.

The portal has about 1,400 strings per language (made up). The build makes one small file per locale, with a name that changes whenever its text changes. Browsers can then keep each file for a long time safely.

Where the portal's words come from

Notice that the API never sends a sentence. Words live in catalogs and reach the browser one language at a time.

Diagram connections:

  • Translators and reviewers to Message catalogs (Edit text)
  • Message catalogs to Build step
  • Build step to CDN (One bundle per locale)
  • CDN to Portal in the browser
  • Pension status API to Portal in the browser (Status code and amount)
  • Translators and reviewers: Native speakers for each of 12 languages
  • Message catalogs: One file of keys and text per locale
  • Build step: Checks for missing keys, splits by locale
  • CDN: Caches language bundles and script fonts
  • Portal in the browser: Loads one locale, formats with Intl
  • Pension status API: Sends codes, amounts and dates only
  • Load the language bundle in parallel with the main code, so picking a language does not add a second wait.
  • Fall back key by key. If one Odia string is missing, show the English one and log the missing key.
  • Fail the build when a key is missing from the English catalog, since that is a bug, not a translation gap.

How do you test accessibility and translations before release?

Test in layers: automated checks on every change, a fake test language for layout problems, a manual keyboard and screen reader pass, and sessions with real users. No single layer catches everything.

We ran axe-core 4.14.0, a widely used open-source accessibility checker, on the old screen in Chromium 141. It flagged the two nameless buttons, the low-contrast grey and the missing lang attribute.

It did not flag the Submit div, which keyboard users could not reach. It also accepted the field's placeholder as its name, though that hint vanishes once a retiree starts typing.

Then we built a form that axe passed with zero violations. Its fields were labelled Field 1 and Field 2, and CSS showed the buttons in reverse order, so Tab moved against what people saw. Its only error message was Error 4021.

You can check a colour pair yourself with the WCAG contrast formula. Save this as contrast.ts, run npx -p typescript tsc contrast.ts, then node contrast.js. Expect 2.32 for the old grey and 7.00 for the new one.

typescript
// WCAG 2.2 contrast ratio for two "#rrggbb" colours
function luminance(hex: string): number {
  const [r, g, b] = [1, 3, 5].map((i) => {
    const c = parseInt(hex.slice(i, i + 2), 16) / 255;
    return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function contrastRatio(text: string, background: string): number {
  const [hi, lo] = [luminance(text), luminance(background)].sort((a, b) => b - a);
  return (hi + 0.05) / (lo + 0.05);
}

console.log(contrastRatio("#aaaaaa", "#ffffff").toFixed(2));
console.log(contrastRatio("#595959", "#ffffff").toFixed(2));

Normal text needs 4.5 or more, so the old grey fails and the new one passes with room to spare.

This matches what the tool's makers say. The axe-core README claims it finds on average 57 percent of WCAG issues automatically. The W3C's guide to evaluation tools says tools cannot determine accessibility and human judgement is required.

A pseudo-locale is a fake language used only for testing. The portal's version swaps each letter for an accented look-alike and pads every string, so clipped words and hard-coded English show up before real translations exist.

  1. Automated checks on every change

    Run axe-core in the test suite. Fail the build on any violation, and treat incomplete results as items for a person to review.

  2. Pseudo-locale screenshots

    Render each new screen in the pseudo-locale and in Urdu. Look for cut-off words, English that never moved to a catalog, and arrows pointing the wrong way.

  3. Manual keyboard pass

    Unplug the mouse and file a certificate with Tab, Enter and Space only. Every control must be reachable, and focus must stay visible.

  4. Screen reader and zoom pass

    Repeat the task with a screen reader in Hindi and in Urdu. Then zoom to 200 percent and check that nothing is cut off.

  5. Real users

    Before each release, ask a few retirees to finish the task alone, each in their own language.

After six months, helpline calls fell from 1,800 to about 700 a week. The share of retirees filing without help rose from 61 to 84 percent (made up).

Which accessibility and i18n details catch out experienced engineers?

The surprises live in browser data and in tooling gaps, not in the WCAG text. Each row below was run as of 19 October 2026 in Playwright 1.56.0's headless Chromium 141.0.7390.37 and in Node 22.22.0. Chrome 141 reached stable on 30 September 2025, so newer builds may differ. Node uses ICU 77.1, the Unicode library that supplies Intl's locale data.

Tested results, 19 October 2026
BehaviourWhat we sawWhat to do
Missing locale dataChromium 141 has no Odia number data, so new Intl.NumberFormat("or-IN") used the browser's default locale. For 250000 an en-US default printed 250,000 and an en-IN default printed 2,50,000. Node gave 2,50,000.Pass a list such as ["or-IN", "en-IN"]. Chromium then gave 2,50,000 under every default locale we tried. Its PluralRules still chose or, because it has Odia plural data.
Silent fallbackIn Chromium 141, Assamese, Maithili, Sanskrit, Kashmiri, Konkani and seven other Indian locales fell back to the browser's default locale, en-US in our test, without any error.Run Intl.NumberFormat.supportedLocalesOf in each target browser before you promise a language.
Partial dataChromium 141 resolved pa-IN to pa and wrote the month of October as M10.Check resolvedOptions().locale and a sample date, not only the number.
Default digitsur-IN printed Extended Arabic-Indic digits, mr-IN and bn-IN printed their own script's digits, and hi-IN printed Latin digits.Ask users which digits they read. Add -u-nu-deva to hi-IN for Devanagari digits.
Zero is singular in HindiIntl.PluralRules("hi").select(0) returned one, as did Bengali, Gujarati, Kannada, Punjabi and Assamese. English returned other.Write the one form so it also reads well for zero.
Rounding moneymaximumFractionDigits 0 turned 32500.5 into ₹32,501.Keep two decimals for amounts with paise, or round on the server first.
Odia money groupingWith Odia data, Node 22 formatted 250000 rupees in or-IN as ₹250,000.00, grouped in thousands. So formatRupees(250000, ["or-IN", "en-IN"]) gave ₹2,50,000.00 in Chromium 141 but ₹250,000.00 in Node.If every screen should show lakh grouping, format money with en-IN. Check the Odia screen in each target runtime.
Text differs by runtimeHindi October came out as अक्तूबर in Chromium 141 and अक्टूबर in Node 22.Do not compare formatted strings in tests run on a different runtime from users.
Splitting a letter"हिन्दी" has 6 code units but 2 grapheme clusters, the units a reader sees as letters, under rules added in Unicode 15.1, which both runtimes follow. slice(0, 3) cut it mid-cluster.Truncate with Intl.Segmenter using grapheme granularity. Runtimes on older Unicode rules split न्दी into two clusters.
Live region defaultsChromium 141 exposed role=status as live polite with atomic true, before any text was added.Keep the region in the first HTML and change only its text. MDN also advises a redundant aria-live=polite with role=status to maximise compatibility.

To check your own browser, open the developer console and run Intl.NumberFormat.supportedLocalesOf(["or-IN", "hi-IN"]). Chromium 141 returned only hi-IN. A locale missing from the answer falls back to the next locale in your list, or else to the browser's default locale.

How do you explain frontend accessibility and i18n in a system design interview?

Present them as requirements you set at the start, then show where each one changes your components, your data and your build. Interviewers listen for concrete choices, not a promise to add them later.

  1. Ask who the users are

    Ask about age, devices, networks, disabilities and languages. For the pension portal that means older users, cheap phones and 12 languages, one of them right to left.

  2. Set the targets

    Name WCAG 2.2 level AA and the supported locales. Pick the two or three rules that matter most for these users, such as target size and contrast.

  3. Design the component layer

    Shared components use native elements, carry their own labels and focus styles, and use logical CSS properties. Every screen then inherits them.

  4. Design the text pipeline

    Keys in components, catalogs per locale, a build step that splits them, and an API that sends codes. Mention plural rules and Intl formatting with a fallback locale.

  5. Cover performance

    One bundle per locale, fonts split by unicode-range, long cache times on a CDN, and a key-by-key fallback when a string is missing.

  6. Explain testing

    Automated checks on every change, a pseudo-locale, a manual keyboard and screen reader pass, and sessions with real users.

Questions people ask

Do I need ARIA to make a website accessible?
Usually only a little. Native HTML elements such as button, a, input and label already give screen readers the right role and name, and buttons and links come with keyboard behaviour. Use ARIA when HTML has no element for your widget, such as tabs, or for live regions. ARIA roles do not add keyboard behaviour, so every role you add means writing that support yourself.
Can automated tools find all accessibility problems?
No. The axe-core README claims about 57 percent of WCAG issues on average. In our Chromium test, axe-core 4.14.0 found nameless buttons, low contrast and a missing lang attribute. It passed a form with labels like Field 1, reversed visual order and an error that said only Error 4021. Pair tools with keyboard, screen reader and real-user testing.
What is a pseudo-locale?
A pseudo-locale is a fake language used only for testing. It replaces letters with accented look-alikes and makes strings longer, so you can spot clipped text and strings never moved into a catalog. You can run it before any real translation exists, so layout bugs show up while the screen is still being built.
How do I support right-to-left languages like Urdu?
Set dir=rtl on the html element for right-to-left locales, and use CSS logical properties such as margin-inline-start and padding-inline-end. In Chromium 141 both flipped sides when dir changed. Also check icons that show direction, such as back arrows. Test with real text, because mixed Urdu and English runs need care.
Should a language picker use country flags?
No. A flag stands for a country, while many countries use several languages and one language can span many countries. Show each language name in its own script, such as हिन्दी for Hindi. Intl.DisplayNames can supply these names, but Chromium 141 has no Odia data for it. It named Odia in the browser's default language, English in our test, so keep your own list.
Is accessibility only for blind users?
No. Accessibility covers people who are blind or have low vision, people who are deaf, people with limited hand movement, and people with memory or reading difficulties. It also helps older users, people on small screens and anyone in bright sunlight. The pension portal's biggest wins came from larger buttons and clearer errors.
When should a team start on i18n?
Start on day one, even if you launch in one language. Moving strings into catalogs and formatting with Intl costs little at the start. Doing it after a year means hunting hard-coded text across every component, and rewriting sentences that were glued together from parts.

Sources and further reading

Practise this with Lala

Practise