feat(i18n): locale-aware formatters and useLanguage hook
- Create useLanguage() hook following useCurrency/useWeightUnit pattern - Update formatPrice() to use Intl.NumberFormat for locale-aware currency display - Update formatWeight() to use Intl.NumberFormat for locale-aware number formatting - Update formatDualPrice() to pass locale through - Update useFormatters() to pass locale to all formatters - Add formatter tests for en/de locales (15 tests passing) Phase 34, Plan 03
This commit is contained in:
@@ -7,41 +7,50 @@ const GRAMS_PER_KG = 1000;
|
||||
export function formatWeight(
|
||||
grams: number | null | undefined,
|
||||
unit: WeightUnit = "g",
|
||||
locale = "en",
|
||||
): string {
|
||||
if (grams == null) return "--";
|
||||
let value: number;
|
||||
let fractionDigits: number;
|
||||
switch (unit) {
|
||||
case "g":
|
||||
return `${Math.round(grams)}g`;
|
||||
value = Math.round(grams);
|
||||
fractionDigits = 0;
|
||||
break;
|
||||
case "oz":
|
||||
return `${(grams / GRAMS_PER_OZ).toFixed(1)} oz`;
|
||||
value = grams / GRAMS_PER_OZ;
|
||||
fractionDigits = 1;
|
||||
break;
|
||||
case "lb":
|
||||
return `${(grams / GRAMS_PER_LB).toFixed(2)} lb`;
|
||||
value = grams / GRAMS_PER_LB;
|
||||
fractionDigits = 2;
|
||||
break;
|
||||
case "kg":
|
||||
return `${(grams / GRAMS_PER_KG).toFixed(2)} kg`;
|
||||
value = grams / GRAMS_PER_KG;
|
||||
fractionDigits = 2;
|
||||
break;
|
||||
}
|
||||
const formatted = new Intl.NumberFormat(locale, {
|
||||
minimumFractionDigits: fractionDigits,
|
||||
maximumFractionDigits: fractionDigits,
|
||||
}).format(value);
|
||||
return unit === "g" ? `${formatted}g` : `${formatted} ${unit}`;
|
||||
}
|
||||
|
||||
export type Currency = "USD" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD";
|
||||
|
||||
const CURRENCY_SYMBOLS: Record<Currency, string> = {
|
||||
USD: "$",
|
||||
EUR: "€",
|
||||
GBP: "£",
|
||||
JPY: "¥",
|
||||
CAD: "CA$",
|
||||
AUD: "A$",
|
||||
};
|
||||
|
||||
export function formatPrice(
|
||||
cents: number | null | undefined,
|
||||
currency: Currency = "USD",
|
||||
locale = "en",
|
||||
): string {
|
||||
if (cents == null) return "--";
|
||||
const symbol = CURRENCY_SYMBOLS[currency];
|
||||
if (currency === "JPY") {
|
||||
return `${symbol}${Math.round(cents / 100)}`;
|
||||
}
|
||||
return `${symbol}${(cents / 100).toFixed(2)}`;
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency,
|
||||
minimumFractionDigits: currency === "JPY" ? 0 : 2,
|
||||
maximumFractionDigits: currency === "JPY" ? 0 : 2,
|
||||
}).format(cents / 100);
|
||||
}
|
||||
|
||||
export interface DualPriceOptions {
|
||||
@@ -49,6 +58,7 @@ export interface DualPriceOptions {
|
||||
sourceCurrency: Currency;
|
||||
targetCurrency: Currency;
|
||||
convertedCents: number;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,7 +69,8 @@ export function formatDualPrice(options: DualPriceOptions): {
|
||||
source: string;
|
||||
converted: string;
|
||||
} {
|
||||
const source = formatPrice(options.sourceCents, options.sourceCurrency);
|
||||
const converted = `~${formatPrice(options.convertedCents, options.targetCurrency)}`;
|
||||
const locale = options.locale ?? "en";
|
||||
const source = formatPrice(options.sourceCents, options.sourceCurrency, locale);
|
||||
const converted = `~${formatPrice(options.convertedCents, options.targetCurrency, locale)}`;
|
||||
return { source, converted };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user