Skip to content

数字与货币格式化 - 国际化数字处理完整指南

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. 总结

数字与货币格式化的关键要点:

  1. Intl API: 使用标准的国际化API
  2. 地区差异: 尊重不同地区的格式习惯
  3. 货币处理: 正确显示货币符号和精度
  4. 大数字: 使用紧凑格式提高可读性
  5. 单位系统: 支持度量单位和文件大小
  6. 性能优化: 缓存和memo化格式化结果

通过正确的数字和货币格式化,可以为全球用户提供本地化的数据展示。