Zum Inhalt springen
18. Oktober 202610 min readGuides

ICU und MessageFormat 2 in i18next: typisierte Argumente, Linting und der App Router

i18next schreibt Nachrichten in seinem eigenen Format ({{name}}, Suffix-Keys wie photos_one), und ein i18nFormat-Plugin kann eine andere Syntax einsetzen: Fluent, polyglot, das Shopify-Format und die beiden, um die es in diesem Beitrag geht, ICU MessageFormat mit i18next-icu und Unicode MessageFormat 2 (MF2) mit i18next-mf2. Das i18next-Format bleibt der Standard, und für Projekte, die es nutzen, ändert sich nichts. Wer aber ICU oder MF2 schreibt, stieß bisher auf Lücken: Nachrichten, die ohne ein Wort fehlschlugen, t()-Optionen, die TypeScript nicht prüfen konnte, ein CLI, das _one- und _other-Keys erzeugte, die niemand liest, und ein selbst geschriebener Wrapper, um ICU in den Next.js App Router zu bringen.

Diese Lücken haben wir diese Woche geschlossen, in sechs Paketen. Dieser Beitrag zeigt, was sich geändert hat, mit Code, den wir gegen die veröffentlichten Versionen ausgeführt haben. Die Referenz für alle Details ist die i18next-Dokuseite Using ICU or MF2.

Die Releases (17. Oktober 2026)
  • i18next 26.5: typgeprüfte ICU- und MF2-Argumente mit parseInterpolation: 'icu' oder 'mf2'; 26.5.1 macht die Typprüfung großer Namespaces schnell, 26.5.2 lässt TypeScript 7 eine falsche Option so klar melden wie TypeScript 6.
  • i18next-icu 2.6 und i18next-mf2 2.1: Debug-Warnungen, wenn i18next-Syntax in ICU- oder MF2-Nachrichten landet; 2.6.1 und 2.1.1 kompilieren Nachrichten neu, die sich zur Laufzeit ändern.
  • i18next-cli 1.76 und 1.77: ein ICU- und MF2-Modus für extract, lint, status, instrument und types.
  • react-i18next 17.0.21 und 17.1: IcuTransWithoutContext für Server Components, typisierte values von IcuTrans.
  • next-i18next 16.4: die Provider next-i18next/icu und next-i18next/mf2 für den App Router.

Die Syntax wählen

Dieselbe Nachricht im i18next-Format, in ICU und in MF2:

{
  "photos_one": "You have one photo.",
  "photos_other": "You have {{count}} photos."
}
{
  "photos": "You have {count, plural, one {one photo} other {# photos}}."
}
{
  "photos": ".input {$count :number} .match $count one {{You have one photo.}} * {{You have {$count} photos.}}"
}

Die erste ist das i18next-Format: ein Key pro Pluralform, aufgelöst von i18next. Die zweite ist ICU, die dritte MF2: ein Key, und der Plural steckt in der Nachricht. In allen drei Fällen rufen Sie t('photos', { count: 3 }) auf.

  • Das i18next-Format ist der Standard. Bleiben Sie dabei, wenn Sie neu starten, durchgehend i18next-Tooling nutzen und Nesting und die eingebaute Formatierung mögen.
  • ICU ist die Syntax von react-intl, next-intl, Lingui und FormatJS und vieler Übersetzungstools. Wählen Sie sie, wenn Ihre Nachrichten bereits ICU sind, wenn Sie von einer dieser Bibliotheken zu i18next wechseln oder wenn Ihre Übersetzer sie erwarten. Argumente, plural, select, selectordinal sowie Zahlen- und Datumsformate werden unverändert übernommen; benannte Rich-Text-Tags wie <b> werden für die React-Komponenten zu nummerierten Tags wie <0>.
  • MF2 ist der Unicode-Nachfolger von ICU MessageFormat. Seine Syntax ist expliziter (.input, .match, Varianten), was sich bei Nachrichten auszahlt, die nach mehr als einem Wert auswählen. Mehr zu seinem Stand weiter unten.

Die Einrichtung ist ein Plugin pro i18next-Instanz:

import i18next from 'i18next'
import ICU from 'i18next-icu' // npm install i18next i18next-icu intl-messageformat

