---
id: "packages/active-i18n/changelog"
title: "Changelog"
description: "What changed for users of active-i18n, per version."
url: "/packages/active-i18n/changelog"
locale: "en"
revision: "bf32a4b9ea134e3c"
---

# Changelog

> [!NOTE]
> **Release state**
>
> The version line at the top of this page is read from the package and the
> release register of Foundation. A version heading in the changelog below is
> not a release by itself: only a recorded release is. The section Unreleased
> lists what changed since the last version, including the changes made since
> Foundation maintains the package.

Notable changes to `@workspace/active-i18n`. Format based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning per
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

**This changelog starts on 2026-10-18.** The manifest already carried version
1.1.0, but no release of this package was ever recorded: there is no earlier
changelog and no release heading, so it is not known which content 1.1.0 once
named. The history before this file is not reconstructed here; the completed
milestones are in `DONE.md` and the durable decisions in
`DECISIONS.md`, dated, but not tied to a version. The first
release heading below, `[1.1.0]`, is the first release of this package.

---

## \[Unreleased\]

### Maintained in Foundation (since 2026-10-21)

- **A lint warning in a test:** `language-names.test.ts` types its stub with
  `typeof Intl.DisplayNames` instead of an unused variable. No code changed.
- **Documentation:** references to documents of the source repository are
  text, marked `(source repository: …)`, instead of links that resolve nowhere here.

### Added (2026-10-19, active-ui ADR-039)

- `getCurrencyOptions(codes, displayLocale, {priorityCodes?, current?})` with
  the types `CurrencyOption` and `CurrencyOptionsConfig`, from the root: the
  options of a currency picker (priority first, then by name; read tolerance
  and a server-resolved name for the stored code). Replaces the option
  assembly that `apps/admin` carried itself.

### Changed (Accept-Language candidates, 2026-10-19: D33)

- **Breaking: `getAcceptLanguageCandidates()` returns full canonical tags**
  (`en-US`, `zh-Hant-TW`) instead of lower-case primary subtags. Match them
  with `lookupFirstSupportedLocale`; an exact comparison with primary tags no
  longer finds `en` in `en-US`.
- The header reading allows whitespace around `;` and `=`, ignores parameters
  other than `q`, and drops an invalid qvalue, `q=0`, `*` and a tag `Intl`
  refuses; a repeated tag keeps its highest quality; at most 32 entries.

### Added (D33)

- `lookupSupportedLocale` and `lookupFirstSupportedLocale` on the root: the
  RFC 4647 lookup of the runtime, public and framework-free.

### Removed (D33)

- `normalizeLocale` (internal helper; it had no user left).

### Changed (re-check remediation, 2026-10-19: D32)

- **`LocaleSwitchOutcome` gains `'declined'`, `'navigation-failed'` and
  `'navigation-guarded'`.** An exhaustive `switch` over the outcome adds
  them; a caller that only tests for `'switched'` needs nothing.
- **`setLocale()` never throws and its promise never rejects**, also when the
  host's guard, writer or logger throws, or when starting the navigation
  throws. A failure after a successful write is `'navigation-failed'`.
- **`'switched'` now means the browser is leaving the page**: the outcome
  follows the page's own `beforeunload` instead of the return of
  `location.replace()`. A test with a `location.replace` double dispatches
  `beforeunload` from it.

### Added

- **`confirmLocaleSwitch`** on `<I18nProvider>`: the host's guard, asked
  before anything is stored; `false`, a throw or a rejection resolves
  `'declined'`.

### Fixed

- **A callable thenable is awaited** (PKG-RECHECK-001): any object or
  function returned by `persistLocale` is assimilated like `Promise.resolve`.
- **The error boundary** (PKG-RECHECK-002): a throwing `then` getter is a
  persistence failure; a throwing logger never changes an outcome.
- **A `beforeunload` objection** (PKG-RECHECK-003) no longer leaves
  `<html lang>` on the new language and the selector locked: the page stays
  consistent and accepts a new choice (`'navigation-guarded'`).

### Changed — BREAKING (independent review remediation, 2026-10-19: D29, D30, D31)

- **React 19 is required by `/react`** (D30): the `react` peer is `^19.0.0`
  (was `^18.0.0 || ^19.0.0`, which crashed on React 18 at `React.use`), and
  the `@types/react` peer `^19.0.0` (was `^19.3.0`). The root needs no
  React.
- **`setLocale(locale)` returns `Promise<LocaleSwitchOutcome>`** (D31), was
  `boolean`. The promise never rejects; `'switched'` replaces `true`, and the
  refusals `'disabled'`, `'unchanged'`, `'unavailable'` replace `false`. New:
  `'pending'` (a switch is already under way) and `'persist-failed'`.
- **`persistLocale` may return a promise** (D31): the provider awaits it and
  navigates only on success; a throw or rejection does not navigate and is
  reported as `Locale persistence failed`.
- **`createServerTranslations` refuses a non-canonical, malformed or repeated
  `supportedLocales` entry** (D29), and a fallback not spelled as an entry.

### Fixed

- **Regional and script locales on the server** (D29, PKG-ARCH-001): a
  supported `en-GB`, `pt-BR`, `pt-PT` or `zh-Hant` loads itself instead of
  the fallback. The request is canonicalised, the exact tag wins, and only
  then is it made less specific; it never reaches a sibling region or script.
