Formatage monétaire avec Claude Code : guide Intl.NumberFormat
Implémentez les devises avec Intl.NumberFormat, minor units, arrondi, affichage comptable et tests JPY/USD/EUR.
Le formatage monétaire fait partie de la facturation
Afficher $19.99, ¥1,980 ou R$ 1.234,56 semble être un détail d’interface. En réalité, dans un SaaS, une boutique ou une facture, cela touche les remboursements, les taxes, les exports et les rapports de revenus. Le JPY s’affiche généralement sans décimales, l’USD et l’EUR avec deux, l’anglais indien groupe les grands nombres comme12,34,567, et les montants négatifs peuvent exiger une notation comptable comme($10.00).
La règle robuste consiste à stocker un entier en minor units, à calculer sur ces entiers, puis à formater seulement au dernier moment avecIntl.NumberFormat. Une minor unit est l’unité minimale de calcul : centimes pour USD, yen entier pour JPY.
Ce guide donne une base que Claude Code peut modifier sans masquer les décisions de facturation : prix SaaS multi-devises, arrondi, affichage comptable, interdiction de stocker des chaînes formatées, tests JPY/USD/EUR/BRL/INR/IDR et prompt de revue. Les références officielles sont MDN pourIntl.NumberFormat, MDN pourformatToParts et la spécificationECMA-402 NumberFormat.
flowchart LR
A[DB: minor unit integer] --> B[Domain math]
B --> C[Tax, discount, refund]
C --> D[Intl.NumberFormat]
D --> E[UI, invoice, email]
Séparer la donnée stockée du texte affiché
Ne stockez pas"$19.99" en base. StockezamountMinor etcurrency : 1999 USD, 1980 JPY, 123456 IDR. Vous évitez ainsi le parsing de textes localisés pour trier, additionner, rembourser ou auditer.
| Approche | Exemple | Avantage | Risque |
|---|---|---|---|
| Chaîne formatée | "$19.99" | Rapide pour une page statique | Casse les rapports, remboursements et traductions |
| Nombre décimal | 19.99 | Simple au début | Erreurs de virgule flottante et règles de devise dispersées |
| Entier minor unit | 1999 | Calculs et tests stables | Conversion nécessaire aux frontières |
Intl.NumberFormat formate. Il ne décide pas des taux de change, des taxes, des règles Stripe/Paddle ni du moment où une facture doit être arrondie.
Implémentation exécutable
Enregistrez ce fichier souscurrency-format-demo.mjs, puis lanceznode currency-format-demo.mjs.
// currency-format-demo.mjs
import assert from "node:assert/strict";
const minorUnitDigits = Object.freeze({
JPY: 0,
USD: 2,
EUR: 2,
BRL: 2,
INR: 2,
IDR: 0,
});
function assertCurrency(currency) {
if (!(currency in minorUnitDigits)) {
throw new Error(`Unsupported currency: ${currency}`);
}
}
function roundHalfAwayFromZero(value) {
return value < 0 ? -Math.round(Math.abs(value)) : Math.round(value);
}
export function moneyFromMajor(amount, currency) {
assertCurrency(currency);
if (!Number.isFinite(amount)) {
throw new Error(`Invalid amount: ${amount}`);
}
const digits = minorUnitDigits[currency];
return {
minor: roundHalfAwayFromZero(amount * 10 ** digits),
currency,
};
}
export function toMajor(money) {
assertCurrency(money.currency);
return money.minor / 10 ** minorUnitDigits[money.currency];
}
export function addMoney(left, right) {
if (left.currency !== right.currency) {
throw new Error(`Currency mismatch: ${left.currency} vs ${right.currency}`);
}
return { minor: left.minor + right.minor, currency: left.currency };
}
export function multiplyMoney(money, factor) {
if (!Number.isFinite(factor)) {
throw new Error(`Invalid factor: ${factor}`);
}
return {
minor: roundHalfAwayFromZero(money.minor * factor),
currency: money.currency,
};
}
export function formatMoney(
money,
{
locale = "en-US",
accounting = false,
currencyDisplay = "symbol",
roundingMode = "halfExpand",
} = {},
) {
assertCurrency(money.currency);
return new Intl.NumberFormat(locale, {
style: "currency",
currency: money.currency,
currencyDisplay,
currencySign: accounting ? "accounting" : "standard",
roundingMode,
}).format(toMajor(money));
}
const samples = [
["ja-JP", { minor: 123456, currency: "JPY" }],
["en-US", { minor: 123456, currency: "USD" }],
["de-DE", { minor: 123456, currency: "EUR" }],
["pt-BR", { minor: 123456, currency: "BRL" }],
["en-IN", { minor: 123456789, currency: "INR" }],
["id-ID", { minor: 123456, currency: "IDR" }],
];
for (const [locale, money] of samples) {
const formatter = new Intl.NumberFormat(locale, {
style: "currency",
currency: money.currency,
});
const options = formatter.resolvedOptions();
const parts = formatter.formatToParts(toMajor(money));
assert.equal(options.maximumFractionDigits, minorUnitDigits[money.currency]);
assert.ok(parts.some((part) => part.type === "currency"));
console.log(`${locale} ${money.currency}: ${formatMoney(money, { locale })}`);
}
assert.equal(
addMoney(moneyFromMajor(19.99, "USD"), moneyFromMajor(5, "USD")).minor,
2499,
);
assert.equal(multiplyMoney(moneyFromMajor(1980, "JPY"), 1.1).minor, 2178);
assert.match(
formatMoney({ minor: -129900, currency: "USD" }, { locale: "en-US", accounting: true }),
/^\(\$/,
);
assert.throws(
() => addMoney(moneyFromMajor(10, "USD"), moneyFromMajor(10, "JPY")),
/Currency mismatch/,
);
console.log("currency formatting checks passed");
Cas d’usage réels (Use case)
Un tableau de prix SaaS multi-devises doit recevoir des données structurées, pas des libellés déjà traduits.
type CurrencyCode = "JPY" | "USD" | "EUR" | "BRL" | "INR" | "IDR";
type PlanPrice = {
planId: "starter" | "pro" | "team";
currency: CurrencyCode;
amountMinor: number;
};
const prices: PlanPrice[] = [
{ planId: "pro", currency: "JPY", amountMinor: 1980 },
{ planId: "pro", currency: "USD", amountMinor: 1999 },
{ planId: "pro", currency: "EUR", amountMinor: 1899 },
];
Deuxième cas : factures et remboursements. currencySign: "accounting" peut produire une notation plus adaptée pour les montants négatifs, mais chaque locale garde ses règles. Pour un PDF de facture, ajoutez des tests de rendu.
Troisième cas : taxes, remises et prorata. 1999 * 10 / 31 ne tombe pas juste. Décidez si l’arrondi se fait par ligne ou après totalisation, puis rendez cette règle visible dans les noms de tests.
Quatrième cas : back-office et CSV. Exportezamount_minor, currency etamount_display. Les humains lisent le dernier champ, les outils de BI utilisent les deux premiers pour calculer MRR, LTV et remboursements.
Pitfall : pièges fréquents et corrections
Ne stockez pas les chaînes monétaires formatées. Ne confondez pas locale et devise : une interface française peut afficher une facture USD. Ne supposez pas deux décimales pour toutes les devises. Ne prenez pasroundingMode pour une politique de facturation complète. Pour isoler le symbole ou les décimales, utilisezformatToParts plutôt qu’une expression régulière.
Périmètre de Claude Code et validation humaine
Claude Code peut rédiger le helper de formatage, remplacer les appels dispersés à toLocaleString, ajouter des tests avec des montants synthétiques et signaler les champs suspects comme formattedPrice ou displayAmount. Il peut aussi proposer des diffs pour les composants de prix, les factures et les exports CSV lorsqu’ils reçoivent amountMinor, currency et locale.
La validation humaine reste obligatoire pour les prix, les taxes, les règles de remboursement, les migrations de données réelles, les emails envoyés aux clients et la configuration du prestataire de paiement. Ne laissez pas l’agent deviner les taux de change ni les obligations légales d’une facture. Utilisez d’abord des données synthétiques, exécutez le script Node, puis faites relire le diff par finance, support ou produit avant la production.
Prompt de revue pour Claude Code
Review this repository's money formatting and billing math.
Requirements:
- Check that DB/API models do not store formatted currency strings
- Make JPY/USD/EUR/BRL/INR/IDR minor units explicit
- Use Intl.NumberFormat while keeping locale and currency separate
- Decide whether refunds and discounts need currencySign: "accounting"
- Make rounding policy visible in test names
- Add Node-runnable tests for currency formatting behavior
À lire ensuite
Pour l’internationalisation générale, lisezimplémenter i18n avec Claude Code. Pour les dates et fuseaux horaires, voyezgestion date/heure. Pour la monétisation SaaS, combinez ce guide avecStripe subscription.
CTA principal : utilisez leparcours de training Claude Code si vous voulez une checklist guidée pour facturation, auth, sécurité et release. ClaudeCodeLab publie aussi des prompts pour les zones que l’IA simplifie trop vite. Avant de confier une page de prix à Claude Code, vérifiez que les données sont déjà séparées enamountMinor, currency etlocale.
Vérification pratique
Le script a été exécuté avec Node.js 24. Il vérifie les chiffres fractionnaires et les parties de devise pour JPY/USD/EUR/BRL/INR/IDR, l’affichage comptable USD, les calculs entiers et l’erreur de devise mélangée. Les chaînes exactes peuvent varier selon les données ICU ; testez donc la politique métier, pas seulement une capture de texte.
Articles liés
Implémenter l'i18n avec Claude Code : guide Next.js pratique
Méthode concrète pour ajouter l'i18n à Next.js avec Claude Code, next-intl, contrôles de traduction et pièges courants.
Dates et heures avec Claude Code : guide pratique
Guide pratique pour gérer fuseaux horaires, DST, Intl, tests et stockage des dates avec Claude Code.
Configurer Prettier avec Claude Code
Installez Prettier pour Claude Code avec .prettierrc, VS Code, CI, formatage staged et pièges à éviter.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.