Aller au contenu
7 juillet 20266 min readGuides

Formatage i18n : dates, nombres et devises avec Intl et i18next (guide 2026)

La traduction couvre les mots. Le formatage sensible à la locale couvre tout ce qui est numérique, et c'est là que l'i18n bricolé à la main s'effondre en silence : 1,234.56 s'écrit 1.234,56 en allemand et 1 234 567,89 en français, le même jour s'écrit 7/7/2026, 7.7.2026 ou 2026/7/7 selon la locale, et les montants en yens n'ont pas de décimales du tout. Concaténer '$' + price.toFixed(2) est un bug dans la plus grande partie du monde.

La bonne nouvelle : chacune de ces règles est déjà livrée avec le navigateur, et i18next les expose directement dans vos chaînes de traduction. Ce guide couvre les API Intl qui font le travail et le câblage i18next par-dessus.

Sommaire

Pourquoi le formatage à la main échoue

Prenez un nombre, 1234567.89, et affichez-le comme l'attendent cinq locales réelles :

LocaleRendu
en-US (anglais, États-Unis)1,234,567.89
de-DE (allemand)1.234.567,89
fr-FR (français)1 234 567,89
de-CH (allemand, Suisse)1'234'567.89
en-IN (anglais, Inde)12,34,567.89

Le rendu allemand inverse le sens de . et , par rapport à l'anglais. Le français groupe avec des espaces (insécables fines). L'Inde groupe en lakhs et crores, si bien que même la taille des groupes diffère. Les dates ont le même problème (mois/jour/année vs jour.mois.année vs année/mois/jour), et les devises ajoutent la position du symbole et des règles de décimales propres à chaque devise.

Rien de tout cela ne doit être codé à la main par langue. Comme les règles de pluriel, tout est défini dans le CLDR d'Unicode, et c'est déjà dans votre runtime.

La boîte à outils Intl

L'API d'internationalisation ECMAScript (Intl) est livrée avec tous les navigateurs modernes et Node.js, données CLDR incluses, il n'y a donc aucun bundle de locale à installer :

APICe qu'elle formateNom du format i18next
Intl.NumberFormatnombres, pourcentages, unités, notation compacte, devisesnumber, currency
Intl.DateTimeFormatdates et heuresdatetime
Intl.RelativeTimeFormat« il y a 3 jours », « dans 2 heures »relativetime
Intl.ListFormat« a, b et c »list
Intl.PluralRulescatégories de plurielalimente les suffixes _one / _other

Les quatre premières font l'objet de cet article. La dernière est un sujet à part entière, traité dans le guide de pluralisation lié ci-dessus.

Nombres, pourcentages et notation compacte

Intl.NumberFormat gère les séparateurs, les pourcentages, les unités physiques et la notation compacte (celle « des réseaux sociaux »), par locale :

new Intl.NumberFormat('en-US').format(1234567.89)  // "1,234,567.89"
new Intl.NumberFormat('de-DE').format(1234567.89)  // "1.234.567,89"

new Intl.NumberFormat('en', { style: 'percent' }).format(0.256)  // "26%"
new Intl.NumberFormat('de', { style: 'percent' }).format(0.256)  // "26 %"

new Intl.NumberFormat('en', { notation: 'compact' }).format(1234567)  // "1.2M"
new Intl.NumberFormat('de', { notation: 'compact' }).format(1234567)  // "1,2 Mio."

new Intl.NumberFormat('en', { style: 'unit', unit: 'kilometer-per-hour' }).format(88)  // "88 km/h"

Remarquez les détails que personne ne coderait à la main : le pourcentage allemand reçoit une espace (insécable) avant %, et le suffixe compact est un mot propre à la locale, pas toujours une lettre.

Devises

Le formatage des devises, c'est le formatage des nombres plus trois décisions dépendantes de la locale : quel symbole, où il se place, et combien de décimales la devise utilise.

const price = 1234.5

