ICU e MessageFormat 2 in i18next: argomenti tipizzati, linting e App Router
i18next scrive i messaggi nel proprio formato ({{name}}, chiavi con suffisso come photos_one), e un plugin i18nFormat può sostituirlo con un’altra sintassi: Fluent, polyglot, il formato Shopify e i due di cui parla questo articolo, ICU MessageFormat con i18next-icu e Unicode MessageFormat 2 (MF2) con i18next-mf2. Il formato i18next resta quello predefinito, e per i progetti che lo usano non cambia nulla. Ma chi scrive in ICU o in MF2 finora incontrava delle lacune: messaggi che fallivano senza dire una parola, opzioni di t() che TypeScript non poteva verificare, una CLI che creava chiavi _one e _other che nessuno legge, e un wrapper scritto a mano per portare ICU nell’App Router di Next.js.
Abbiamo colmato queste lacune questa settimana, in sei pacchetti. Questo articolo mostra cosa è cambiato, con codice che abbiamo eseguito sulle versioni pubblicate. Il riferimento per tutti i dettagli è la pagina della documentazione i18next Using ICU or MF2.
- i18next 26.5: argomenti ICU e MF2 verificati da TypeScript con
parseInterpolation: 'icu'o'mf2'; la 26.5.1 rende veloce il controllo dei tipi sui namespace grandi, la 26.5.2 fa segnalare a TypeScript 7 un’opzione sbagliata con la stessa chiarezza di TypeScript 6. - i18next-icu 2.6 e i18next-mf2 2.1: avvisi di debug quando la sintassi i18next finisce in messaggi ICU o MF2; la 2.6.1 e la 2.1.1 ricompilano i messaggi che cambiano a runtime.
- i18next-cli 1.76 e 1.77: una modalità ICU e MF2 per extract, lint, status, instrument e types.
- react-i18next 17.0.21 e 17.1:
IcuTransWithoutContextper i Server Components,valuesdiIcuTranstipizzati. - next-i18next 16.4: i provider
next-i18next/icuenext-i18next/mf2per l’App Router.
Scegliere la sintassi
Lo stesso messaggio nel formato i18next, in ICU e 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.}}"
}Il primo è il formato i18next: una chiave per ogni forma plurale, risolta da i18next. Il secondo è ICU, il terzo MF2: una sola chiave, e il plurale vive dentro il messaggio. In tutti e tre i casi chiamate t('photos', { count: 3 }).
- Il formato i18next è quello predefinito. Tenetelo se partite da zero, usate gli strumenti di i18next ovunque e vi piacciono il nesting e la formattazione integrata.
- ICU è la sintassi di react-intl, next-intl, Lingui e FormatJS, e di molti strumenti di traduzione. Sceglietela se i vostri messaggi sono già in ICU, se passate da una di queste librerie a i18next o se i vostri traduttori se la aspettano. Argomenti,
plural,select,selectordinale formati di numeri e date restano come sono; i tag di rich text con nome come<b>diventano tag numerati come<0>per i componenti React. - MF2 è il successore Unicode di ICU MessageFormat. La sua sintassi è più esplicita (
.input,.match, varianti), e questo ripaga nei messaggi che selezionano su più di un valore. Sul suo stato, più sotto.
La configurazione è un plugin per istanza di i18next:
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.'Per MF2 installate i18next-mf2, registrate MF2 al suo posto e scrivete {$name}. Impostate i18nFormat: { bidiIsolation: 'none' }, a meno che non vogliate i segni invisibili di isolamento bidi di MF2 intorno a ogni placeholder.
Cosa cambia con il passaggio
i18next-icu e i18next-mf2 non estendono la gestione dei messaggi di i18next, la sostituiscono. Tutto ciò che sta intorno al messaggio continua a funzionare: rilevamento della lingua, fallback, namespace, backend, saveMissing, keyPrefix, defaultVariables. Ciò che appartiene alla sintassi dei messaggi propria di i18next smette di funzionare:
- Le chiavi con suffisso di plurale e di contesto (
photos_one,friend_male) non vengono cercate. Mettete unpluralo unselectnel messaggio. - L’interpolazione
{{name}}non viene analizzata. Scrivete{name}(ICU) o{$name}(MF2). - Il nesting con
$t(key)viene mostrato come testo. Traducete la chiave interna e passatela come valore. interpolation.escapeValuenon viene applicato. React fa l’escape da solo; altrove usate l’opzioneescapeVariables: truedi i18next-icu o fate l’escape dei valori voi stessi.- La formattazione di i18next e
returnObjectsnon si applicano nemmeno, esaveMissingPluralsinvia chiavi con suffisso che i plugin non leggono mai, quindi disattivatelo.
Nessuna di queste cose genera un errore. La pagina della documentazione ha la tabella completa con il sostituto di ciascuna.
Avvisi di debug quando le sintassi si mescolano
Quel "nessuna genera un errore" era la trappola più comune: un progetto migrato con un {{name}} o una chiave _one rimasti mostrava semplicemente il testo sbagliato. Da i18next-icu 2.6.0 e i18next-mf2 2.1.0 entrambi i plugin ve lo dicono quando i18next gira con debug: true:
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-mf2Ogni avviso compare una volta per chiave, più una volta per istanza quando escapeValue è attivo ma non viene applicato. Senza debug non gira nulla di tutto questo e non cambia nulla.
Argomenti ICU e MF2 tipizzati
Il supporto TypeScript di i18next verifica gli argomenti {{name}} leggendoli dal tipo del messaggio. Chi usava ICU e MF2 doveva disattivarlo con parseInterpolation: false e perdeva il controllo degli argomenti. Da i18next 26.5.0 l’opzione legge invece ICU o 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
}
}Con 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 rifiuta le ultime tre chiamate. Le righe rilevanti di ogni errore:
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; }; }] | ...Un argomento plural accetta un numero, {album} una stringa o un numero, un select le sue chiavi di caso o qualsiasi stringa, {d, date} una Date. Contano anche gli argomenti annidati nei casi di plural e select; # e i tag no. MF2 funziona allo stesso modo, con tipi presi da :number, :date e dalle altre funzioni. L’output sopra viene da TypeScript 6; da i18next 26.5.2 TypeScript 7 segnala gli stessi errori.
Lo stesso controllo copre react-i18next. Da react-i18next 17.1.0 i values di IcuTrans sono tipizzati a partire dal messaggio del suo i18nKey:
<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; }'.I controlli hanno bisogno dei messaggi come tipi letterali. Un import typeof di un file JSON allarga ogni messaggio a string, e allora non si verifica nulla. Il comando types di i18next-cli scrive un file di risorse con messaggi letterali, e dalla 1.77.0 imposta anche parseInterpolation: 'icu' o 'mf2' in un i18next.d.ts generato da zero quando il vostro i18next è alla 26.5.0 o successiva.
Un’altra modifica TypeScript di i18next 26.5.1 aiuta tutti, con ICU o senza: il controllo dei tipi di t() su un namespace grande non cresce più in modo quadratico con il numero di chiavi. Con 5.000 chiavi in un namespace e una chiamata per chiave, il controllo è sceso da 14,8 a 1,1 secondi con TypeScript 6.
i18next-cli: modalità ICU e MF2
i18next-cli finora trattava ogni progetto come formato i18next. Dalla 1.76.0 basta un’opzione per cambiarlo:
// 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'
}
})Con questa, extract scrive una chiave per messaggio. t('photos', 'You have {count, plural, one {# photo} other {# photos}} in {album}.', { count, album }) produce photos, non photos_one e photos_other (più _few e _many per il russo). IcuTrans e i figli di Trans vengono estratti con placeholder ICU o MF2, status conta un messaggio al plurale come una sola chiave, e instrument scrive messaggi ICU o MF2.
lint legge ogni valore in ogni locale. Qui la traduzione russa ha rinominato un argomento e ha tenuto solo le categorie di plurale inglesi:
{
"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"Segnala anche errori di sintassi e categorie che una locale non usa mai (zero in inglese). I selettori esatti contano quando coprono una categoria: =1 copre one in inglese, ma non in russo, dove one vale anche per 21, 31 e così via.
React: IcuTrans, anche nei Server Components
Trans costruisce il messaggio dai figli JSX, e JSX riserva le parentesi graffe, quindi i plurali ICU non si possono scrivere lì. IcuTrans prende il messaggio come stringa e sostituisce i tag numerati con i componenti che dichiarate in content:
'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 un Server Component, importare da react-i18next poteva far fallire il build con createContext is not a function, perché il punto di ingresso principale crea un contesto React. Da react-i18next 17.0.21 importate IcuTransWithoutContext dal suo punto di ingresso dedicato e passate t e i18n:
import { IcuTransWithoutContext } from 'react-i18next/IcuTransWithoutContext'La sezione successiva lo mostra in una pagina Next.js. Plurali, select e formati vivono tutti nel messaggio. La macro Babel icu.macro è deprecata in favore di IcuTrans.
App Router di Next.js: next-i18next/icu e next-i18next/mf2
next-i18next 16 supporta l’App Router (e sempre il Pages Router, vedi l’articolo sulla v16). ICU lì richiedeva finora un passaggio in più: un Server Component non può passare una classe di plugin a un Client Component, quindi <I18nProvider use={[ICU]}> in un layout falliva con "Functions cannot be passed directly to Client Components", e dovevate scrivere il vostro wrapper 'use client'. Da next-i18next 16.4.0 importate invece il provider da next-i18next/icu (o 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 nei Server Components e useT nei Client Components formattano poi i messaggi ICU, e il rich text in un Server Component usa 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>
)
}Entrambi i plugin sono peer dependency opzionali di next-i18next, quindi le app che non importano mai questi punti di ingresso non li includono nel bundle. La versione completa, con un Client Component, argomenti tipizzati e una traduzione tedesca, è l’esempio app-router-icu; i dettagli sono nella sezione del README ICU and MF2 with the App Router.
MessageFormat 2: lo stato di i18next-mf2
i18next-mf2 2.0 è passato da una prerelease bloccata a messageformat 4.1, l’implementazione stabile di MF2 (LDML 48.2). :number, :integer, :currency e :percent funzionano subito, :date, :datetime, :time e :unit sono funzioni draft dietro mf2DraftFunctions: true, e la nuova opzione bidiIsolation disattiva i segni di isolamento. Richiede Node.js 20.19, 22.12 o 24 e successivi. Tutto quanto visto sopra (avvisi di debug, argomenti tipizzati, modalità CLI, provider next-i18next) funziona anche per MF2.
MF2 è bravo dove ICU è scomodo: un messaggio che seleziona su due valori, come genere e quantità, è una lista piatta di varianti invece di un select annidato in un plural. Ha anche una vera sintassi di escape al posto delle regole sugli apostrofi di ICU.
Onestamente, sulla maturità: la specifica è stabile, l’implementazione JavaScript è solida, e i18next-mf2 ha test e CI su ogni versione di Node.js supportata. Ma l’ecosistema è giovane. Finora ci sono pochi resoconti pubblici di MF2 in produzione, la proposta TC39 per Intl.MessageFormat è ancora allo Stage 1, e la maggior parte degli editor di traduzione e dei linter conosce ICU, non MF2. Se iniziate un nuovo progetto e la sintassi vi piace, MF2 è una scelta ragionevole. Se oggi avete messaggi ICU, non c’è bisogno di migrare. Il nostro articolo sullo stato di MF2 in i18next ne dà la versione lunga.
Correzioni minori da conoscere
- I messaggi modificati vengono recepiti. Entrambi i plugin tengono in cache i messaggi compilati. Fino a i18next-icu 2.6.1 e i18next-mf2 2.1.1, un messaggio cambiato dopo il primo utilizzo (
addResource,reloadResources, unreloadIntervaldel backend, l’hot reload, ilreloadOnPrerenderdi next-i18next) continuava a restituire il vecchio testo, a meno che non aveste impostatobindI18nStore. Ora la cache confronta il messaggio. replacefunziona.t('key', { replace: { name } })riempie gli argomenti ICU e MF2 come fa i18next per i propri (stesse versioni).- React Native con Hermes. Hermes non ha né
Intl.PluralRulesnéIntl.Locale, quindi lì i casi di plurale ICU comeoneeotherfalliscono (i casi esatti come=0funzionano ancora), e senzaIntl.Localemessageformat non formatta nessun messaggio. I README di i18next-icu e i18next-mf2 ora elencano quali polyfill caricare, e in quale ordine. - I
valuesdiIcuTranshanno la precedenza sudefaultVariables, come int()(react-i18next 17.0.21).
Se le vostre traduzioni sono su Locize
I messaggi MessageFormat valgono quanto le loro traduzioni, e la maggior parte dei loro bug nasce durante la traduzione: un argomento rinominato, una categoria di plurale mancante, una parentesi in meno. In un progetto Locize con il formato MessageFormat (messageformat, spesso chiamato ICU MessageFormat):
- L’editor evidenzia la sintassi MessageFormat e mostra l’errore di parsing sotto un messaggio che non si riesce ad analizzare.
- I controlli segnalano una traduzione i cui nomi di argomento differiscono dalla sorgente, anche dentro i casi di plural e select (issue 241, e issue 242 per le variabili Fluent). I controlli linguistici leggono MessageFormat come sintassi e di un plurale confrontano solo il caso
other, così una traduzione russa corretta con più casi della sua sorgente inglese non viene segnalata. - La revisione IA giudica un plurale MessageFormat in base alle categorie di plurale della lingua di destinazione: una proposta per il russo deve avere
one,few,manyeother, e una che copia soltanto ioneeotheringlesi viene scartata.
I messaggi MF2 vengono salvati e distribuiti come testo, come ogni altro formato. Appena MF2 prenderà piede, Locize ne amplierà il supporto; cosa prevediamo e quali segnali aspettiamo è nell’articolo su MF2. Se volete vedere questi controlli sui vostri messaggi, create un progetto gratuito e importate i vostri file.
Domande frequenti
i18next supporta ICU MessageFormat?
Sì, con il plugin ufficiale i18next-icu, che formatta i messaggi con intl-messageformat. Il formato i18next resta quello predefinito, quindi non cambia nulla finché non aggiungete il plugin. I messaggi ICU di react-intl, next-intl o Lingui mantengono argomenti, plurali, select e formati, e con debug: true i18next-icu avvisa della sintassi i18next che ICU non legge.
Esiste un plugin i18next per MessageFormat 2? Sì: i18next-mf2, mantenuto nell’organizzazione i18next. La versione 2 gira su messageformat 4.1, l’implementazione stabile di MF2. Funziona, ma ancora pochi strumenti ed editor di traduzione capiscono MF2.
TypeScript può verificare gli argomenti dei messaggi ICU in i18next?
Sì, da i18next 26.5.0: impostate parseInterpolation: 'icu' (o 'mf2') e usate il file di risorse di i18next-cli types. Argomenti mancanti, scritti male o del tipo sbagliato sono errori di tipo, e con react-i18next 17.1.0 lo sono anche i values sbagliati di IcuTrans.
next-i18next funziona con l’App Router e ICU?
Sì. next-i18next 16 supporta l’App Router e il Pages Router, e dalla 16.4.0 next-i18next/icu e next-i18next/mf2 forniscono un I18nProvider con il plugin registrato, quindi non serve un wrapper 'use client'.
i18next-cli può estrarre e controllare messaggi ICU?
Sì, dalla 1.76.0 con extract.messageFormat: 'icu' o 'mf2': una chiave per messaggio, lint per sintassi, argomenti e categorie di plurale per locale, e messaggi al plurale contati come una sola chiave in status.
Conviene usare il formato i18next, ICU o MessageFormat 2? Il formato i18next se partite da zero con gli strumenti di i18next, ICU se i vostri messaggi o traduttori lo parlano già, MF2 se volete il nuovo standard e accettate un ecosistema giovane.
Stanco di gestire le traduzioni a mano?
Locize è il backend di gestione delle traduzioni creato dal team di i18next: distribuzione via CDN, traduzione con AI, editing in-context, senza nuovi deploy.
Inizia la prova gratuita di 14 giorni