Skip to content
Published on

Accessibility & Internationalization 2025 — WCAG 2.2, EU Accessibility Act, ARIA, next-intl, Korean/Japanese Typography, RTL (S6 E7)

Authors

Prologue — "performance looks great, but it's unusable"

Core Web Vitals can be bright green while the screen reader cannot read a list and Korean versions break on particles and line breaks. That's when the team says: "we should have designed this from the start."

Three 2025 inflection points:

  1. EU Accessibility Act (effective 2025-06-28) — digital products in EU (web, app, ecommerce, e-books, ATMs, transit) must meet WCAG. Non-compliance → withdrawal from market.
  2. WCAG 2.2 W3C Recommendation (2023-10) — touch target sizes, drag alternatives, cognitive-load auth. 2025 public / enterprise procurement baseline.
  3. AI-assisted translation reached "good enough", but Korean particles, Japanese honorifics, and cultural nuance still need humans.

Korea enforces Anti-Discrimination Against Disabled Persons Act + e-Gov Act for public/finance/education. This is quality investment: SEO + retention + new users + lower legal risk.


1. Redefining accessibility — "quality for everyone"

2025 definition covers six situations:

  1. Permanent — vision, hearing, motor, cognitive.
  2. Temporary — broken arm, post-surgery, medication side effects.
  3. Situational — sunlight, noise, one hand holding a baby.
  4. Aging — gradual sensory decline.
  5. Device diversity — small phones, huge TVs, low-end devices, slow networks.
  6. Language / culture — non-English, minor languages, literacy.

~16% (1.3B) have permanent disabilities. Counting the rest — nearly everyone needs accessibility at some moment.

Accessibility also improves SEO, conversion, and reduces churn. Google found that complete localization increases purchase intent by 72%.


2. WCAG 2.2 — the 2025 baseline

POUR principles

  1. Perceivable — every info perceivable.
  2. Operable — every control operable.
  3. Understandable — info & operation.
  4. Robust — parseable by assistive tech.

Levels

  • A — minimum. AA — industry standard (EU Act, KWCAG, US 508 all base on this). AAA — high.

WCAG 2.2 new success criteria (most impactful)

  • 2.4.11 Focus Not Obscured (Min) — sticky headers mustn't cover focus.
  • 2.4.13 Focus Appearance:focus-visible minimum contrast/size.
  • 2.5.7 Dragging Movements — non-drag alternative for drag-only UIs (Kanban).
  • 2.5.8 Target Size (Min) — 24×24 CSS px minimum.
  • 3.2.6 Consistent Help — consistent help/contact location.
  • 3.3.7 Redundant Entry — don't re-ask already-entered info.
  • 3.3.8 Accessible Authentication — no cognitive-puzzle auth (Passkey/WebAuthn surge).

Target size affects nearly every mobile icon button and close (×).


3. Semantic HTML — escape the <div> republic

Assistive tech reads semantics. <div>s are read as plain text.

Bad

<div class="button" onclick="submit()">Confirm</div>
<div class="list"><div class="item">Item 1</div></div>

Good

<header>
  <nav aria-label="Primary">
    <ul><li><a href="/">Home</a></li></ul>
  </nav>
</header>
<main>
  <article><h1>Title</h1><p>Body</p></article>
</main>
<button type="submit">Confirm</button>
<ul><li>Item 1</li></ul>

Screen readers announce landmarks, list counts, button roles, and heading navigation automatically.

<button> vs <a>

  • Action<button>. Navigation<a href>. Do not swap.

4. ARIA — powerful, dangerous

"No ARIA is better than bad ARIA."

Use ARIA when

  1. Native HTML can't express the UI (tabs, combobox, tree).
  2. Dynamic state (aria-expanded, aria-selected, aria-busy).
  3. Name/description (aria-label, aria-labelledby, aria-describedby).
  4. Live regions (aria-live).

Common patterns

<button aria-label="Close menu"><svg aria-hidden="true">...</svg></button>
<button aria-expanded="false" aria-controls="menu-1">Menu</button>
<ul id="menu-1" hidden>...</ul>
<div aria-live="polite">Saved.</div>
<div aria-live="assertive" role="alert">Error: wrong password</div>
<input aria-invalid="true" aria-describedby="email-error" />
<span id="email-error" role="alert">Enter a valid email</span>

ARIA anti-patterns

  1. <div role="button"> — use <button>.
  2. role="list" on <div> — use <ul>.
  3. aria-hidden="true" on <main> — hides the whole app.
  4. aria-hidden on focusable elements — contradiction.
  5. aria-label="image" on informational images — use <img alt>.

5. Keyboard navigation

Unplug your mouse for 30 minutes; problems surface instantly.

Required keys

Tab, Shift+Tab, Enter/Space, Esc, arrow keys within widgets, Home/End.

Focus visibility — :focus-visible

button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
button:focus:not(:focus-visible) { outline: none; }

Focus trap

Use native <dialog>; showModal() auto-traps. For custom modals use focus-trap.

<a href="#main" class="skip-link">Skip to main</a>

Offscreen by default, visible on focus.


6. Screen reader testing

SRPlatformShare (WebAIM)
NVDAWindows67%
JAWSWindows30%
VoiceOvermacOS/iOSbuilt-in
TalkBackAndroidbuilt-in

VoiceOver shortcuts (macOS)

  • Cmd+F5 toggle.
  • Ctrl+Option+Arrow navigate.
  • Ctrl+Option+U rotor (landmarks/headings/links).

Test checklist

  1. Tab reaches every interactive element?
  2. Focus order matches visual order?
  3. Roles announced correctly?
  4. Labels bound to inputs?
  5. Error messages live-announced?
  6. Modal focus management works?
  7. Dynamic content announced?

