Appearance
数字与货币格式化 - 国际化数字处理完整指南
1. 数字格式化基础
1.1 Intl.NumberFormat概述
Intl.NumberFormat是JavaScript内置的数字格式化API,支持多种语言环境和格式选项。
typescript
// 基础用法
const formatter = new Intl.NumberFormat('zh-CN');
formatter.format(1234567.89); // "1,234,567.89"
// 不同地区格式
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('fr-FR').format(1234567.89); // "1 234 567,89"
new Intl.NumberFormat('ar-EG').format(1234567.89); // "١٬٢٣٤٬٥٦٧٫٨٩"1.2 格式化选项
typescript
interface NumberFormatOptions {
style?: 'decimal' | 'currency' | 'percent' | 'unit';
currency?: string; // 货币代码
currencyDisplay?: 'symbol' | 'narrowSymbol' | 'code' | 'name';
currencySign?: 'standard' | 'accounting';
unit?: string; // 单位
unitDisplay?: 'short' | 'narrow' | 'long';
minimumIntegerDigits?: number;
minimumFractionDigits?: number;
maximumFractionDigits?: number;
minimumSignificantDigits?: number;
maximumSignificantDigits?: number;
notation?: 'standard' | 'scientific' | 'engineering' | 'compact';
compactDisplay?: 'short' | 'long';
useGrouping?: boolean | 'min2' | 'auto' | 'always';
signDisplay?: 'auto' | 'never' | 'always' | 'exceptZero';
}2. 数字格式化
2.1 基础数字格式
typescript
// 千分位分隔符
new Intl.NumberFormat('en-US').format(1234567);
// "1,234,567"
// 小数位数
new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(1234.5);
// "1,234.50"
// 有效数字
new Intl.NumberFormat('en-US', {
minimumSignificantDigits: 3,
maximumSignificantDigits: 5
}).format(1234.5678);
// "1,234.6"
// 不使用分组
new Intl.NumberFormat('en-US', {
useGrouping: false
}).format(1234567);
// "1234567"2.2 React组件实现
tsx
// NumberDisplay.tsx
interface NumberDisplayProps {
value: number;
locale?: string;
options?: Intl.NumberFormatOptions;
}
export function NumberDisplay({
value,
locale = 'zh-CN',
options = {}
}: NumberDisplayProps) {
const formatted = useMemo(() => {
return new Intl.NumberFormat(locale, options).format(value);
}, [value, locale, options]);
return <span>{formatted}</span>;
}
// 使用示例
function Example() {
return (
<div>
<NumberDisplay value={1234567.89} />
<NumberDisplay
value={1234.5}
options={{
minimumFractionDigits: 2,
maximumFractionDigits: 2
}}
/>
</div>
);
}2.3 紧凑格式
typescript
// 紧凑数字表示
const compactShort = new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'short'
});
compactShort.format(1234); // "1.2K"
compactShort.format(1234567); // "1.2M"
compactShort.format(1234567890); // "1.2B"
// 长格式
const compactLong = new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'long'
});
compactLong.format(1234567); // "1.2 million"
// 中文紧凑格式
const compactZh = new Intl.NumberFormat('zh-CN', {
notation: 'compact'
});
compactZh.format(10000); // "1万"
compactZh.format(1234567); // "123万"
compactZh.format(12345678); // "1235万"
// 自定义紧凑格式
function formatCompact(num: number, locale: string = 'zh-CN'): string {
const units = {
'zh-CN': ['', '万', '亿', '万亿'],
'en-US': ['', 'K', 'M', 'B', 'T']
};
const divisors = locale === 'zh-CN' ? [1, 1e4, 1e8, 1e12] : [1, 1e3, 1e6, 1e9, 1e12];
const unitList = units[locale] || units['en-US'];
for (let i = divisors.length - 1; i >= 0; i--) {
if (Math.abs(num) >= divisors[i]) {
const value = num / divisors[i];
return value.toFixed(1).replace(/\.0$/, '') + unitList[i];
}
}
return num.toString();
}
formatCompact(1234567, 'zh-CN'); // "123.5万"
formatCompact(1234567, 'en-US'); // "1.2M"3. 货币格式化
3.1 基础货币格式
typescript
// 美元
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(1234.56);
// "$1,234.56"
// 人民币
new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(1234.56);
// "¥1,234.56"
// 欧元
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(1234.56);
// "1.234,56 €"
// 日元(无小数)
new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY'
}).format(1234.56);
// "¥1,235"3.2 货币显示选项
typescript
// 货币符号
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencyDisplay: 'symbol'
}).format(100);
// "$100.00"
// 窄符号
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencyDisplay: 'narrowSymbol'
}).format(100);
// "$100.00"
// 货币代码
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencyDisplay: 'code'
}).format(100);
// "USD 100.00"
// 货币名称
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencyDisplay: 'name'
}).format(100);
// "100.00 US dollars"
// 会计格式(负数用括号)
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencySign: 'accounting'
}).format(-100);
// "($100.00)"3.3 货币组件
tsx
// CurrencyDisplay.tsx
interface CurrencyDisplayProps {
amount: number;
currency: string;
locale?: string;
options?: Intl.NumberFormatOptions;
}
export function CurrencyDisplay({
amount,
currency,
locale = 'zh-CN',
options = {}
}: CurrencyDisplayProps) {
const formatted = useMemo(() => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
...options
}).format(amount);
}, [amount, currency, locale, options]);
return (
<span className={amount < 0 ? 'text-red-600' : 'text-green-600'}>
{formatted}
</span>
);
}
// 使用示例
function PriceList() {
return (
<div>
<CurrencyDisplay amount={99.99} currency="USD" locale="en-US" />
<CurrencyDisplay amount={888} currency="CNY" />
<CurrencyDisplay amount={-50} currency="USD" options={{ currencySign: 'accounting' }} />
</div>
);
}3.4 多货币支持
tsx
// MultiCurrencyDisplay.tsx
const currencies = [
{ code: 'USD', symbol: '$', name: '美元' },
{ code: 'CNY', symbol: '¥', name: '人民币' },
{ code: 'EUR', symbol: '€', name: '欧元' },
{ code: 'GBP', symbol: '£', name: '英镑' },
{ code: 'JPY', symbol: '¥', name: '日元' }
];
export function MultiCurrencyDisplay({
amount,
fromCurrency,
exchangeRates
}: {
amount: number;
fromCurrency: string;
exchangeRates: Record<string, number>;
}) {
return (
<div className="space-y-2">
{currencies.map(currency => {
const convertedAmount = amount * (exchangeRates[currency.code] || 1);
return (
<div key={currency.code} className="flex justify-between">
<span>{currency.name}</span>
<CurrencyDisplay
amount={convertedAmount}
currency={currency.code}
/>
</div>
);
})}
</div>
);
}4. 百分比格式化
4.1 基础百分比
typescript
// 百分比格式
new Intl.NumberFormat('en-US', {
style: 'percent'
}).format(0.157);
// "15.7%"
// 带小数位
new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(0.157);
// "15.70%"
// 整数百分比
new Intl.NumberFormat('en-US', {
style: 'percent',
maximumFractionDigits: 0
}).format(0.157);
// "16%"4.2 百分比组件
tsx
// PercentageDisplay.tsx
interface PercentageDisplayProps {
value: number; // 0-1之间的值
locale?: string;
decimals?: number;
showSign?: boolean;
}
export function PercentageDisplay({
value,
locale = 'zh-CN',
decimals = 2,
showSign = false
}: PercentageDisplayProps) {
const formatted = useMemo(() => {
const formatter = new Intl.NumberFormat(locale, {
style: 'percent',
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
signDisplay: showSign ? 'always' : 'auto'
});
return formatter.format(value);
}, [value, locale, decimals, showSign]);
const colorClass = value > 0 ? 'text-green-600' : value < 0 ? 'text-red-600' : '';
return <span className={colorClass}>{formatted}</span>;
}
// 使用
function Example() {
return (
<div>
<PercentageDisplay value={0.157} /> {/* 15.70% */}
<PercentageDisplay value={0.05} showSign /> {/* +5.00% */}
<PercentageDisplay value={-0.03} showSign /> {/* -3.00% */}
</div>
);
}5. 单位格式化
5.1 度量单位
typescript
// 距离
new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'kilometer'
}).format(100);
// "100 km"
new Intl.NumberFormat('zh-CN', {
style: 'unit',
unit: 'kilometer',
unitDisplay: 'long'
}).format(100);
// "100公里"
// 速度
new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'mile-per-hour'
}).format(60);
// "60 mph"
// 温度
new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'celsius'
}).format(25);
// "25°C"
new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'fahrenheit'
}).format(77);
// "77°F"
// 文件大小
new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'megabyte'
}).format(1024);
// "1,024 MB"5.2 单位组件
tsx
// UnitDisplay.tsx
interface UnitDisplayProps {
value: number;
unit: string;
locale?: string;
unitDisplay?: 'short' | 'narrow' | 'long';
}
export function UnitDisplay({
value,
unit,
locale = 'zh-CN',
unitDisplay = 'short'
}: UnitDisplayProps) {
const formatted = useMemo(() => {
return new Intl.NumberFormat(locale, {
style: 'unit',
unit,
unitDisplay
}).format(value);
}, [value, unit, locale, unitDisplay]);
return <span>{formatted}</span>;
}
// 文件大小组件
export function FileSize({ bytes }: { bytes: number }) {
const [value, unit] = useMemo(() => {
const units = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'];
const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
const size = bytes / Math.pow(1024, unitIndex);
return [size, units[Math.min(unitIndex, units.length - 1)]];
}, [bytes]);
return (
<UnitDisplay
value={value}
unit={unit}
locale="en-US"
unitDisplay="narrow"
/>
);
}
// 使用
<FileSize bytes={1048576} /> {/* 1 MB */}
<FileSize bytes={1073741824} /> {/* 1 GB */}6. 科学计数法
typescript
// 科学计数法
new Intl.NumberFormat('en-US', {
notation: 'scientific'
}).format(123456);
// "1.235E5"
// 工程计数法
new Intl.NumberFormat('en-US', {
notation: 'engineering'
}).format(123456);
// "123.456E3"
// 指定有效数字
new Intl.NumberFormat('en-US', {
notation: 'scientific',
minimumSignificantDigits: 3,
maximumSignificantDigits: 5
}).format(123456);
// "1.2346E5"7. 符号显示
typescript
// 符号显示选项
const options: Intl.NumberFormatOptions = {
signDisplay: 'always'
};
new Intl.NumberFormat('en-US', options).format(5); // "+5"
new Intl.NumberFormat('en-US', options).format(-5); // "-5"
new Intl.NumberFormat('en-US', options).format(0); // "+0"
// exceptZero: 零不显示符号
const exceptZeroOptions: Intl.NumberFormatOptions = {
signDisplay: 'exceptZero'
};
new Intl.NumberFormat('en-US', exceptZeroOptions).format(5); // "+5"
new Intl.NumberFormat('en-US', exceptZeroOptions).format(-5); // "-5"
new Intl.NumberFormat('en-US', exceptZeroOptions).format(0); // "0"
// never: 不显示符号
const neverOptions: Intl.NumberFormatOptions = {
signDisplay: 'never'
};
new Intl.NumberFormat('en-US', neverOptions).format(5); // "5"
new Intl.NumberFormat('en-US', neverOptions).format(-5); // "5"8. 实战组件
8.1 统计卡片
tsx
// StatCard.tsx
interface StatCardProps {
label: string;
value: number;
type: 'currency' | 'number' | 'percent';
change?: number;
currency?: string;
locale?: string;
}
export function StatCard({
label,
value,
type,
change,
currency = 'USD',
locale = 'en-US'
}: StatCardProps) {
const formatValue = () => {
switch (type) {
case 'currency':
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(value);
case 'percent':
return new Intl.NumberFormat(locale, {
style: 'percent',
minimumFractionDigits: 1,
maximumFractionDigits: 1
}).format(value);
default:
return new Intl.NumberFormat(locale, {
notation: 'compact',
compactDisplay: 'short'
}).format(value);
}
};
return (
<div className="p-4 border rounded-lg">
<div className="text-sm text-gray-600">{label}</div>
<div className="text-2xl font-bold mt-1">{formatValue()}</div>
{change !== undefined && (
<div className={`text-sm mt-1 ${change >= 0 ? 'text-green-600' : 'text-red-600'}`}>
<PercentageDisplay value={change} showSign />
<span className="ml-1">vs last period</span>
</div>
)}
</div>
);
}8.2 价格表
tsx
// PricingTable.tsx
interface PricingTier {
name: string;
price: number;
currency: string;
features: string[];
}
export function PricingTable({
tiers,
locale
}: {
tiers: PricingTier[];
locale: string;
}) {
return (
<div className="grid grid-cols-3 gap-4">
{tiers.map(tier => (
<div key={tier.name} className="p-6 border rounded-lg">
<h3 className="text-xl font-bold">{tier.name}</h3>
<div className="my-4">
<CurrencyDisplay
amount={tier.price}
currency={tier.currency}
locale={locale}
/>
<span className="text-gray-600">/月</span>
</div>
<ul className="space-y-2">
{tier.features.map((feature, i) => (
<li key={i}>{feature}</li>
))}
</ul>
</div>
))}
</div>
);
}9. 格式化工具类
typescript
// formatters.ts
export class NumberFormatters {
private locale: string;
constructor(locale: string = 'zh-CN') {
this.locale = locale;
}
currency(amount: number, currency: string = 'CNY'): string {
return new Intl.NumberFormat(this.locale, {
style: 'currency',
currency
}).format(amount);
}
percent(value: number, decimals: number = 2): string {
return new Intl.NumberFormat(this.locale, {
style: 'percent',
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(value);
}
compact(value: number): string {
return new Intl.NumberFormat(this.locale, {
notation: 'compact',
compactDisplay: 'short'
}).format(value);
}
decimal(value: number, decimals: number = 2): string {
return new Intl.NumberFormat(this.locale, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(value);
}
fileSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
const size = bytes / Math.pow(1024, unitIndex);
return `${this.decimal(size, 2)} ${units[unitIndex]}`;
}
}
// 使用
const formatter = new NumberFormatters('zh-CN');
formatter.currency(99.99, 'CNY'); // "¥99.99"
formatter.percent(0.157); // "15.70%"
formatter.compact(1234567); // "123万"
formatter.fileSize(1048576); // "1.00 MB"10. 最佳实践
typescript
const bestPractices = {
localization: [
'使用Intl API进行格式化',
'尊重用户的语言环境',
'考虑不同地区的数字格式',
'正确处理货币符号位置'
],
precision: [
'货币通常保留2位小数',
'百分比根据需求调整精度',
'大数字使用紧凑格式',
'科学数据使用有效数字'
],
performance: [
'缓存格式化器实例',
'使用useMemo避免重复格式化',
'考虑服务端格式化',
'预处理大量数据'
],
accessibility: [
'提供完整的数字信息',
'使用语义化标记',
'考虑屏幕阅读器',
'提供上下文说明'
]
};11. 总结
数字与货币格式化的关键要点:
- Intl API: 使用标准的国际化API
- 地区差异: 尊重不同地区的格式习惯
- 货币处理: 正确显示货币符号和精度
- 大数字: 使用紧凑格式提高可读性
- 单位系统: 支持度量单位和文件大小
- 性能优化: 缓存和memo化格式化结果
通过正确的数字和货币格式化,可以为全球用户提供本地化的数据展示。