- **Navigation before asynchronous persistence** (D31, PKG-ARCH-003): the
  order is now `persist-start → persist-complete → navigate`.

### Added

- The type `LocaleSwitchOutcome` (`@workspace/active-i18n/react`).

### Migration (D29 to D31)

| Before | After |
| --- | --- |
| `if (setLocale(code)) …` | `if ((await setLocale(code)) === 'switched') …` |
| `onClick={() => setLocale(code)}` | unchanged, or `void setLocale(code)` to mark the ignored outcome |
| `persistLocale={async (l) => { await save(l) }}` | unchanged; it is now awaited, and a rejection no longer navigates |
| `supportedLocales: ['pt-br']` | `supportedLocales: ['pt-BR']` (the error names the canonical form) |
| React 18 | React 19 (`^19.0.0`); with active-ui its own floor `^19.2.4` |

### Changed — BREAKING (standalone product, D28, platform ADR-114)

- **`createServerTranslations(source, options)`**: `options` is required and
  carries `supportedLocales` and `fallbackLocale` (previously read from
  `@workspace/active-config`); an empty set or a fallback outside it throws.
  New optional `logger` (a factory `(source) => {warn, error}`).
- **`<I18nProvider>`**: `cookieName` and `cookieDomain` are removed; the new
  `persistLocale?: (locale) => void` receives a language switch, and the new
  `logger?` factory receives the provider's reports. Without them nothing is
  stored or reported.
- **`getLanguageName` / `getStandaloneLanguageName`**: an optional third
  argument `knownLabels` replaces the built-in fallback onto the platform
  language set; without it an unnamed language shows its code in upper case.
- Manifest: no `@workspace/active-config`, `@workspace/active-logging` or
  `@workspace/active-types`; `date-fns` and `@date-fns/tz` declared because the
  closure reaches them through active-format; `next` no longer a devDependency
  (an optional peer only), `@types/react` an optional peer; tsconfig
  self-contained.

### Added

- The types `KnownLanguageLabels`, `I18nLogger` and `I18nLoggerFactory`.
- Registration as a reusable product (`.`, `./react`; `./server` and
  `./server/get-locale` as Next framework integrations).

### Removed — BREAKING (formatting moved to `@workspace/active-format`)

- **The `./formatters` entry.** Every export of it now lives, under the same
  name and with the same behaviour, in `@workspace/active-format` (its one
  root entry). There is no second path and no re-export.
- **`LocaleCode` from the root.** The open BCP 47 tag is owned by
  `@workspace/active-format`; this package imports it from there.
- The compatibility selection `formatters` of the reusable package transfer
  ends with this change: no reusable product reaches this package any more,
  and its register row is removed. The recorded release
  `1.1.0-compat.formatters` stays in the release register as history.

### Changed

- The package depends on `@workspace/active-format`; `date-fns` and
  `@date-fns/tz` are no longer its own dependencies.
- `/react` (`useFormatters`, `usePresentation`, `<RecencyTime>`,
  `<ViewerDateTime>`, the presentation-profile and time-zone providers) is
  unchanged in behaviour and imports the formatters from
  `@workspace/active-format`.

### Migration

| Before | After |
| --- | --- |
| `import {…} from '@workspace/active-i18n/formatters'` | `import {…} from '@workspace/active-format'` |
| `import type {LocaleCode} from '@workspace/active-i18n'` | `import type {LocaleCode} from '@workspace/active-format'` |

## \[1.1.0\] — 2026-10-18

**The first recorded release of this package.** 1.1.0 is the source state
recorded on this date; the content before it is not reconstructed. What this
release does and does not guarantee:

- **An older copy that says 1.1.0 is not automatically byte-identical** to
  this release. The version string existed before any release was recorded,
  and earlier development transfers carried it with other content.
- **The content guarantee covers the recorded selection** `1.1.0-compat.formatters`
  and every later recorded release: the release register binds that name,
  selection and version to exactly one content hash.
- **The record does not prove that the full package is portable on its own.**
  Only the `formatters` selection is transferred; the root, server and React
  entries are not part of it.

### Added — compatibility selection `formatters` (2026-10-18, platform ADR-112, RPS-D10)

- **The reusable package transfer carries this package as the compatibility
  selection `formatters`**, not as the full package: the full package also
  has a root, a server and a React entry that depend on packages outside the
  transfer. The selection is a fixed definition in the reusable package
  register: the closure of the portable entry `./formatters`, the `LICENSE`,
  and a generated manifest. That manifest keeps this package's import name
  and `license`, names exactly the `./formatters` export with the third-party
  dependencies its closure imports, and says it is a selection (an
  `rpsSelection` field with its owner, owner version and end condition).
- **Its own identity.** The selection's version is
  `<this package's version>-compat.formatters`, with its own content hash and,
  once released, its own row in the release register. An equal import name or
  owner version does not make it the full package.
- **End condition** (from the register): the selection ends when this package
  can be transferred as its own canonical folder, which is the separate
  decoupling workstream.
- No source change: the formatter code and its public API are unchanged by
  this entry.