7. Color, contrast, motion

Contrast (AA)

  • Normal text 4.5:1, large text 3:1, UI borders/icons 3:1.

Never rely on color alone

Pair color with icon + text label.

prefers-reduced-motion

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

200% zoom

Use rem/em, max-width for line length. No fixed-px critical text.


8. Form accessibility

Label must bind to input

<label for="email">Email</label>
<input id="email" type="email" required />

Placeholder is not a label

Disappears on input, insufficient contrast, memory burden. Always separate label.

Error messaging

<input id="pw" aria-invalid="true" aria-describedby="pw-help pw-error" />
<span id="pw-help">8+ chars, letters + digits</span>
<span id="pw-error" role="alert">Password too short</span>

Autocomplete

autocomplete="email" | "tel" | "current-password" | "new-password" | "name" | "postal-code" — helps password managers and assistive tech. WCAG 1.3.5.


9. i18n basics

  • i18n — design to support locales.
  • l10n — actual translation/adaptation.
  • g11n — combined business strategy.

Five axes

  1. Text translation.
  2. Formatting (numbers, dates, currency).
  3. Collation (sort order).
  4. Directionality (LTR/RTL).
  5. Layout (German expands ~30%).

Framework landscape 2025

  • next-intl — Next.js App Router de facto.
  • react-intl / FormatJS — React + ICU.
  • i18next — framework-agnostic.
  • vue-i18n, Svelte-i18n / Paraglide, Lingui, @angular/localize.

10. next-intl in practice

// i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
export default getRequestConfig(async ({ requestLocale }) => {
  const locale = (await requestLocale) ?? 'en'
  return { locale, messages: (await import(`../messages/${locale}.json`)).default }
})
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
export default async function Layout({ children, params }) {
  const { locale } = await params
  const messages = await getMessages()
  return <html lang={locale}><body><NextIntlClientProvider locale={locale} messages={messages}>{children}</NextIntlClientProvider></body></html>
}
import { getTranslations } from 'next-intl/server'
export default async function Home() {
  const t = await getTranslations('home')
  return <main><h1>{t('title')}</h1></main>
}

ICU MessageFormat

{
  "cart": "{count, plural, =0 {Empty} one {# item} other {# items}}",
  "welcome": "Hello, {name}!"
}

Plurals, gender, select — handled in the message, not in JS.


11. Locale-aware formatting — Intl API

Intl.NumberFormat

new Intl.NumberFormat('en-US').format(1234567) // "1,234,567"
new Intl.NumberFormat('de-DE').format(1234567) // "1.234.567"
new Intl.NumberFormat('hi-IN').format(1234567) // "12,34,567"
new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(29000) // "₩29,000"

Intl.DateTimeFormat, Intl.RelativeTimeFormat, Intl.ListFormat, Intl.Collator — never manually format dates/lists. Use Intl + Temporal API (Stage 3).


12. Korean / Japanese specifics

Korean particles

// Hardcoded "을(를)" burns users
function suffix(word: string, withBatchim: string, without: string) {
  const last = word.charCodeAt(word.length - 1)
  const hasBatchim = (last - 0xac00) % 28 !== 0
  return hasBatchim ? withBatchim : without
}
// suffix('사과', '을', '를') → '을'

Toss's es-hangul bundles particle handling, jamo decomposition.

Korean line breaking

body { word-break: keep-all; overflow-wrap: break-word; line-break: strict; }

keep-all avoids mid-word breaks in CJK. Supported everywhere in 2025.

Japanese vertical writing

.tategaki { writing-mode: vertical-rl; text-orientation: mixed; font-family: 'Yu Mincho', serif; }

CJK font optimization

  • Korean: Pretendard (7MB → 100KB subset).
  • Japanese: Noto Sans JP, Yu Gothic.
  • Chinese (SC/TC): Noto Sans SC/TC.
  • Subsetting is mandatory.

RTL

<html lang="ar" dir="rtl">...</html>

Use logical CSS properties (margin-inline-start, padding-inline-end) — auto LTR/RTL.


13. Checklist + anti-patterns

Pre-launch a11y checklist (15)

  1. Meaningful alt on every image (decorative: alt="").
  2. All inputs labeled.
  3. Contrast ≥ 4.5:1.
  4. One <h1> per page; no skipped heading levels.
  5. Full keyboard operability.
  6. Visible :focus-visible.
  7. Touch targets ≥ 24×24.
  8. Modal focus trap + Esc close.
  9. Skip link.
  10. prefers-reduced-motion honored.
  11. Live regions for dynamic content.
  12. Lighthouse a11y score 100.
  13. axe-core / Pa11y in CI.
  14. Manual SR testing.
  15. Real users with disabilities.

Top 10 a11y / i18n anti-patterns

  1. <div onclick> sprawl.
  2. outline: none without :focus-visible.
  3. Placeholder instead of label.
  4. alt="image of ..." (redundant).
  5. aria-hidden on focusable.
  6. Color-only info.
  7. Autoplay audio/video.
  8. String concat "Hi " + name (particles, word order).
  9. Manual date/currency formatting (use Intl).
  10. Bulk Google Translate without review.

Next episode

Season 6 Episode 8: Frontend Monitoring & Error Tracking 2025 — Sentry, Datadog RUM, PostHog, LogRocket, Session Replay, Source Maps, AI anomaly detection.

"Accessibility isn't a favor for a minority. It's quality for everyone. i18n isn't translation — it's respect for people with different rhythms of life."

— End of Accessibility & Internationalization.