await i18next.use(ICU).init({
  lng: 'en',
  resources: {
    en: {
      translation: {
        greeting: 'Hello {name}!',
        photos: 'You have {count, plural, =0 {no photos} one {one photo} other {# photos}}.',
        order: '{status, select, shipped {Your order is on its way.} other {Your order is being prepared.}}'
      }
    }
  }
})

i18next.t('greeting', { name: 'Ana' }) // -> 'Hello Ana!'
i18next.t('photos', { count: 1250 }) // -> 'You have 1,250 photos.'
i18next.t('order', { status: 'shipped' }) // -> 'Your order is on its way.'

Für MF2 installieren Sie i18next-mf2, registrieren stattdessen MF2 und schreiben {$name}. Setzen Sie i18nFormat: { bidiIsolation: 'none' }, außer Sie wollen die unsichtbaren Bidi-Isolationszeichen von MF2 um jeden Platzhalter.

Was sich beim Wechsel ändert

i18next-icu und i18next-mf2 erweitern die Nachrichtenverarbeitung von i18next nicht, sie ersetzen sie. Alles rund um die Nachricht funktioniert weiter: Spracherkennung, Fallbacks, Namespaces, Backends, saveMissing, keyPrefix, defaultVariables. Was zur eigenen Nachrichtensyntax von i18next gehört, funktioniert nicht mehr:

  • Plural- und Kontext-Suffix-Keys (photos_one, friend_male) werden nicht nachgeschlagen. Schreiben Sie ein plural oder select in die Nachricht.
  • {{name}}-Interpolation wird nicht geparst. Schreiben Sie {name} (ICU) oder {$name} (MF2).
  • Nesting mit $t(key) wird als Text ausgegeben. Übersetzen Sie den inneren Key und übergeben Sie ihn als Wert.
  • interpolation.escapeValue wird nicht angewendet. React escapt selbst; anderswo nutzen Sie die Option escapeVariables: true von i18next-icu oder escapen die Werte selbst.
  • i18next-Formatierung und returnObjects greifen ebenfalls nicht, und saveMissingPlurals sendet Suffix-Keys, die die Plugins nie lesen, schalten Sie es also aus.

Nichts davon wirft einen Fehler. Die Dokuseite hat die vollständige Tabelle mit dem Ersatz für jedes Feature.

Debug-Warnungen bei gemischter Syntax

Dieses „nichts davon wirft einen Fehler“ war die häufigste Falle: Ein migriertes Projekt mit einem übrig gebliebenen {{name}} oder _one-Key zeigte einfach den falschen Text. Seit i18next-icu 2.6.0 und i18next-mf2 2.1.0 sagen es Ihnen beide Plugins, wenn i18next mit debug: true läuft:

await i18next.use(ICU).init({
  debug: true,
  lng: 'en',
  resources: {
    en: {
      translation: {
        greeting: 'Hello {{name}}!',
        photos: 'You have photos.',
        photos_one: 'You have one photo.',
        photos_other: 'You have {count} photos.',
        footer: 'Read our $t(terms)',
        terms: 'terms'
      }
    }
  }
})

