fix(common): add locale currency values (#21783)

we now use locale currency symbols, since they may be different in each locale (we were only using english data previously)

Fixes #20385

PR Close #21783
This commit is contained in:
Olivier Combe
2018-01-26 11:06:13 +01:00
committed by Miško Hevery
parent 5fc77c90cb
commit 420cc7afc6
11 changed files with 208 additions and 175 deletions

View File

@ -9,8 +9,10 @@
// THIS CODE IS GENERATED - DO NOT MODIFY
// See angular/tools/gulp-tasks/cldr/extract.js
export type CurrenciesSymbols = [string] | [string | undefined, string];
/** @internal */
export const CURRENCIES: {[code: string]: (string | undefined)[]} = {
export const CURRENCIES_EN: {[code: string]: CurrenciesSymbols} = {
'AOA': [, 'Kz'],
'ARS': [, '$'],
'AUD': ['A$', '$'],
@ -111,5 +113,5 @@ export const CURRENCIES: {[code: string]: (string | undefined)[]} = {
'XOF': ['CFA'],
'XPF': ['CFPF'],
'ZAR': [, 'R'],
'ZMW': [, 'ZK'],
'ZMW': [, 'ZK']
};

View File

@ -18,33 +18,28 @@ const DIGIT_CHAR = '#';
const CURRENCY_CHAR = '¤';
const PERCENT_CHAR = '%';
/** @internal */
export type FormatNumberRes = {
str: string | null,
error?: string
};
/**
* Transform a number to a locale string based on a style and a format
*
* @internal
* Transforms a string into a number (if needed)
*/
export function formatNumber(
value: number | string, locale: string, style: NumberFormatStyle, digitsInfo?: string | null,
currency: string | null = null): FormatNumberRes {
const res: FormatNumberRes = {str: null};
const format = getLocaleNumberFormat(locale, style);
let num;
function strToNumber(value: number | string): number {
// Convert strings to numbers
if (typeof value === 'string' && !isNaN(+value - parseFloat(value))) {
num = +value;
} else if (typeof value !== 'number') {
res.error = `${value} is not a number`;
return res;
} else {
num = value;
return +value;
}
if (typeof value !== 'number') {
throw new Error(`${value} is not a number`);
}
return value;
}
/**
* Transforms a number to a locale string based on a style and a format
*/
function formatNumber(
value: number | string, locale: string, style: NumberFormatStyle, groupSymbol: NumberSymbol,
decimalSymbol: NumberSymbol, digitsInfo?: string): string {
const format = getLocaleNumberFormat(locale, style);
const num = strToNumber(value);
const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
let formattedText = '';
@ -66,8 +61,7 @@ export function formatNumber(
if (digitsInfo) {
const parts = digitsInfo.match(NUMBER_FORMAT_REGEXP);
if (parts === null) {
res.error = `${digitsInfo} is not a valid digit info`;
return res;
throw new Error(`${digitsInfo} is not a valid digit info`);
}
const minIntPart = parts[1];
const minFractionPart = parts[3];
@ -125,12 +119,10 @@ export function formatNumber(
groups.unshift(digits.join(''));
}
const groupSymbol = currency ? NumberSymbol.CurrencyGroup : NumberSymbol.Group;
formattedText = groups.join(getLocaleNumberSymbol(locale, groupSymbol));
// append the decimal digits
if (decimals.length) {
const decimalSymbol = currency ? NumberSymbol.CurrencyDecimal : NumberSymbol.Decimal;
formattedText += getLocaleNumberSymbol(locale, decimalSymbol) + decimals.join('');
}
@ -145,22 +137,42 @@ export function formatNumber(
formattedText = pattern.posPre + formattedText + pattern.posSuf;
}
if (style === NumberFormatStyle.Currency && currency !== null) {
res.str = formattedText
.replace(CURRENCY_CHAR, currency)
// if we have 2 time the currency character, the second one is ignored
.replace(CURRENCY_CHAR, '');
return res;
}
return formattedText;
}
if (style === NumberFormatStyle.Percent) {
res.str = formattedText.replace(
new RegExp(PERCENT_CHAR, 'g'), getLocaleNumberSymbol(locale, NumberSymbol.PercentSign));
return res;
}
/**
* Formats a currency to a locale string
*/
export function formatCurrency(
value: number | string, locale: string, currency: string, currencyCode?: string,
digitsInfo?: string): string {
const res = formatNumber(
value, locale, NumberFormatStyle.Currency, NumberSymbol.CurrencyGroup,
NumberSymbol.CurrencyDecimal, digitsInfo);
return res
.replace(CURRENCY_CHAR, currency)
// if we have 2 time the currency character, the second one is ignored
.replace(CURRENCY_CHAR, '');
}
res.str = formattedText;
return res;
/**
* Formats a percentage to a locale string
*/
export function formatPercent(value: number | string, locale: string, digitsInfo?: string): string {
const res = formatNumber(
value, locale, NumberFormatStyle.Percent, NumberSymbol.Group, NumberSymbol.Decimal,
digitsInfo);
return res.replace(
new RegExp(PERCENT_CHAR, 'g'), getLocaleNumberSymbol(locale, NumberSymbol.PercentSign));
}
/**
* Formats a number to a locale string
*/
export function formatDecimal(value: number | string, locale: string, digitsInfo?: string): string {
return formatNumber(
value, locale, NumberFormatStyle.Decimal, NumberSymbol.Group, NumberSymbol.Decimal,
digitsInfo);
}
interface ParsedNumberFormat {

View File

@ -54,6 +54,7 @@ export const enum LocaleDataIndex {
NumberFormats,
CurrencySymbol,
CurrencyName,
Currencies,
PluralCase,
ExtraData
}
@ -66,3 +67,8 @@ export const enum ExtraLocaleDataIndex {
ExtraDayPeriodStandalone,
ExtraDayPeriodsRules
}
/**
* Index of each value in currency data (used to describe CURRENCIES_EN in currencies.ts)
*/
export const enum CurrencyIndex {Symbol = 0, SymbolNarrow}

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CURRENCIES} from './currencies';
import localeEn from './locale_en';
import {LOCALE_DATA, LocaleDataIndex, ExtraLocaleDataIndex} from './locale_data';
import {LOCALE_DATA, LocaleDataIndex, ExtraLocaleDataIndex, CurrencyIndex} from './locale_data';
import {CURRENCIES_EN, CurrenciesSymbols} from './currencies';
/**
* The different format styles that can be used to represent numbers.
@ -391,6 +391,14 @@ export function getLocaleCurrencyName(locale: string): string|null {
return data[LocaleDataIndex.CurrencyName] || null;
}
/**
* Returns the currency values for the locale
*/
function getLocaleCurrencies(locale: string): {[code: string]: CurrenciesSymbols} {
const data = findLocaleData(locale);
return data[LocaleDataIndex.Currencies];
}
/**
* The locale plural function used by ICU expressions to determine the plural case to use.
* See {@link NgPlural} for more information.
@ -526,18 +534,19 @@ export function findLocaleData(locale: string): any {
}
/**
* Return the currency symbol for a given currency code, or the code if no symbol available
* Returns the currency symbol for a given currency code, or the code if no symbol available
* (e.g.: format narrow = $, format wide = US$, code = USD)
* If no locale is provided, it uses the locale "en" by default
*
* @experimental i18n support is experimental.
*/
export function getCurrencySymbol(code: string, format: 'wide' | 'narrow'): string {
const currency = CURRENCIES[code] || [];
const symbolNarrow = currency[1];
export function getCurrencySymbol(code: string, format: 'wide' | 'narrow', locale = 'en'): string {
const currency = getLocaleCurrencies(locale)[code] || CURRENCIES_EN[code] || [];
const symbolNarrow = currency[CurrencyIndex.SymbolNarrow];
if (format === 'narrow' && typeof symbolNarrow === 'string') {
return symbolNarrow;
}
return currency[0] || code;
return currency[CurrencyIndex.Symbol] || code;
}

View File

@ -48,5 +48,5 @@ export default [
'{1} \'at\' {0}',
],
['.', ',', ';', '%', '+', '-', 'E', '×', '‰', '∞', 'NaN', ':'],
['#,##0.###', '#,##0%', '¤#,##0.00', '#E0'], '$', 'US Dollar', plural
['#,##0.###', '#,##0%', '¤#,##0.00', '#E0'], '$', 'US Dollar', {}, plural
];

View File

@ -7,8 +7,8 @@
*/
import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
import {formatNumber} from '../i18n/format_number';
import {NumberFormatStyle, getCurrencySymbol, getLocaleCurrencyName, getLocaleCurrencySymbol} from '../i18n/locale_data_api';
import {formatCurrency, formatDecimal, formatPercent} from '../i18n/format_number';
import {getCurrencySymbol} from '../i18n/locale_data_api';
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
/**
@ -41,18 +41,16 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
export class DecimalPipe implements PipeTransform {
constructor(@Inject(LOCALE_ID) private _locale: string) {}
transform(value: any, digits?: string, locale?: string): string|null {
transform(value: any, digitsInfo?: string, locale?: string): string|null {
if (isEmpty(value)) return null;
locale = locale || this._locale;
const {str, error} = formatNumber(value, locale, NumberFormatStyle.Decimal, digits);
if (error) {
throw invalidPipeArgumentError(DecimalPipe, error);
try {
return formatDecimal(value, locale, digitsInfo);
} catch (error) {
throw invalidPipeArgumentError(DecimalPipe, error.message);
}
return str;
}
}
@ -65,7 +63,7 @@ export class DecimalPipe implements PipeTransform {
*
* Formats a number as percentage.
*
* - `digitInfo` See {@link DecimalPipe} for detailed description.
* - `digitInfo` See {@link DecimalPipe} for a detailed description.
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default)
*
@ -79,18 +77,16 @@ export class DecimalPipe implements PipeTransform {
export class PercentPipe implements PipeTransform {
constructor(@Inject(LOCALE_ID) private _locale: string) {}
transform(value: any, digits?: string, locale?: string): string|null {
transform(value: any, digitsInfo?: string, locale?: string): string|null {
if (isEmpty(value)) return null;
locale = locale || this._locale;
const {str, error} = formatNumber(value, locale, NumberFormatStyle.Percent, digits);
if (error) {
throw invalidPipeArgumentError(PercentPipe, error);
try {
return formatPercent(value, locale, digitsInfo);
} catch (error) {
throw invalidPipeArgumentError(PercentPipe, error.message);
}
return str;
}
}
@ -104,14 +100,15 @@ export class PercentPipe implements PipeTransform {
*
* - `currencyCode` is the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code, such
* as `USD` for the US dollar and `EUR` for the euro.
* - `display` indicates whether to show the currency symbol or the code.
* - `display` indicates whether to show the currency symbol, the code or a custom value
* - `code`: use code (e.g. `USD`).
* - `symbol`(default): use symbol (e.g. `$`).
* - `symbol-narrow`: some countries have two symbols for their currency, one regular and one
* narrow (e.g. the canadian dollar CAD has the symbol `CA$` and the symbol-narrow `$`).
* - `string`: use this value instead of a code or a symbol
* - boolean (deprecated from v5): `true` for symbol and false for `code`
* If there is no narrow symbol for the chosen currency, the regular symbol will be used.
* - `digitInfo` See {@link DecimalPipe} for detailed description.
* - `digitInfo` See {@link DecimalPipe} for a detailed description.
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default)
*
@ -127,7 +124,7 @@ export class CurrencyPipe implements PipeTransform {
transform(
value: any, currencyCode?: string,
display: 'code'|'symbol'|'symbol-narrow'|boolean = 'symbol', digits?: string,
display: 'code'|'symbol'|'symbol-narrow'|string|boolean = 'symbol', digitsInfo?: string,
locale?: string): string|null {
if (isEmpty(value)) return null;
@ -141,18 +138,20 @@ export class CurrencyPipe implements PipeTransform {
display = display ? 'symbol' : 'code';
}
let currency = currencyCode || 'USD';
let currency: string = currencyCode || 'USD';
if (display !== 'code') {
currency = getCurrencySymbol(currency, display === 'symbol' ? 'wide' : 'narrow');
if (display === 'symbol' || display === 'symbol-narrow') {
currency = getCurrencySymbol(currency, display === 'symbol' ? 'wide' : 'narrow', locale);
} else {
currency = display;
}
}
const {str, error} = formatNumber(value, locale, NumberFormatStyle.Currency, digits, currency);
if (error) {
throw invalidPipeArgumentError(CurrencyPipe, error);
try {
return formatCurrency(value, locale, currency, currencyCode, digitsInfo);
} catch (error) {
throw invalidPipeArgumentError(CurrencyPipe, error.message);
}
return str;
}
}

View File

@ -11,6 +11,7 @@ import localeEn from '@angular/common/locales/en';
import localeFr from '@angular/common/locales/fr';
import localeZh from '@angular/common/locales/zh';
import localeFrCA from '@angular/common/locales/fr-CA';
import localeEnAU from '@angular/common/locales/en-AU';
import {registerLocaleData} from '../../src/i18n/locale_data';
import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth} from '../../src/i18n/locale_data_api';
@ -24,6 +25,7 @@ import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth} fro
registerLocaleData(localeFr, 'fake-id');
registerLocaleData(localeFrCA, 'fake_Id2');
registerLocaleData(localeZh);
registerLocaleData(localeEnAU);
});
describe('findLocaleData', () => {
@ -54,7 +56,7 @@ import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth} fro
});
});
describe('getCurrencySymbolElseCode', () => {
describe('getting currency symbol', () => {
it('should return the correct symbol', () => {
expect(getCurrencySymbol('USD', 'wide')).toEqual('$');
expect(getCurrencySymbol('USD', 'narrow')).toEqual('$');
@ -62,8 +64,13 @@ import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth} fro
expect(getCurrencySymbol('AUD', 'narrow')).toEqual('$');
expect(getCurrencySymbol('CRC', 'wide')).toEqual('CRC');
expect(getCurrencySymbol('CRC', 'narrow')).toEqual('₡');
expect(getCurrencySymbol('FAKE', 'wide')).toEqual('FAKE');
expect(getCurrencySymbol('FAKE', 'narrow')).toEqual('FAKE');
expect(getCurrencySymbol('unexisting_ISO_code', 'wide')).toEqual('unexisting_ISO_code');
expect(getCurrencySymbol('unexisting_ISO_code', 'narrow')).toEqual('unexisting_ISO_code');
expect(getCurrencySymbol('USD', 'wide', 'en-AU')).toEqual('USD');
expect(getCurrencySymbol('USD', 'narrow', 'en-AU')).toEqual('$');
expect(getCurrencySymbol('AUD', 'wide', 'en-AU')).toEqual('$');
expect(getCurrencySymbol('AUD', 'narrow', 'en-AU')).toEqual('$');
expect(getCurrencySymbol('USD', 'wide', 'fr')).toEqual('$US');
});
});

View File

@ -125,7 +125,15 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
expect(pipe.transform(5.1234, 'CAD', 'symbol-narrow', '5.2-2')).toEqual('$00,005.12');
expect(pipe.transform(5.1234, 'CAD', 'symbol-narrow', '5.2-2', 'fr'))
.toEqual('00 005,12 $');
expect(pipe.transform(5.1234, 'FAKE', 'symbol')).toEqual('FAKE5.12');
expect(pipe.transform(5, 'USD', 'symbol', '', 'fr')).toEqual('5,00 $US');
});
it('should support any currency code name', () => {
// currency code is unknown, default formatting options will be used
expect(pipe.transform(5.1234, 'unexisting_ISO_code', 'symbol'))
.toEqual('unexisting_ISO_code5.12');
// currency code is USD, the pipe will format based on USD but will display "Custom name"
expect(pipe.transform(5.1234, 'USD', 'Custom name')).toEqual('Custom name5.12');
});
it('should not support other objects', () => {