# Internationalisation (i18n)

taga11y renders and announces thirteen user-facing strings. By default they are
the built-in English values. The `i18n` option swaps in another locale,
declares the widget's language to screen readers (`lang`), and optionally sets
text direction (`dir`) for RTL layouts.

`i18n` is **init-only**. It is ignored by `settings()`; to switch locale at
runtime, call `destroy()` then construct a new instance.

## The string map

All slots are flat dot-keys. Eleven are plain strings; `a11y.resultsCount` and
`a11y.tagsSummary` are CLDR plural-forms objects resolved per-locale via
`Intl.PluralRules`.

| Key | English default | Placeholders |
|---|---|---|
| `a11y.tagAdded` | `Tag added: {label}` | `{label}` |
| `a11y.tagRemoved` | `Tag removed: {label}` | `{label}` |
| `a11y.tagsCleared` | `Tags cleared` | — |
| `a11y.noResults` | `No results` | — |
| `a11y.resultsCount` | `{ one: "{n} result available", other: "{n} results available" }` | `{n}` |
| `a11y.tagsSummary` | `{ one: "{n} tag selected: {labels}", other: "{n} tags selected: {labels}" }` | `{n}`, `{labels}` |
| `a11y.selectedTags` | `Selected tags` | — |
| `a11y.removeTag` | `Remove {label}` | `{label}` |
| `ui.loading` | `Loading suggestions…` | — |
| `error.duplicate` | `Already added: {label}` | `{label}` |
| `error.maxReached` | `Maximum tags reached` | — |
| `error.notInList` | `Not in the list` | — |
| `error.loadError` | `Failed to load suggestions` | — |

`a11y.tagsSummary` is pushed through the existing polite live announcer
(`aria-live="polite"`) when the combobox input gains focus with one or more
tags selected. Empty selection: no announcement is made (the AT speaks only
the field name). `{labels}` is the locale-formatted conjunction list produced
by `Intl.ListFormat(locale, { type: 'conjunction', style: 'long' })` — e.g.
`en` yields `"Apple, Banana, and Cherry"`, `pt` yields `"Apple, Banana e
Cherry"`, `ar` uses Arabic conjunction.

### Count-only override

The English default is intentionally verbose so screen-reader users get full
parity with what a sighted user sees instantly next to the cursor. At
typical small selections (1–5 tags) this is barely longer than count-only and
removes the "what's in there?" navigation step. If your application
routinely shows ~8 or more selected tags, reading every label on focus becomes
long — override the slot with a template that omits `{labels}`:

```ts
i18n: {
  locale: 'en',
  strings: {
    'a11y.tagsSummary': {
      one: '{n} tag selected',
      other: '{n} tags selected',
    },
  },
}
```

The count and plural resolution still work; the per-label readout is
suppressed. The default remains verbose because most consumers will not opt
in, and the SR-parity property of this change should hold out of the box.

Reference translations ship in `locales/pt.json` (Portuguese) and
`locales/ar.json` (Arabic). They are **not** bundled — copy them, fetch them,
or import them. Both files include a translated `a11y.tagsSummary` (Portuguese
`one`/`other`; Arabic `one`/`two`/`few`/`many`/`other`).

## Partial overrides

`strings` accepts a `Partial<I18nStrings>`. Any missing key falls back to the
English default and a single `console.warn` (dev signal only) lists the
fallen-back keys at init. A supplied `a11y.resultsCount` **wholly replaces**
the English default for that slot and must include `other`; absent CLDR
categories fall back to that object's own `other` (never to English — mixing
languages across plural categories is disallowed by design).

## ESM import (bundlers)

```ts
import { Taga11y } from 'taga11y';
import ptStrings from 'taga11y/locales/pt.json';

new Taga11y(document.getElementById('tags'), {
  suggestions: ['Maçã', 'Banana', 'Cereja'],
  i18n: { locale: 'pt', strings: ptStrings },
});
```

## IIFE — fetch then await

```html
<script src="https://unpkg.com/taga11y/dist/taga11y.iife.js"></script>
<script>
  (async () => {
    const strings = await fetch('./locales/ar.json').then((r) => r.json());
    new taga11y.Taga11y(document.getElementById('tags'), {
      suggestions: ['تفاح', 'موز', 'كرز'],
      i18n: { locale: 'ar', dir: 'rtl', strings },
    });
  })();
</script>
```

## IIFE — fetch chained

```html
<script src="https://unpkg.com/taga11y/dist/taga11y.iife.js"></script>
<script>
  fetch('./locales/pt.json')
    .then((r) => r.json())
    .then((strings) => {
      new taga11y.Taga11y(document.getElementById('tags'), {
        i18n: { locale: 'pt', strings },
      });
    });
</script>
```

## `lang` and `dir`

- `lang` is stamped on the wrapper only when the widget locale differs from
  the document's primary language subtag (`document.documentElement.lang`),
  or when the document declares no language. `"pt-BR"` matches `"pt"` at the
  primary-subtag level.
- `dir` is stamped on the wrapper only when explicitly provided. Otherwise
  direction cascades from the parent hierarchy (most RTL pages set
  `<html dir="rtl">` and the widget inherits it). RTL layout is driven by the
  CSS `:dir(rtl)` pseudo-class.

## Known limitations

- **Partial fallback TTS mismatch.** English fallback strings under a
  non-English `lang` may be read by the wrong TTS voice. This is surfaced by
  the init `console.warn` and is a developer error, not a library bug.
- **Voice control language.** When aria-labels are translated, voice-control
  commands must be issued in the widget's configured language.
- **Browser baseline.** `:dir()` and `Intl.PluralRules` require Chrome 120+,
  Firefox 120+, Safari 17+, Edge 120+. Older browsers receive LTR-only layout.