i18next.t('greeting', { name: 'Ana' })
i18next.t('photos', { count: 3 })
i18next.t('footer')
i18next: i18next-icu: key "greeting" (en/translation) could not be parsed (MALFORMED_ARGUMENT). It contains "{{": i18next interpolation like {{name}} does not work with ICU, use {name}. See https://www.i18next.com/how-to/using-icu-or-mf2
i18next: i18next-icu: key "photos" (en/translation) was called with count, but i18next-icu does not look up plural suffix keys (photos_one, photos_other). Put the plural into the message: {count, plural, one {# item} other {# items}}. See https://www.i18next.com/how-to/using-icu-or-mf2
i18next: i18next-icu: key "footer" (en/translation) contains "$t(", but i18next-icu does not support nesting, the text is output as is. See https://www.i18next.com/how-to/using-icu-or-mf2

Jede Warnung kommt einmal pro Key, dazu einmal pro Instanz, wenn escapeValue an ist, aber nicht angewendet wird. Ohne debug läuft nichts davon, und nichts ändert sich.

Typisierte ICU- und MF2-Argumente

Die TypeScript-Unterstützung von i18next prüft {{name}}-Argumente, indem sie sie aus dem Nachrichtentyp liest. ICU- und MF2-Nutzer mussten das mit parseInterpolation: false abschalten und verloren die Argumentprüfung. Seit i18next 26.5.0 liest die Option stattdessen ICU oder MF2:

// i18next.d.ts
import 'i18next'
import type Resources from './resources' // generated by i18next-cli

declare module 'i18next' {
  interface CustomTypeOptions {
    parseInterpolation: 'icu' // or 'mf2'
    resources: Resources
  }
}

Mit photos: 'You have {count, plural, one {# photo} other {# photos}} in {album}.':

i18next.t('photos', { count: 3, album: 'Summer' }) // ok
i18next.t('photos', { count: 3, albm: 'Summer' })
i18next.t('photos', { count: 'three', album: 'Summer' })
i18next.t('photos')

tsc lehnt die letzten drei Aufrufe ab. Die relevanten Zeilen jedes Fehlers:

app.ts(4,11): error TS2345: Argument of type '["photos", { count: 3; albm: "Summer"; }]' is not assignable to parameter of type ...
          Property 'album' is missing in type '{ count: 3; albm: "Summer"; }' but required in type '{ count: number; album: string | number; }'.

app.ts(5,11): error TS2345: Argument of type '["photos", { count: string; album: string; }]' is not assignable to parameter of type ...
            Types of property 'count' are incompatible.
              Type 'string' is not assignable to type 'number'.

app.ts(6,11): error TS2345: Argument of type '["photos"]' is not assignable to parameter of type '[key: ("photos" | "photos"[]) & { 'pass these arguments as t() options': { count: number; album: string | number; }; }] | ...

Ein plural-Argument nimmt eine Zahl, {album} einen String oder eine Zahl, ein select seine Case-Keys oder einen beliebigen String, {d, date} ein Date. Argumente in Plural- und Select-Cases zählen mit, # und Tags nicht. MF2 funktioniert genauso, mit Typen aus :number, :date und den anderen Funktionen. Die Ausgabe oben stammt von TypeScript 6; seit i18next 26.5.2 meldet TypeScript 7 dieselben Fehler.

Dieselbe Prüfung gilt für react-i18next. Seit react-i18next 17.1.0 sind die values von IcuTrans aus der Nachricht seines i18nKey typisiert:

<IcuTrans
  i18nKey="welcome" // 'Welcome back, <0>{name}</0>!'
  defaultTranslation="Welcome back, <0>{name}</0>!"
  content={[{ type: 'strong' }]}
  values={{ nmae: 'Ada' }}
/>
// error TS2353: Object literal may only specify known properties, and 'nmae' does not exist in type '{ name: string | number; }'.

Die Prüfungen brauchen die Nachrichten als literale Typen. Ein typeof-Import einer JSON-Datei erweitert jede Nachricht zu string, und dann wird nichts geprüft. Der types-Befehl von i18next-cli schreibt eine Ressourcendatei mit literalen Nachrichten, und seit 1.77.0 setzt er in einer neu erzeugten i18next.d.ts auch parseInterpolation: 'icu' oder 'mf2', wenn Ihr i18next 26.5.0 oder neuer ist.

Eine weitere TypeScript-Änderung in i18next 26.5.1 hilft allen, ob ICU oder nicht: Die Typprüfung von t() gegen einen großen Namespace wächst nicht mehr quadratisch mit der Anzahl der Keys. Mit 5.000 Keys in einem Namespace und einem Aufruf pro Key sank die Prüfzeit unter TypeScript 6 von 14,8 auf 1,1 Sekunden.

i18next-cli: ICU- und MF2-Modus

i18next-cli behandelte bisher jedes Projekt als i18next-Format. Seit 1.76.0 schaltet eine Option um:

// i18next.config.ts
import { defineConfig } from 'i18next-cli'

export default defineConfig({
  locales: ['en', 'de', 'ru'],
  extract: {
    input: ['src/**/*.{ts,tsx}'],
    output: 'locales/{{language}}/{{namespace}}.json',
    messageFormat: 'icu' // or 'mf2'
  }
})

Damit schreibt extract einen Key pro Nachricht. t('photos', 'You have {count, plural, one {# photo} other {# photos}} in {album}.', { count, album }) ergibt photos, nicht photos_one und photos_other (plus _few und _many für Russisch). IcuTrans und die Kinder von Trans werden mit ICU- oder MF2-Platzhaltern extrahiert, status zählt eine Plural-Nachricht als einen Key, und instrument schreibt ICU- oder MF2-Nachrichten.

lint liest jeden Wert in jeder Locale. Hier hat die russische Übersetzung ein Argument umbenannt und nur die englischen Plural-Kategorien behalten:

{
  "photos": "У вас {count, plural, one {# фото} other {# фото}} в {albom}."
}
✖ Linter found 2 potential issues.

locales/ru/translation.json
  2: Error: ICU message: "photos: arguments differ from "en": missing {album}, unexpected {albom}"
  2: Error: ICU message: "photos: plural {count} is missing the "ru" categories few, many"

Es meldet auch Syntaxfehler und Kategorien, die eine Locale nie verwendet (zero im Englischen). Exakte Selektoren zählen, wenn sie eine Kategorie abdecken: =1 deckt im Englischen one ab, aber nicht im Russischen, wo one auch 21, 31 und so weiter bedeutet.

React: IcuTrans, auch in Server Components

Trans baut seine Nachricht aus JSX-Kindern, und JSX reserviert geschweifte Klammern, also lassen sich ICU-Plurals dort nicht schreiben. IcuTrans nimmt die Nachricht als String und ersetzt nummerierte Tags durch die Komponenten, die Sie in content deklarieren:

'use client'
import { IcuTrans } from 'react-i18next'

export function Rendered() {
  return (
    <IcuTrans
      i18nKey="rendered"
      defaultTranslation="Rendered by a <0>{where}</0> Component."
      content={[{ type: 'em' }]}
      values={{ where: 'Client' }}
    />
  )
}

In einer Server Component konnte ein Import aus react-i18next den Build mit createContext is not a function scheitern lassen, weil der Haupteinstieg einen React-Kontext erzeugt. Seit react-i18next 17.0.21 importieren Sie IcuTransWithoutContext aus einem eigenen Einstiegspunkt und übergeben t und i18n:

import { IcuTransWithoutContext } from 'react-i18next/IcuTransWithoutContext'

Der nächste Abschnitt zeigt es in einer Next.js-Seite. Plurals, Selects und Formate stecken alle in der Nachricht. Das Babel-Makro icu.macro ist zugunsten von IcuTrans veraltet.

Next.js App Router: next-i18next/icu und next-i18next/mf2

next-i18next 16 unterstützt den App Router (und weiterhin den Pages Router, siehe den v16-Beitrag). ICU brauchte dort bisher einen Extraschritt: Eine Server Component kann einer Client Component keine Plugin-Klasse übergeben, also scheiterte <I18nProvider use={[ICU]}> in einem Layout mit "Functions cannot be passed directly to Client Components", und Sie schrieben Ihren eigenen 'use client'-Wrapper. Seit next-i18next 16.4.0 importieren Sie den Provider stattdessen aus next-i18next/icu (oder next-i18next/mf2):

// i18n.config.ts
import type { I18nConfig } from 'next-i18next/proxy'
import ICU from 'i18next-icu'

const i18nConfig: I18nConfig = {
  supportedLngs: ['en', 'de'],
  fallbackLng: 'en',
  defaultNS: 'common',
  ns: ['common'],
  resourceLoader: (language, namespace) => import(`./app/i18n/locales/${language}/${namespace}.json`),
  use: [ICU],
}

export default i18nConfig
// app/[lng]/layout.tsx
import { initServerI18next, getT, getResources, generateI18nStaticParams } from 'next-i18next/server'
import { I18nProvider } from 'next-i18next/icu' // or 'next-i18next/mf2'
import i18nConfig from '../../i18n.config'

initServerI18next(i18nConfig)

export async function generateStaticParams() {
  return generateI18nStaticParams()
}

export default async function RootLayout({ children, params }: {
  children: React.ReactNode
  params: Promise<{ lng: string }>
}) {
  const { lng } = await params
  const { i18n } = await getT()

  return (
    <html lang={lng}>
      <body>
        <I18nProvider language={lng} resources={getResources(i18n)}>
          {children}
        </I18nProvider>
      </body>
    </html>
  )
}

getT in Server Components und useT in Client Components formatieren dann ICU-Nachrichten, und Rich Text in einer Server Component nutzt IcuTransWithoutContext:

// app/[lng]/page.tsx
import { getT } from 'next-i18next/server'
import { IcuTransWithoutContext } from 'react-i18next/IcuTransWithoutContext'

export default async function Page() {
  const { t, i18n } = await getT()

  return (
    <main>
      <p>{t('photos', { count: 5 })}</p>
      <IcuTransWithoutContext
        i18nKey="welcome"
        defaultTranslation="Welcome back, <0>{name}</0>!"
        content={[{ type: 'strong' }]}
        values={{ name: 'Ada' }}
        t={t}
        i18n={i18n}
      />
    </main>
  )
}

Beide Plugins sind optionale Peer-Dependencies von next-i18next, Apps, die diese Einstiegspunkte nie importieren, bündeln sie also nicht. Die vollständige Version mit einer Client Component, typisierten Argumenten und einer deutschen Übersetzung ist das app-router-icu-Beispiel; Details stehen im README-Abschnitt ICU and MF2 with the App Router.

MessageFormat 2: wo i18next-mf2 steht

i18next-mf2 2.0 wechselte von einer festgepinnten Vorabversion zu messageformat 4.1, der stabilen Implementierung von MF2 (LDML 48.2). :number, :integer, :currency und :percent funktionieren direkt, :date, :datetime, :time und :unit sind Draft-Funktionen hinter mf2DraftFunctions: true, und die neue Option bidiIsolation schaltet die Isolationszeichen ab. Es braucht Node.js 20.19, 22.12 oder 24 und neuer. Alles oben Gezeigte (Debug-Warnungen, typisierte Argumente, der CLI-Modus, der next-i18next-Provider) funktioniert auch für MF2.

MF2 ist gut in dem, was ICU umständlich macht: Eine Nachricht, die nach zwei Werten auswählt, etwa Geschlecht und Anzahl, ist eine flache Liste von Varianten statt eines Selects, das in einem Plural steckt. Außerdem hat es eine richtige Escape-Syntax statt der Apostroph-Regeln von ICU.

Ehrlich zur Reife: Die Spezifikation ist stabil, die JavaScript-Implementierung solide, und i18next-mf2 hat Tests und CI auf jeder unterstützten Node.js-Version. Aber das Ökosystem ist jung. Es gibt bisher wenige öffentliche Berichte über MF2 in Produktion, der TC39-Vorschlag für Intl.MessageFormat steht noch bei Stage 1, und die meisten Übersetzungseditoren und Linter kennen ICU, nicht MF2. Wenn Sie ein neues Projekt starten und die Syntax mögen, ist MF2 eine vernünftige Wahl. Wenn Sie heute ICU-Nachrichten haben, gibt es keinen Grund zu migrieren. Unser Beitrag zum Stand von MF2 in i18next hat die ausführliche Version.

Kleinere Fixes, die man kennen sollte

  • Geänderte Nachrichten werden übernommen. Beide Plugins cachen kompilierte Nachrichten. Bis i18next-icu 2.6.1 und i18next-mf2 2.1.1 lieferte eine Nachricht, die sich nach ihrer ersten Verwendung änderte (addResource, reloadResources, ein reloadInterval des Backends, Hot Reload, reloadOnPrerender von next-i18next), weiter den alten Text, außer Sie setzten bindI18nStore. Der Cache vergleicht jetzt die Nachricht.
  • replace funktioniert. t('key', { replace: { name } }) füllt ICU- und MF2-Argumente so, wie i18next es für seine eigenen tut (gleiche Versionen).
  • React Native mit Hermes. Hermes hat kein Intl.PluralRules und kein Intl.Locale, daher scheitern ICU-Plural-Cases wie one und other dort (exakte Cases wie =0 funktionieren weiter), und ohne Intl.Locale formatiert messageformat gar keine Nachricht. Die READMEs von i18next-icu und i18next-mf2 listen jetzt, welche Polyfills in welcher Reihenfolge zu laden sind.
  • Die values von IcuTrans haben Vorrang vor defaultVariables, wie in t() (react-i18next 17.0.21).

Wenn Ihre Übersetzungen in Locize liegen

MessageFormat-Nachrichten sind nur so gut wie ihre Übersetzungen, und die meisten ihrer Fehler entstehen beim Übersetzen: ein umbenanntes Argument, eine fehlende Plural-Kategorie, eine Klammer zu wenig. In einem Locize-Projekt mit dem Format MessageFormat (messageformat, oft ICU MessageFormat genannt):

  • Der Editor hebt die MessageFormat-Syntax hervor und zeigt den Parse-Fehler unter einer Nachricht an, die sich nicht parsen lässt.
  • Die Checks markieren eine Übersetzung, deren Argumentnamen von der Quelle abweichen, auch innerhalb von Plural- und Select-Cases (Issue 241, und Issue 242 für Fluent-Variablen). Die linguistischen Checks lesen MessageFormat als Syntax und vergleichen bei einem Plural nur den other-Case, sodass eine korrekte russische Übersetzung mit mehr Cases als ihre englische Quelle nicht markiert wird.
  • Das KI-Review beurteilt einen MessageFormat-Plural nach den Plural-Kategorien der Zielsprache: Ein Vorschlag für Russisch braucht one, few, many und other, und einer, der nur das englische one und other kopiert, wird verworfen.

MF2-Nachrichten werden wie jedes andere Format als Text gespeichert und ausgeliefert. Sobald MF2 mehr Verbreitung findet, baut Locize die Unterstützung dafür aus; was wir planen und auf welche Signale wir warten, steht im MF2-Beitrag. Wenn Sie diese Checks an Ihren eigenen Nachrichten sehen wollen, erstellen Sie ein kostenloses Projekt und importieren Sie Ihre Dateien.

Häufig gestellte Fragen

Unterstützt i18next ICU MessageFormat? Ja, mit dem offiziellen Plugin i18next-icu, das Nachrichten mit intl-messageformat formatiert. Das i18next-Format bleibt der Standard; solange Sie das Plugin nicht hinzufügen, ändert sich nichts. ICU-Nachrichten aus react-intl, next-intl oder Lingui behalten ihre Argumente, Plurals, Selects und Formate, und mit debug: true warnt i18next-icu vor i18next-Syntax, die ICU nicht liest.

Gibt es ein i18next-Plugin für MessageFormat 2? Ja: i18next-mf2, gepflegt in der i18next-Organisation. Version 2 läuft auf messageformat 4.1, der stabilen MF2-Implementierung. Es funktioniert, aber noch verstehen weniger Tools und Übersetzungseditoren MF2.

Kann TypeScript die Argumente von ICU-Nachrichten in i18next prüfen? Ja, seit i18next 26.5.0: Setzen Sie parseInterpolation: 'icu' (oder 'mf2') und verwenden Sie die Ressourcendatei von i18next-cli types. Fehlende, falsch geschriebene oder falsch typisierte Argumente sind Typfehler, und mit react-i18next 17.1.0 auch falsche values von IcuTrans.

Funktioniert next-i18next mit dem App Router und ICU? Ja. next-i18next 16 unterstützt den App Router und den Pages Router, und seit 16.4.0 liefern next-i18next/icu und next-i18next/mf2 einen I18nProvider mit registriertem Plugin, ein 'use client'-Wrapper ist also nicht nötig.

Kann i18next-cli ICU-Nachrichten extrahieren und prüfen? Ja, seit 1.76.0 mit extract.messageFormat: 'icu' oder 'mf2': ein Key pro Nachricht, lint für Syntax, Argumente und Plural-Kategorien pro Locale, und Plural-Nachrichten zählen in status als ein Key.

Soll ich das i18next-Format, ICU oder MessageFormat 2 verwenden? Das i18next-Format, wenn Sie neu mit i18next-Tooling starten, ICU, wenn Ihre Nachrichten oder Übersetzer es schon sprechen, MF2, wenn Sie den neuen Standard wollen und ein junges Ökosystem akzeptieren.

Übersetzungen noch von Hand verwalten?

Locize ist das Translation-Management-Backend vom i18next-Team: CDN-Auslieferung, KI-Übersetzung, In-Context-Editing, keine Redeploys.

14 Tage kostenlos testen