new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price)
// "$1,234.50"
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price)
// "1.234,50 €"
new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(price)
// "¥1,235"

La ligne du yen est celle qui piège : le JPY n'a pas de sous-unités, donc Intl arrondit au yen entier. price.toFixed(2) aurait affiché ¥1234.50, qui n'existe pas.

Une règle à intérioriser : la devise vient de la transaction, le formatage vient de la locale de l'utilisateur. Un utilisateur allemand qui achète en dollars voit des dollars, formatés à l'allemande :

new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'USD' }).format(price)
// "1.234,50 $"

Ne déduisez jamais le code devise de la langue de l'interface.

Dates, heures et temps relatifs

Intl.DateTimeFormat couvre l'ordre, les séparateurs et les noms de mois et de jours. Les préréglages dateStyle / timeStyle choisissent une mise en forme raisonnable par locale :

const d = new Date('2026-07-07T15:30:00Z')

new Intl.DateTimeFormat('en-US').format(d)  // "7/7/2026"
new Intl.DateTimeFormat('de-DE').format(d)  // "7.7.2026"
new Intl.DateTimeFormat('ja-JP').format(d)  // "2026/7/7"

new Intl.DateTimeFormat('en-US', { dateStyle: 'full' }).format(d)
// "Tuesday, July 7, 2026"
new Intl.DateTimeFormat('de-DE', { dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Zurich' }).format(d)
// "7. Juli 2026 um 17:30"

Le formatage ne convertit pas les fuseaux horaires à votre place ; sans option timeZone explicite, le rendu se fait dans le fuseau du runtime.

Pour les chaînes « il y a 3 jours » / « dans 2 heures », Intl.RelativeTimeFormat prend un décalage signé plus une unité :

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
rtf.format(-1, 'day')   // "yesterday"
rtf.format(3, 'hour')   // "in 3 hours"

new Intl.RelativeTimeFormat('de', { numeric: 'auto' }).format(-1, 'day')  // "gestern"

Et Intl.ListFormat assemble des listes de mots avec les bons séparateurs et la bonne conjonction par langue :

new Intl.ListFormat('en').format(['HTML', 'CSS', 'JavaScript'])  // "HTML, CSS, and JavaScript"
new Intl.ListFormat('de').format(['HTML', 'CSS', 'JavaScript'])  // "HTML, CSS und JavaScript"

Le formatage dans les traductions i18next

Appeler Intl soi-même fonctionne, mais cela découpe une phrase en fragments formatés par le code et en fragments traduits. i18next résout ce problème : depuis la v21.3, les formateurs Intl sont intégrés à l'interpolation, l'espace réservé porte donc son format :

{
  "downloads_one": "{{count, number}} download",
  "downloads_other": "{{count, number}} downloads",
  "price": "On sale for {{val, currency(USD)}}",
  "updated": "Updated {{val, relativetime}}",
  "released": "Released on {{val, datetime}}",
  "authors": "By {{val, list}}"
}
t('downloads', { count: 1234567 })  // "1,234,567 downloads"
t('price', { val: 42.5 })           // "On sale for $42.50"
t('updated', { val: -3 })           // "Updated 3 days ago"
t('released', { val: new Date() })  // "Released on 7/7/2026"
t('authors', { val: ['Nina', 'Marco', 'Aisha'] })  // "By Nina, Marco, and Aisha"

i18next passe la langue active à la bonne API Intl pour vous, et les versions actuelles mettent en cache les instances de formateurs sous-jacentes. La clé downloads montre comment le formatage se compose avec les suffixes de pluriel _one / _other : count sélectionne la forme plurielle et s'affiche localisé. Et comme il s'agit d'une interpolation t() ordinaire, cela fonctionne tel quel dans react-i18next, next-i18next et tous les autres bindings.

Les options de format se placent soit en ligne dans la clé, soit par appel via formatParams :

{
  "revenue": "Revenue: {{val, currency}}",
  "score": "{{val, number(minimumFractionDigits: 2)}} points"
}
t('revenue', {
  val: 12345.67,
  formatParams: { val: { currency: 'EUR' } }
})
// active language en → "Revenue: €12,345.67"
// active language de → "Umsatz: 12.345,67 €"

La chaîne comme le formatage suivent la langue active ; rien ne change dans votre composant. Gardez les ressources en simple .json (référence du format), et notez qu'il s'agit de formatage au niveau de l'interpolation : pour comparer des formats de messages entiers (ICU, Fluent, gettext, …), voir I18N in the Multiverse of Formats.

Formateurs personnalisés

Tout ce qu'Intl ne couvre pas, vous pouvez l'enregistrer vous-même et l'utiliser comme un format intégré :

i18next.services.formatter.add('filesize', (value, lng) => {
  const mb = value / (1024 * 1024)
  return `${new Intl.NumberFormat(lng, { maximumFractionDigits: 1 }).format(mb)} MB`
})
{ "attachment": "Attachment size: {{val, filesize}}" }

Si le travail d'initialisation se fait par locale (comme la construction de cet Intl.NumberFormat), les versions récentes d'i18next offrent aussi formatter.addCached, qui construit le formateur une fois par langue et le réutilise. Les projets démarrés avant i18next v21 utilisent parfois encore la fonction héritée interpolation.format(value, format, lng) ; elle fonctionne toujours, mais les formats intégrés couvrent désormais l'essentiel de ce à quoi elle servait (typiquement des wrappers Moment.js ou Numeral.js).

Erreurs fréquentes

  • Concaténer les symboles. '$' + price.toFixed(2) se trompe de symbole, de position, de séparateurs et de nombre de décimales (le JPY n'en a pas). Utilisez style: 'currency', toujours.
  • Pas d'argument de locale. toLocaleString() ou new Intl.NumberFormat() sans locale utilise la locale du runtime, pas la langue de votre utilisateur, une source classique d'écarts d'hydratation serveur/client. Les formats intégrés d'i18next passent toujours la langue active.
  • Déduire la devise de la langue de l'interface. La devise est une donnée de la transaction. Formatez USD avec les séparateurs allemands pour un utilisateur allemand ; ne basculez pas silencieusement vers EUR.
  • Saisir les formats dans les traductions. Si les traducteurs écrivent des motifs du type 1.234,56 dans les chaînes, chaque changement de texte risque un bug de format. Gardez l'espace réservé brut ({{val, number}}) dans la chaîne et laissez CLDR faire le reste.
  • Confondre formatage et fuseaux horaires. Intl.DateTimeFormat affiche dans le fuseau du runtime sauf si vous passez timeZone. Le formatage localise l'affichage, il ne déplace pas l'horloge.
  • Données ICU manquantes sur le serveur. Les builds officiels de Node.js embarquent l'ICU complet depuis la v13, mais les builds personnalisés ou small-icu retombent en silence sur les formats anglais, un bug qui ne se voit que côté serveur.

Gérer les chaînes formatées d'une langue à l'autre

Le formatage lui-même est gratuit, CLDR fait le travail. Ce qui casse en pratique, ce sont les espaces réservés. Un traducteur qui retape {{val, currency}} en {{val currency}}, ou qui traduit le mot relativetime en français, casse exactement une langue en production, et aucun test écrit sur les chaînes anglaises ne l'attrapera.

C'est là qu'un système de gestion de traduction justifie sa place. Locize est construit autour du format de messages d'i18next :

  • Les espaces réservés comme {{val, datetime}} restent visibles dans l'éditeur ; les traducteurs les déplacent au bon endroit au lieu de les retaper.
  • La traduction automatique et par IA connaît le format i18next, les espaces réservés survivent donc intacts à l'aller-retour.
  • Les clés ajoutées via saveMissing arrivent avec leurs espaces réservés, prêtes à traduire.

Prenez le formatage dans Intl, gardez les chaînes en JSON, et ajouter une locale devient une tâche de traduction, pas d'ingénierie, ce qui est tout l'objectif de l'i18n.

Foire aux questions

Comment formater les nombres et les dates dans i18next ?

Depuis i18next v21.3, le formatage est intégré : écrivez {{val, number}}, {{val, datetime}}, {{val, currency(USD)}}, {{val, relativetime}} ou {{val, list}} directement dans la chaîne de traduction. i18next appelle l'API Intl correspondante (Intl.NumberFormat, Intl.DateTimeFormat, …) avec la langue active, de sorte que la sortie suit automatiquement la locale.

Comment formater une devise dans i18next ?

Utilisez {{val, currency(USD)}} dans la chaîne de traduction, ou gardez la chaîne générique ({{val, currency}}) et passez le code devise à l'appel via formatParams. Le code devise doit venir de vos données de transaction, pas de la langue de l'interface : un utilisateur allemand peut payer en USD, ce qui s'affiche 1.234,50 $ avec les séparateurs allemands et le symbole dollar.

Qu'est-ce que l'API Intl ?

Intl est l'API d'internationalisation ECMAScript intégrée à tous les navigateurs modernes et à Node.js. Elle fournit des formateurs sensibles à la locale, adossés aux données CLDR : Intl.NumberFormat (nombres, devises, pourcentages, unités), Intl.DateTimeFormat (dates et heures), Intl.RelativeTimeFormat (« il y a 3 jours »), Intl.ListFormat (listes de mots) et Intl.PluralRules (catégories de pluriel). Aucune bibliothèque supplémentaire ni fichier de locale à livrer.

Ai-je encore besoin de Moment.js, date-fns ou Numeral.js pour le formatage i18n ?

Pour formater l'affichage : en général non. Intl.DateTimeFormat et Intl.NumberFormat couvrent l'affichage sensible à la locale sans expédier de bundles de locales. Des bibliothèques comme date-fns ou Day.js restent utiles pour l'arithmétique des dates et le parsing, et Moment.js est en mode maintenance ; sa propre documentation recommande des alternatives basées sur Intl.

Pourquoi mon application affiche-t-elle le mauvais format de nombre ou de date ?

La cause la plus courante est un appel à toLocaleString() ou new Intl.NumberFormat() sans argument de locale : c'est la locale du runtime qui est utilisée, pas la langue choisie par l'utilisateur, ce qui provoque aussi des écarts d'hydratation serveur/client. Les formateurs intégrés d'i18next passent toujours la langue active. Vérifiez aussi que la valeur est bien un nombre ou un objet Date, pas une chaîne.

Comment fonctionnent les temps relatifs comme « il y a 3 jours » ?

Intl.RelativeTimeFormat formate un décalage numérique plus une unité : format(-3, "day") donne « il y a 3 jours », format(2, "hour") donne « dans 2 heures », et l'option numeric: "auto" transforme un décalage de 1 jour en « hier » ou « demain ». Dans i18next, vous écrivez {{val, relativetime}} et passez le décalage ; le calcul du décalage à partir d'un timestamp reste dans votre code.

Puis-je ajouter mon propre format à i18next ?

Oui. Enregistrez-le avec i18next.services.formatter.add("myformat", (value, lng, options) => …) et utilisez {{val, myformat}} dans les chaînes de traduction. Pour le travail d'initialisation par locale, il existe formatter.addCached, qui construit le formateur une fois par langue et le réutilise, le même cache qu'i18next applique à ses formats Intl intégrés.

Les traducteurs doivent-ils modifier les formats de nombres ou de dates ?

Non. Les séparateurs, la position du symbole et l'ordre des dates viennent de CLDR via Intl, par locale, personne n'a donc à les saisir. Les traducteurs déplacent seulement les espaces réservés comme {{val, datetime}} à la bonne position dans la phrase. Un système de gestion de traduction comme Locize garde ces espaces réservés visibles et traduit automatiquement autour d'eux sans les casser.