Cómo formatear cantidades de moneda en React Router v7

Mostrar precios con símbolos de moneda y separadores

Problema

Mostrar precios en aplicaciones web requiere manejar dos aspectos interrelacionados de localización: la moneda utilizada y las convenciones regionales para presentar valores monetarios. Un precio de 1200.50 dólares estadounidenses aparece como "$1,200.50" para usuarios en Estados Unidos pero como "1 200,50 $US" en Francia. La posición del símbolo de moneda, el separador decimal, la agrupación de miles y el espaciado varían según la configuración regional. Cuando estas convenciones no se respetan, los usuarios pueden malinterpretar las cantidades o cuestionar si el precio es correcto, socavando la confianza en la aplicación.

El desafío se complica cuando una aplicación sirve a múltiples regiones o permite a los usuarios ver precios en diferentes monedas. Codificar de forma rígida las cadenas de formato o colocar manualmente los símbolos crea código frágil que falla cuando se añaden nuevas configuraciones regionales o monedas. Sin un enfoque sistemático, mantener visualizaciones de precios consistentes y correctas en toda la aplicación se vuelve propenso a errores.

Solución

Formatear valores de moneda utilizando tanto el código de moneda objetivo como la configuración regional activa del usuario. Este enfoque delega las reglas complejas de presentación de moneda a las APIs de internacionalización del navegador, que ya codifican las convenciones de formato para cientos de combinaciones de configuración regional y moneda.

Al pasar una cantidad numérica, un código de moneda y la configuración regional actual a una función de formateo, el sistema aplica automáticamente el símbolo correcto, los separadores y el diseño. Esto asegura que los precios siempre aparezcan en un formato familiar para el usuario, independientemente de qué moneda se esté mostrando o en qué región se encuentre el usuario.

Pasos

1. Crear un componente reutilizable de formateo de moneda

Construir un componente que utilice FormattedNumber de react-intl con las propiedades style y currency para formatear valores monetarios.

import { FormattedNumber } from "react-intl";

interface PriceProps {
  amount: number;
  currency: string;
}

export function Price({ amount, currency }: PriceProps) {
  return (
    <FormattedNumber value={amount} style="currency" currency={currency} />
  );
}

El componente FormattedNumber utiliza la API formatNumber y acepta Intl.NumberFormatOptions. La opción style="currency" indica al formateador que incluya símbolos de moneda, y la propiedad currency especifica qué moneda mostrar.

2. Utiliza el componente Price en componentes de ruta

Renderiza precios en cualquier componente de ruta de React Router pasando el valor numérico y el código de moneda.

import { Price } from "~/components/Price";

export default function ProductPage() {
  const product = {
    name: "Auriculares inalámbricos",
    price: 129.99,
    currency: "USD",
  };

  return (
    <div>
      <h1>{product.name}</h1>
      <p>
        <Price amount={product.price} currency={product.currency} />
      </p>
    </div>
  );
}

El componente Price formatea automáticamente el importe según la configuración regional establecida en el IntlProvider. Para una configuración regional de EE.UU., esto renderiza "$129.99"; para una configuración regional alemana con la misma moneda USD, renderiza "129,99 $".

3. Formatea la moneda imperativamente cuando sea necesario

Para escenarios donde necesitas la cadena formateada directamente, utiliza el hook useIntl para acceder al método formatNumber.

import { useIntl } from "react-intl";

export default function CheckoutSummary() {
  const intl = useIntl();
  const subtotal = 89.99;
  const tax = 7.2;
  const total = subtotal + tax;

  const formattedTotal = intl.formatNumber(total, {
    style: "currency",
    currency: "EUR",
  });

  return (
    <div>
      <h2>Resumen del pedido</h2>
      <dl>
        <dt>Subtotal</dt>
        <dd>
          {intl.formatNumber(subtotal, {
            style: "currency",
            currency: "EUR",
          })}
        </dd>
        <dt>Impuesto</dt>
        <dd>
          {intl.formatNumber(tax, {
            style: "currency",
            currency: "EUR",
          })}
        </dd>
        <dt>Total</dt>
        <dd aria-label={`Total: ${formattedTotal}`}>
          <strong>{formattedTotal}</strong>
        </dd>
      </dl>
    </div>
  );
}

El método formatNumber acepta un valor y un objeto de opciones con FormatNumberOptions. Esto es útil cuando necesitas la cadena formateada para atributos, registro o contextos que no son JSX.

4. Controla la precisión decimal para monedas de números enteros

Algunas monedas no utilizan unidades fraccionarias. Ajusta el número de decimales para que coincida con las convenciones monetarias.

import { FormattedNumber } from "react-intl";

interface PriceProps {
  amount: number;
  currency: string;
}

export function Price({ amount, currency }: PriceProps) {
  const isWholeNumberCurrency = currency === "JPY" || currency === "KRW";

  return (
    <FormattedNumber
      value={amount}
      style="currency"
      currency={currency}
      minimumFractionDigits={isWholeNumberCurrency ? 0 : 2}
      maximumFractionDigits={isWholeNumberCurrency ? 0 : 2}
    />
  );
}

El yen japonés no utiliza una unidad menor, por lo que los importes se muestran sin decimales. Las opciones minimumFractionDigits y maximumFractionDigits anulan el comportamiento decimal predeterminado cuando es necesario.