From fe7d604b54c7754d2200b7ae7055ae739945e2ae Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Thu, 20 Jul 2017 11:01:33 +0200 Subject: [PATCH] feat(common): drop use of the Intl API to improve browser support BREAKING CHANGE: Because of multiple bugs and browser inconsistencies, we have dropped the intl api in favor of data exported from the Unicode Common Locale Data Repository (CLDR). Unfortunately we had to change the i18n pipes (date, number, currency, percent) and there are some breaking changes. ## I18n pipes ### Breaking change By default Angular now only contains locale data for the language `en-US`, if you set the value of `LOCALE_ID` to another locale, you will have to import new locale data for this language because we don't use the intl API anymore. ### Features - angular doesn't need the intl polyfill anymore - all i18n pipes now have an additional last parameter `locale` which allows you to use a specific locale instead of the one defined in the token `LOCALE_ID` (whose value is `en-US` by default). - the new locale data extracted from CLDR are now available to developers as well and can be used through an API (which should be especially useful for library authors). - you can still use the old pipes for now, but their names have been changed and they are no longer included in the `CommonModule`. To use them, you will have to import the `DeprecatedI18NPipesModule` after the `CommonModule` (the order is important): ```ts import { NgModule } from '@angular/core'; import { CommonModule, DeprecatedI18NPipesModule } from '@angular/common'; @NgModule({ imports: [ CommonModule, // import deprecated module after DeprecatedI18NPipesModule ] }) export class AppModule { } ``` Dont forget that you will still need to import the intl API polyfill if you want to use those deprecated pipes. ## Date pipe ### Breaking changes - predefined formats `short`, and `shortDate` now use a short year, e.g. `8/15/17` instead of `8/15/2017`. - the narrow version of eras is now `GGGGG` instead of `G`, the format `G` is now similar to `GG` and `GGG`. - the narrow version of months is now `MMMMM` instead of `L`, the format `L` is now the short standalone version of months. - the narrow version of the week day is now `EEEEE` instead of `E`, the format `E` is now similar to `EE` and `EEE`. - the timezone `z` will now fallback to `O` and output `GMT+1` instead of the complete zone name (e.g. `Pacific Standard Time`), this is because the quantity of data required to have all the zone names in all of the existing locales is too big. - the timezone `Z` will now output the ISO8601 basic format, e.g. `+0100`, you should now use `ZZZZ` to get `GMT+01:00`. | Field type | Format | Example value | v4 | v5 | |------------|---------------|-----------------------|----|---------------| | Eras | Narrow | A for AD | G | GGGGG | | Months | Narrow | S for September | L | MMMMM | | Week day | Narrow | M for Monday | E | EEEEE | | Timezone | Long location | Pacific Standard Time | z | Not available | | Timezone | Long GMT | GMT+01:00 | Z | ZZZZ | ### Features - new predefined formats `long`, `full`, `longTime`, `fullTime`. - the format `yyy` is now supported, e.g. the year `52` will be `052` and the year `2017` will be `2017`. - standalone months are now supported with the formats `L` to `LLLLL`. - week of the year is now supported with the formats `w` and `ww`, e.g. weeks `5` and `05`. - week of the month is now supported with the format `W`, e.g. week `3`. - fractional seconds are now supported with the format `S` to `SSS`. - day periods for AM/PM now supports additional formats `aa`, `aaa`, `aaaa` and `aaaaa`. The formats `a` to `aaa` are similar, while `aaaa` is the wide version if available (e.g. `ante meridiem` for `am`), or equivalent to `a` otherwise, and `aaaaa` is the narrow version (e.g. `a` for `am`). - extra day periods are now supported with the formats `b` to `bbbbb` (and `B` to `BBBBB` for the standalone equivalents), e.g. `morning`, `noon`, `afternoon`, .... - the short non-localized timezones are now available with the format `O` to `OOOO`. The formats `O` to `OOO` will output `GMT+1` while the format `OOOO` will be `GMT+01:00`. - the ISO8601 basic time zones are now available with the formats `Z` to `ZZZZZ`. The formats `Z` to `ZZZ` will output `+0100`, while the format `ZZZZ` will be `GMT+01:00` and `ZZZZZ` will be `+01:00`. ### Bug fixes - the date pipe will now work exactly the same across all browsers, which will fix a lot of bugs for safari and IE. - eras can now be used on their own without the date, e.g. the format `GG` will be `AD` instead of `8 15, 2017 AD`. ## Currency pipe ### Breaking changes - the second parameter of the currency pipe (`symbolDisplay`) is no longer a boolean, it now takes the values `code`, `symbol` or `symbol-narrow`. - the default value for `symbolDisplay` is now `symbol` instead of `code`. This means that by default you will see `$4.99` for `en-US` instead of `USD4.99` previously. ### Features - you can now choose between `code`, `symbol` or `symbol-narrow` which gives you access to more options for some currencies (e.g. the canadian dollar with the code `CAD` has the symbol `CA$` and the symbol-narrow `$`). ## Percent pipe ### Breaking change - if you don't specify the number of digits to round to, the local format will be used (and it usually rounds numbers to 0 digits, instead of not rounding previously), e.g. `{{ 3.141592 | percent }}` will output `314%` for the locale `en-US` instead of `314.1592%` previously. ----- Fixes #10809, #9524, #7008, #9324, #7590, #6724, #3429, #17576, #17478, #17319, #17200, #16838, #16624, #16625, #16591, #14131, #12632, #11376, #11187 --- .../examples/i18n/src/app/app.locale_data.ts | 6 + .../i18n/src/app/app.locale_data_extra.ts | 7 + aio/content/examples/pipes/e2e-spec.ts | 2 +- aio/content/guide/browser-support.md | 2 +- aio/content/guide/i18n.md | 22 + aio/content/guide/pipes.md | 18 - build.sh | 7 +- packages/common/BUILD.bazel | 1 + packages/common/i18n_data/tsconfig-build.json | 20 + packages/common/src/common.ts | 7 +- packages/common/src/common_module.ts | 16 +- packages/common/src/directives/ng_plural.ts | 2 +- packages/common/src/i18n/format_date.ts | 602 ++++++++++++++++++ packages/common/src/i18n/format_number.ts | 377 +++++++++++ packages/common/src/i18n/locale_data.ts | 22 + packages/common/src/i18n/locale_data_api.ts | 594 +++++++++++++++++ packages/common/src/i18n/localization.ts | 76 +++ packages/common/src/localization.ts | 401 ------------ packages/common/src/pipes/date_pipe.ts | 177 ++--- .../common/src/pipes/deprecated/date_pipe.ts | 145 +++++ packages/common/src/pipes/deprecated/index.ts | 27 + .../common/src/pipes/{ => deprecated}/intl.ts | 7 +- .../src/pipes/deprecated/number_pipe.ts | 164 +++++ packages/common/src/pipes/i18n_plural_pipe.ts | 14 +- packages/common/src/pipes/number_pipe.ts | 153 ++--- .../common/test/i18n/locale_data_api_spec.ts | 51 ++ .../test/{ => i18n}/localization_spec.ts | 14 +- packages/common/test/pipes/date_pipe_spec.ts | 296 ++++++--- .../test/pipes/deprecated/date_pipe_spec.ts | 208 ++++++ .../test/pipes/deprecated/number_pipe_spec.ts | 109 ++++ .../common/test/pipes/number_pipe_spec.ts | 68 +- .../examples/common/pipes/ts/date_pipe.ts | 3 +- .../examples/common/pipes/ts/number_pipe.ts | 5 +- packages/tsconfig.json | 3 +- tools/gulp-tasks/cldr/extract.js | 18 + tools/public_api_guard/common/common.d.ts | 184 +++++- 36 files changed, 3091 insertions(+), 737 deletions(-) create mode 100644 aio/content/examples/i18n/src/app/app.locale_data.ts create mode 100644 aio/content/examples/i18n/src/app/app.locale_data_extra.ts create mode 100644 packages/common/i18n_data/tsconfig-build.json create mode 100644 packages/common/src/i18n/format_date.ts create mode 100644 packages/common/src/i18n/format_number.ts create mode 100644 packages/common/src/i18n/locale_data.ts create mode 100644 packages/common/src/i18n/locale_data_api.ts create mode 100644 packages/common/src/i18n/localization.ts delete mode 100644 packages/common/src/localization.ts create mode 100644 packages/common/src/pipes/deprecated/date_pipe.ts create mode 100644 packages/common/src/pipes/deprecated/index.ts rename packages/common/src/pipes/{ => deprecated}/intl.ts (99%) create mode 100644 packages/common/src/pipes/deprecated/number_pipe.ts create mode 100644 packages/common/test/i18n/locale_data_api_spec.ts rename packages/common/test/{ => i18n}/localization_spec.ts (93%) create mode 100644 packages/common/test/pipes/deprecated/date_pipe_spec.ts create mode 100644 packages/common/test/pipes/deprecated/number_pipe_spec.ts diff --git a/aio/content/examples/i18n/src/app/app.locale_data.ts b/aio/content/examples/i18n/src/app/app.locale_data.ts new file mode 100644 index 00000000000000..9129a68200eee2 --- /dev/null +++ b/aio/content/examples/i18n/src/app/app.locale_data.ts @@ -0,0 +1,6 @@ +// #docregion import-locale +import { registerLocaleData } from '@angular/common'; +import localeFr from '@angular/common/i18n_data/locale_fr'; + +registerLocaleData(localeFr); +// #enddocregion import-locale diff --git a/aio/content/examples/i18n/src/app/app.locale_data_extra.ts b/aio/content/examples/i18n/src/app/app.locale_data_extra.ts new file mode 100644 index 00000000000000..312c73feecb055 --- /dev/null +++ b/aio/content/examples/i18n/src/app/app.locale_data_extra.ts @@ -0,0 +1,7 @@ +// #docregion import-locale-extra +import { registerLocaleData } from '@angular/common'; +import localeEnGB from '@angular/common/i18n_data/locale_en-GB'; +import localeEnGBExtra from '@angular/common/i18n_data/extra/locale_en-GB'; + +registerLocaleData(localeEnGB, localeEnGBExtra); +// #enddocregion import-locale-extra diff --git a/aio/content/examples/pipes/e2e-spec.ts b/aio/content/examples/pipes/e2e-spec.ts index 0aa5f09a5785a2..9675b66367cc2f 100644 --- a/aio/content/examples/pipes/e2e-spec.ts +++ b/aio/content/examples/pipes/e2e-spec.ts @@ -28,7 +28,7 @@ describe('Pipes', function () { it('should be able to toggle birthday formats', function () { let birthDayEle = element(by.css('hero-birthday2 > p')); - expect(birthDayEle.getText()).toEqual(`The hero's birthday is 4/15/1988`); + expect(birthDayEle.getText()).toEqual(`The hero's birthday is 4/15/88`); let buttonEle = element(by.cssContainingText('hero-birthday2 > button', 'Toggle Format')); expect(buttonEle.isDisplayed()).toBe(true); buttonEle.click().then(function() { diff --git a/aio/content/guide/browser-support.md b/aio/content/guide/browser-support.md index e5e20cfaff00f0..8d796ff576d98b 100644 --- a/aio/content/guide/browser-support.md +++ b/aio/content/guide/browser-support.md @@ -347,7 +347,7 @@ Here are the features which may require additional polyfills: - [Date](api/common/DatePipe), [currency](api/common/CurrencyPipe), [decimal](api/common/DecimalPipe) and [percent](api/common/PercentPipe) pipes + If you use the following deprecated i18n pipes: [date](api/common/DeprecatedDatePipe), [currency](api/common/DeprecatedCurrencyPipe), [decimal](api/common/DeprecatedDecimalPipe) and [percent](api/common/DeprecatedPercentPipe) diff --git a/aio/content/guide/i18n.md b/aio/content/guide/i18n.md index 19da44d0053e47..810bae6bdccfeb 100644 --- a/aio/content/guide/i18n.md +++ b/aio/content/guide/i18n.md @@ -40,6 +40,28 @@ You need to build and deploy a separate version of the application for each supp {@a i18n-attribute} +## i18n pipes + +Angular pipes can help you with internationalization: the `DatePipe`, `CurrencyPipe`, `DecimalPipe` +and `PercentPipe` use locale data to format your data based on your `LOCALE_ID`. + +By default Angular only contains locale data for the language `en-US`, if you set the value of +`LOCALE_ID` to another locale, you will have to import new locale data for this language: + + + + +
+ +Note that the files in `@angular/common/i18n_data` contain most of the locale data that you will +need, but some advanced formatting options might only be available in the extra dataset that you can +import from `@angular/common/i18n_data/extra`: + + + + +
+ ## Mark text with the _i18n_ attribute The Angular `i18n` attribute is a marker for translatable content. diff --git a/aio/content/guide/pipes.md b/aio/content/guide/pipes.md index 4c2a3db8ef2c6b..0445b942f0f7f7 100644 --- a/aio/content/guide/pipes.md +++ b/aio/content/guide/pipes.md @@ -46,24 +46,6 @@ Inside the interpolation expression, you flow the component's `birthday` value t function on the right. All pipes work this way. -
- - - -The `Date` and `Currency` pipes need the *ECMAScript Internationalization API*. -Safari and other older browsers don't support it. You can add support with a polyfill. - - - - <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script> - - - - - -
- - ## Built-in pipes diff --git a/build.sh b/build.sh index 71b476bb8d2bfb..293bcf729ee4a8 100755 --- a/build.sh +++ b/build.sh @@ -86,7 +86,7 @@ done ####################################### isIgnoredDirectory() { name=$(basename ${1}) - if [[ -f "${1}" || "${name}" == "src" || "${name}" == "test" || "${name}" == "integrationtest" ]]; then + if [[ -f "${1}" || "${name}" == "src" || "${name}" == "test" || "${name}" == "integrationtest" || "${name}" == "i18n_data" ]]; then return 0 else return 1 @@ -470,6 +470,11 @@ do minify ${BUNDLES_DIR} ) 2>&1 | grep -v "as external dependency" + + if [[ ${PACKAGE} == "common" ]]; then + echo "====== Copy i18n locale data" + rsync -a --exclude=*.d.ts --exclude=*.metadata.json ${OUT_DIR}/i18n_data/ ${NPM_DIR}/i18n_data + fi else echo "====== Copy ${PACKAGE} node tool" rsync -a ${OUT_DIR}/ ${NPM_DIR} diff --git a/packages/common/BUILD.bazel b/packages/common/BUILD.bazel index cee67edccd871b..f1dcaea6c68717 100644 --- a/packages/common/BUILD.bazel +++ b/packages/common/BUILD.bazel @@ -5,6 +5,7 @@ ts_library( name = "common", srcs = glob(["**/*.ts"], exclude=[ "http/**", + "i18n/**", "test/**", "testing/**", ]), diff --git a/packages/common/i18n_data/tsconfig-build.json b/packages/common/i18n_data/tsconfig-build.json new file mode 100644 index 00000000000000..273fbb9b6e5583 --- /dev/null +++ b/packages/common/i18n_data/tsconfig-build.json @@ -0,0 +1,20 @@ +{ +"compilerOptions": { + "baseUrl": ".", + "declaration": true, + "stripInternal": true, + "experimentalDecorators": true, + "module": "es2015", + "moduleResolution": "node", + "outDir": "../../../dist/packages/common/i18n_data", + "paths": { + "@angular/common": ["../../../dist/packages/common"], + "@angular/core": ["../../../dist/packages/core"] + }, + "sourceMap": true, + "inlineSources": true, + "target": "es5", + "skipLibCheck": true, + "lib": ["es2015", "dom"] + } +} diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts index 5e79a49ad185be..d6f7158680b9f7 100644 --- a/packages/common/src/common.ts +++ b/packages/common/src/common.ts @@ -12,11 +12,16 @@ * Entry point for all public APIs of the common package. */ export * from './location/index'; -export {NgLocaleLocalization, NgLocalization} from './localization'; +export {NgLocaleLocalization, NgLocalization} from './i18n/localization'; +export {Plural, LOCALE_DATA} from './i18n/locale_data'; +export {findLocaleData, registerLocaleData, NumberFormatStyle, FormStyle, Time, TranslationWidth, FormatWidth, NumberSymbol, WeekDay, getLocaleDayPeriods, getLocaleDayNames, getLocaleMonthNames, getLocaleId, getLocaleEraNames, getLocaleWeekEndRange, getLocaleFirstDayOfWeek, getLocaleDateFormat, getLocaleDateTimeFormat, getLocaleExtraDayPeriodRules, getLocaleExtraDayPeriods, getLocalePluralCase, getLocaleTimeFormat, getLocaleNumberSymbol, getLocaleNumberFormat, getLocaleCurrencyName, getLocaleCurrencySymbol} from './i18n/locale_data_api'; +export {AVAILABLE_LOCALES} from './i18n/available_locales'; +export {CURRENCIES} from './i18n/currencies'; export {parseCookieValue as ɵparseCookieValue} from './cookie'; export {CommonModule, DeprecatedI18NPipesModule} from './common_module'; export {NgClass, NgFor, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index'; export {DOCUMENT} from './dom_tokens'; export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe} from './pipes/index'; +export {DeprecatedDatePipe, DeprecatedCurrencyPipe, DeprecatedDecimalPipe, DeprecatedPercentPipe} from './pipes/deprecated/index'; export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPLATFORM_SERVER_ID, PLATFORM_WORKER_APP_ID as ɵPLATFORM_WORKER_APP_ID, PLATFORM_WORKER_UI_ID as ɵPLATFORM_WORKER_UI_ID, isPlatformBrowser, isPlatformServer, isPlatformWorkerApp, isPlatformWorkerUi} from './platform_id'; export {VERSION} from './version'; diff --git a/packages/common/src/common_module.ts b/packages/common/src/common_module.ts index 4a75bb4f3911bb..dd7750f86fa68c 100644 --- a/packages/common/src/common_module.ts +++ b/packages/common/src/common_module.ts @@ -8,8 +8,9 @@ import {NgModule} from '@angular/core'; -import {COMMON_DEPRECATED_DIRECTIVES, COMMON_DIRECTIVES} from './directives/index'; -import {NgLocaleLocalization, NgLocalization} from './localization'; +import {COMMON_DIRECTIVES} from './directives/index'; +import {NgLocaleLocalization, NgLocalization} from './i18n/localization'; +import {COMMON_DEPRECATED_I18N_PIPES} from './pipes/deprecated/index'; import {COMMON_PIPES} from './pipes/index'; @@ -31,17 +32,10 @@ export class CommonModule { } /** - * I18N pipes are being changed to move away from using the JS Intl API. - * - * The former pipes relying on the Intl API will be moved to this module while the `CommonModule` - * will contain the new pipes that do not rely on Intl. - * - * As a first step this module is created empty to ease the migration. - * - * see https://github.com/angular/angular/pull/18284 + * A module that contains the deprecated i18n pipes. * * @deprecated from v5 */ -@NgModule({declarations: [], exports: []}) +@NgModule({declarations: [COMMON_DEPRECATED_I18N_PIPES], exports: [COMMON_DEPRECATED_I18N_PIPES]}) export class DeprecatedI18NPipesModule { } diff --git a/packages/common/src/directives/ng_plural.ts b/packages/common/src/directives/ng_plural.ts index e014a5e9d22f81..f82479aa5f4b0c 100644 --- a/packages/common/src/directives/ng_plural.ts +++ b/packages/common/src/directives/ng_plural.ts @@ -8,7 +8,7 @@ import {Attribute, Directive, Host, Input, TemplateRef, ViewContainerRef} from '@angular/core'; -import {NgLocalization, getPluralCategory} from '../localization'; +import {NgLocalization, getPluralCategory} from '../i18n/localization'; import {SwitchView} from './ng_switch'; diff --git a/packages/common/src/i18n/format_date.ts b/packages/common/src/i18n/format_date.ts new file mode 100644 index 00000000000000..df2294a339ad83 --- /dev/null +++ b/packages/common/src/i18n/format_date.ts @@ -0,0 +1,602 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {FormStyle, FormatWidth, NumberSymbol, Time, TranslationWidth, getLocaleDateFormat, getLocaleDateTimeFormat, getLocaleDayNames, getLocaleDayPeriods, getLocaleEraNames, getLocaleExtraDayPeriodRules, getLocaleExtraDayPeriods, getLocaleId, getLocaleMonthNames, getLocaleNumberSymbol, getLocaleTimeFormat} from './locale_data_api'; + +const NAMED_FORMATS: {[localeId: string]: {[format: string]: string}} = {}; +const DATE_FORMATS_SPLIT = + /((?:[^GyMLwWdEabBhHmsSzZO']+)|(?:'(?:[^']|'')*')|(?:G{1,5}|y{1,4}|M{1,5}|L{1,5}|w{1,2}|W{1}|d{1,2}|E{1,6}|a{1,5}|b{1,5}|B{1,5}|h{1,2}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|z{1,4}|Z{1,5}|O{1,4}))([\s\S]*)/; + +enum ZoneWidth { + Short, + ShortGMT, + Long, + Extended +} + +enum DateType { + FullYear, + Month, + Date, + Hours, + Minutes, + Seconds, + Milliseconds, + Day +} + +enum TranslationType { + DayPeriods, + Days, + Months, + Eras +} + +/** + * Transforms a date to a locale string based on a pattern and a timezone + * + * @internal + */ +export function formatDate(date: Date, format: string, locale: string, timezone?: string): string { + const namedFormat = getNamedFormat(locale, format); + format = namedFormat || format; + + let parts: string[] = []; + let match; + while (format) { + match = DATE_FORMATS_SPLIT.exec(format); + if (match) { + parts = parts.concat(match.slice(1)); + const part = parts.pop(); + if (!part) { + break; + } + format = part; + } else { + parts.push(format); + break; + } + } + + let dateTimezoneOffset = date.getTimezoneOffset(); + if (timezone) { + dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + date = convertTimezoneToLocal(date, timezone, true); + } + + let text = ''; + parts.forEach(value => { + const dateFormatter = getDateFormatter(value); + text += dateFormatter ? + dateFormatter(date, locale, dateTimezoneOffset) : + value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\''); + }); + + return text; +} + +function getNamedFormat(locale: string, format: string): string { + const localeId = getLocaleId(locale); + NAMED_FORMATS[localeId] = NAMED_FORMATS[localeId] || {}; + + if (NAMED_FORMATS[localeId][format]) { + return NAMED_FORMATS[localeId][format]; + } + + let formatValue = ''; + switch (format) { + case 'shortDate': + formatValue = getLocaleDateFormat(locale, FormatWidth.Short); + break; + case 'mediumDate': + formatValue = getLocaleDateFormat(locale, FormatWidth.Medium); + break; + case 'longDate': + formatValue = getLocaleDateFormat(locale, FormatWidth.Long); + break; + case 'fullDate': + formatValue = getLocaleDateFormat(locale, FormatWidth.Full); + break; + case 'shortTime': + formatValue = getLocaleTimeFormat(locale, FormatWidth.Short); + break; + case 'mediumTime': + formatValue = getLocaleTimeFormat(locale, FormatWidth.Medium); + break; + case 'longTime': + formatValue = getLocaleTimeFormat(locale, FormatWidth.Long); + break; + case 'fullTime': + formatValue = getLocaleTimeFormat(locale, FormatWidth.Full); + break; + case 'short': + const shortTime = getNamedFormat(locale, 'shortTime'); + const shortDate = getNamedFormat(locale, 'shortDate'); + formatValue = formatDateTime( + getLocaleDateTimeFormat(locale, FormatWidth.Short), [shortTime, shortDate]); + break; + case 'medium': + const mediumTime = getNamedFormat(locale, 'mediumTime'); + const mediumDate = getNamedFormat(locale, 'mediumDate'); + formatValue = formatDateTime( + getLocaleDateTimeFormat(locale, FormatWidth.Medium), [mediumTime, mediumDate]); + break; + case 'long': + const longTime = getNamedFormat(locale, 'longTime'); + const longDate = getNamedFormat(locale, 'longDate'); + formatValue = + formatDateTime(getLocaleDateTimeFormat(locale, FormatWidth.Long), [longTime, longDate]); + break; + case 'full': + const fullTime = getNamedFormat(locale, 'fullTime'); + const fullDate = getNamedFormat(locale, 'fullDate'); + formatValue = + formatDateTime(getLocaleDateTimeFormat(locale, FormatWidth.Full), [fullTime, fullDate]); + break; + } + if (formatValue) { + NAMED_FORMATS[localeId][format] = formatValue; + } + return formatValue; +} + +function formatDateTime(str: string, opt_values: string[]) { + if (opt_values) { + str = str.replace(/\{([^}]+)}/g, function(match, key) { + return (opt_values != null && key in opt_values) ? opt_values[key] : match; + }); + } + return str; +} + +function padNumber( + num: number, digits: number, minusSign = '-', trim?: boolean, negWrap?: boolean): string { + let neg = ''; + if (num < 0 || (negWrap && num <= 0)) { + if (negWrap) { + num = -num + 1; + } else { + num = -num; + neg = minusSign; + } + } + let strNum = '' + num; + while (strNum.length < digits) strNum = '0' + strNum; + if (trim) { + strNum = strNum.substr(strNum.length - digits); + } + return neg + strNum; +} + +/** + * Returns a date formatter that transforms a date into its locale digit representation + */ +function dateGetter( + name: DateType, size: number, offset: number = 0, trim = false, + negWrap = false): DateFormatter { + return function(date: Date, locale: string): string { + let part = getDatePart(name, date, size); + if (offset > 0 || part > -offset) { + part += offset; + } + if (name === DateType.Hours && part === 0 && offset === -12) { + part = 12; + } + return padNumber( + part, size, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign), trim, negWrap); + }; +} + +function getDatePart(name: DateType, date: Date, size: number): number { + switch (name) { + case DateType.FullYear: + return date.getFullYear(); + case DateType.Month: + return date.getMonth(); + case DateType.Date: + return date.getDate(); + case DateType.Hours: + return date.getHours(); + case DateType.Minutes: + return date.getMinutes(); + case DateType.Seconds: + return date.getSeconds(); + case DateType.Milliseconds: + const div = size === 1 ? 100 : (size === 2 ? 10 : 1); + return Math.round(date.getMilliseconds() / div); + case DateType.Day: + return date.getDay(); + default: + throw new Error(`Unknown DateType value "${name}".`); + } +} + +/** + * Returns a date formatter that transforms a date into its locale string representation + */ +function dateStrGetter( + name: TranslationType, width: TranslationWidth, form: FormStyle = FormStyle.Format, + extended = false): DateFormatter { + return function(date: Date, locale: string): string { + return getDateTranslation(date, locale, name, width, form, extended); + }; +} + +/** + * Returns the locale translation of a date for a given form, type and width + */ +function getDateTranslation( + date: Date, locale: string, name: TranslationType, width: TranslationWidth, form: FormStyle, + extended: boolean) { + switch (name) { + case TranslationType.Months: + return getLocaleMonthNames(locale, form, width)[date.getMonth()]; + case TranslationType.Days: + return getLocaleDayNames(locale, form, width)[date.getDay()]; + case TranslationType.DayPeriods: + const currentHours = date.getHours(); + const currentMinutes = date.getMinutes(); + if (extended) { + const rules = getLocaleExtraDayPeriodRules(locale); + const dayPeriods = getLocaleExtraDayPeriods(locale, form, width); + let result; + rules.forEach((rule: Time | [Time, Time], index: number) => { + if (Array.isArray(rule)) { + // morning, afternoon, evening, night + const {hours: hoursFrom, minutes: minutesFrom} = rule[0]; + const {hours: hoursTo, minutes: minutesTo} = rule[1]; + if (currentHours >= hoursFrom && currentMinutes >= minutesFrom && + (currentHours < hoursTo || + (currentHours === hoursTo && currentMinutes < minutesTo))) { + result = dayPeriods[index]; + } + } else { // noon or midnight + const {hours, minutes} = rule; + if (hours === currentHours && minutes === currentMinutes) { + result = dayPeriods[index]; + } + } + }); + if (result) { + return result; + } + } + // if no rules for the day periods, we use am/pm by default + return getLocaleDayPeriods(locale, form, width)[currentHours < 12 ? 0 : 1]; + case TranslationType.Eras: + return getLocaleEraNames(locale, width)[date.getFullYear() <= 0 ? 0 : 1]; + } +} + +/** + * Returns a date formatter that transforms a date and an offset into a timezone with ISO8601 or + * GMT format depending on the width (eg: short = +0430, short:GMT = GMT+4, long = GMT+04:30, + * extended = +04:30) + */ +function timeZoneGetter(width: ZoneWidth): DateFormatter { + return function(date: Date, locale: string, offset: number) { + const zone = -1 * offset; + const minusSign = getLocaleNumberSymbol(locale, NumberSymbol.MinusSign); + const hours = zone > 0 ? Math.floor(zone / 60) : Math.ceil(zone / 60); + switch (width) { + case ZoneWidth.Short: + return ((zone >= 0) ? '+' : '') + padNumber(hours, 2, minusSign) + + padNumber(Math.abs(zone % 60), 2, minusSign); + case ZoneWidth.ShortGMT: + return 'GMT' + ((zone >= 0) ? '+' : '') + padNumber(hours, 1, minusSign); + case ZoneWidth.Long: + return 'GMT' + ((zone >= 0) ? '+' : '') + padNumber(hours, 2, minusSign) + ':' + + padNumber(Math.abs(zone % 60), 2, minusSign); + case ZoneWidth.Extended: + if (offset === 0) { + return 'Z'; + } else { + return ((zone >= 0) ? '+' : '') + padNumber(hours, 2, minusSign) + ':' + + padNumber(Math.abs(zone % 60), 2, minusSign); + } + default: + throw new Error(`Unknown zone width "${width}"`); + } + }; +} + +const JANUARY = 0; +const THURSDAY = 4; +function getFirstThursdayOfYear(year: number) { + const firstDayOfYear = (new Date(year, JANUARY, 1)).getDay(); + return new Date( + year, 0, 1 + ((firstDayOfYear <= THURSDAY) ? THURSDAY : THURSDAY + 7) - firstDayOfYear); +} + +function getThursdayThisWeek(datetime: Date) { + return new Date( + datetime.getFullYear(), datetime.getMonth(), + datetime.getDate() + (THURSDAY - datetime.getDay())); +} + +function weekGetter(size: number, monthBased = false): DateFormatter { + return function(date: Date, locale: string) { + let result; + if (monthBased) { + const nbDaysBefore1stDayOfMonth = + new Date(date.getFullYear(), date.getMonth(), 1).getDay() - 1; + const today = date.getDate(); + result = 1 + Math.floor((today + nbDaysBefore1stDayOfMonth) / 7); + } else { + const firstThurs = getFirstThursdayOfYear(date.getFullYear()); + const thisThurs = getThursdayThisWeek(date); + const diff = thisThurs.getTime() - firstThurs.getTime(); + result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week + } + + return padNumber(result, size, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign)); + }; +} + +type DateFormatter = (date: Date, locale: string, offset?: number) => string; + +const DATE_FORMATS: {[format: string]: DateFormatter} = {}; + +// Based on CLDR formats: +// See complete list: http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table +// See also explanations: http://cldr.unicode.org/translation/date-time +// TODO(ocombe): support all missing cldr formats: Y, U, Q, D, F, e, c, j, J, C, A, v, V, X, x +function getDateFormatter(format: string): DateFormatter|null { + if (DATE_FORMATS[format]) { + return DATE_FORMATS[format]; + } + let formatter; + switch (format) { + // Era name (AD/BC) + case 'G': + case 'GG': + case 'GGG': + formatter = dateStrGetter(TranslationType.Eras, TranslationWidth.Abbreviated); + break; + case 'GGGG': + formatter = dateStrGetter(TranslationType.Eras, TranslationWidth.Wide); + break; + case 'GGGGG': + formatter = dateStrGetter(TranslationType.Eras, TranslationWidth.Narrow); + break; + + // 1 digit representation of the year, e.g. (AD 1 => 1, AD 199 => 199) + case 'y': + formatter = dateGetter(DateType.FullYear, 1, 0, false, true); + break; + // 2 digit representation of the year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) + case 'yy': + formatter = dateGetter(DateType.FullYear, 2, 0, true, true); + break; + // 3 digit representation of the year, padded (000-999). (e.g. AD 2001 => 01, AD 2010 => 10) + case 'yyy': + formatter = dateGetter(DateType.FullYear, 3, 0, false, true); + break; + // 4 digit representation of the year (e.g. AD 1 => 0001, AD 2010 => 2010) + case 'yyyy': + formatter = dateGetter(DateType.FullYear, 4, 0, false, true); + break; + + // Month of the year (1-12), numeric + case 'M': + case 'L': + formatter = dateGetter(DateType.Month, 1, 1); + break; + case 'MM': + case 'LL': + formatter = dateGetter(DateType.Month, 2, 1); + break; + + // Month of the year (January, ...), string, format + case 'MMM': + formatter = dateStrGetter(TranslationType.Months, TranslationWidth.Abbreviated); + break; + case 'MMMM': + formatter = dateStrGetter(TranslationType.Months, TranslationWidth.Wide); + break; + case 'MMMMM': + formatter = dateStrGetter(TranslationType.Months, TranslationWidth.Narrow); + break; + + // Month of the year (January, ...), string, standalone + case 'LLL': + formatter = + dateStrGetter(TranslationType.Months, TranslationWidth.Abbreviated, FormStyle.Standalone); + break; + case 'LLLL': + formatter = + dateStrGetter(TranslationType.Months, TranslationWidth.Wide, FormStyle.Standalone); + break; + case 'LLLLL': + formatter = + dateStrGetter(TranslationType.Months, TranslationWidth.Narrow, FormStyle.Standalone); + break; + + // Week of the year (1, ... 52) + case 'w': + formatter = weekGetter(1); + break; + case 'ww': + formatter = weekGetter(2); + break; + + // Week of the month (1, ...) + case 'W': + formatter = weekGetter(1, true); + break; + + // Day of the month (1-31) + case 'd': + formatter = dateGetter(DateType.Date, 1); + break; + case 'dd': + formatter = dateGetter(DateType.Date, 2); + break; + + // Day of the Week + case 'E': + case 'EE': + case 'EEE': + formatter = dateStrGetter(TranslationType.Days, TranslationWidth.Abbreviated); + break; + case 'EEEE': + formatter = dateStrGetter(TranslationType.Days, TranslationWidth.Wide); + break; + case 'EEEEE': + formatter = dateStrGetter(TranslationType.Days, TranslationWidth.Narrow); + break; + case 'EEEEEE': + formatter = dateStrGetter(TranslationType.Days, TranslationWidth.Short); + break; + + // Generic period of the day (am-pm) + case 'a': + case 'aa': + case 'aaa': + formatter = dateStrGetter(TranslationType.DayPeriods, TranslationWidth.Abbreviated); + break; + case 'aaaa': + formatter = dateStrGetter(TranslationType.DayPeriods, TranslationWidth.Wide); + break; + case 'aaaaa': + formatter = dateStrGetter(TranslationType.DayPeriods, TranslationWidth.Narrow); + break; + + // Extended period of the day (midnight, at night, ...), standalone + case 'b': + case 'bb': + case 'bbb': + formatter = dateStrGetter( + TranslationType.DayPeriods, TranslationWidth.Abbreviated, FormStyle.Standalone, true); + break; + case 'bbbb': + formatter = dateStrGetter( + TranslationType.DayPeriods, TranslationWidth.Wide, FormStyle.Standalone, true); + break; + case 'bbbbb': + formatter = dateStrGetter( + TranslationType.DayPeriods, TranslationWidth.Narrow, FormStyle.Standalone, true); + break; + + // Extended period of the day (midnight, night, ...), standalone + case 'B': + case 'BB': + case 'BBB': + formatter = dateStrGetter( + TranslationType.DayPeriods, TranslationWidth.Abbreviated, FormStyle.Format, true); + break; + case 'BBBB': + formatter = + dateStrGetter(TranslationType.DayPeriods, TranslationWidth.Wide, FormStyle.Format, true); + break; + case 'BBBBB': + formatter = dateStrGetter( + TranslationType.DayPeriods, TranslationWidth.Narrow, FormStyle.Format, true); + break; + + // Hour in AM/PM, (1-12) + case 'h': + formatter = dateGetter(DateType.Hours, 1, -12); + break; + case 'hh': + formatter = dateGetter(DateType.Hours, 2, -12); + break; + + // Hour of the day (0-23) + case 'H': + formatter = dateGetter(DateType.Hours, 1); + break; + // Hour in day, padded (00-23) + case 'HH': + formatter = dateGetter(DateType.Hours, 2); + break; + + // Minute of the hour (0-59) + case 'm': + formatter = dateGetter(DateType.Minutes, 1); + break; + case 'mm': + formatter = dateGetter(DateType.Minutes, 2); + break; + + // Second of the minute (0-59) + case 's': + formatter = dateGetter(DateType.Seconds, 1); + break; + case 'ss': + formatter = dateGetter(DateType.Seconds, 2); + break; + + // Fractional second padded (0-9) + case 'S': + formatter = dateGetter(DateType.Milliseconds, 1); + break; + case 'SS': + formatter = dateGetter(DateType.Milliseconds, 2); + break; + // = millisecond + case 'SSS': + formatter = dateGetter(DateType.Milliseconds, 3); + break; + + + // Timezone ISO8601 short format (-0430) + case 'Z': + case 'ZZ': + case 'ZZZ': + formatter = timeZoneGetter(ZoneWidth.Short); + break; + // Timezone ISO8601 extended format (-04:30) + case 'ZZZZZ': + formatter = timeZoneGetter(ZoneWidth.Extended); + break; + + // Timezone GMT short format (GMT+4) + case 'O': + case 'OO': + case 'OOO': + // Should be location, but fallback to format O instead because we don't have the data yet + case 'z': + case 'zz': + case 'zzz': + formatter = timeZoneGetter(ZoneWidth.ShortGMT); + break; + // Timezone GMT long format (GMT+0430) + case 'OOOO': + case 'ZZZZ': + // Should be location, but fallback to format O instead because we don't have the data yet + case 'zzzz': + formatter = timeZoneGetter(ZoneWidth.Long); + break; + default: + return null; + } + DATE_FORMATS[format] = formatter; + return formatter; +} + +function timezoneToOffset(timezone: string, fallback: number): number { + // Support: IE 9-11 only, Edge 13-15+ + // IE/Edge do not "understand" colon (`:`) in timezone + timezone = timezone.replace(/:/g, ''); + const requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; +} + +function addDateMinutes(date: Date, minutes: number) { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + minutes); + return date; +} + +function convertTimezoneToLocal(date: Date, timezone: string, reverse: boolean): Date { + const reverseValue = reverse ? -1 : 1; + const dateTimezoneOffset = date.getTimezoneOffset(); + const timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + return addDateMinutes(date, reverseValue * (timezoneOffset - dateTimezoneOffset)); +} diff --git a/packages/common/src/i18n/format_number.ts b/packages/common/src/i18n/format_number.ts new file mode 100644 index 00000000000000..71a6ff91b0b463 --- /dev/null +++ b/packages/common/src/i18n/format_number.ts @@ -0,0 +1,377 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NumberFormatStyle, NumberSymbol, getLocaleNumberFormat, getLocaleNumberSymbol} from './locale_data_api'; + +export const NUMBER_FORMAT_REGEXP = /^(\d+)?\.((\d+)(-(\d+))?)?$/; +const MAX_DIGITS = 22; +const DECIMAL_SEP = '.'; +const ZERO_CHAR = '0'; +const PATTERN_SEP = ';'; +const GROUP_SEP = ','; +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 + */ +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; + + // 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; + } + + if (style === NumberFormatStyle.Percent) { + num = num * 100; + } + + const numStr = Math.abs(num) + ''; + const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign)); + let formattedText = ''; + let isZero = false; + + if (!isFinite(num)) { + formattedText = getLocaleNumberSymbol(locale, NumberSymbol.Infinity); + } else { + const parsedNumber = parseNumber(numStr); + + let minInt = pattern.minInt; + let minFraction = pattern.minFrac; + let maxFraction = pattern.maxFrac; + + if (digitsInfo) { + const parts = digitsInfo.match(NUMBER_FORMAT_REGEXP); + if (parts === null) { + res.error = `${digitsInfo} is not a valid digit info`; + return res; + } + const minIntPart = parts[1]; + const minFractionPart = parts[3]; + const maxFractionPart = parts[5]; + if (minIntPart != null) { + minInt = parseIntAutoRadix(minIntPart); + } + if (minFractionPart != null) { + minFraction = parseIntAutoRadix(minFractionPart); + } + if (maxFractionPart != null) { + maxFraction = parseIntAutoRadix(maxFractionPart); + } else if (minFractionPart != null && minFraction > maxFraction) { + maxFraction = minFraction; + } + } + + roundNumber(parsedNumber, minFraction, maxFraction); + + let digits = parsedNumber.digits; + let integerLen = parsedNumber.integerLen; + const exponent = parsedNumber.exponent; + let decimals = []; + isZero = digits.every(d => !d); + + // pad zeros for small numbers + for (; integerLen < minInt; integerLen++) { + digits.unshift(0); + } + + // pad zeros for small numbers + for (; integerLen < 0; integerLen++) { + digits.unshift(0); + } + + // extract decimals digits + if (integerLen > 0) { + decimals = digits.splice(integerLen, digits.length); + } else { + decimals = digits; + digits = [0]; + } + + // format the integer digits with grouping separators + const groups = []; + if (digits.length >= pattern.lgSize) { + groups.unshift(digits.splice(-pattern.lgSize, digits.length).join('')); + } + + while (digits.length > pattern.gSize) { + groups.unshift(digits.splice(-pattern.gSize, digits.length).join('')); + } + + if (digits.length) { + groups.unshift(digits.join('')); + } + + const currencyGroup = getLocaleNumberSymbol(locale, NumberSymbol.CurrencyGroup); + formattedText = groups.join( + currency && currencyGroup ? currencyGroup : + getLocaleNumberSymbol(locale, NumberSymbol.Group)); + + // append the decimal digits + if (decimals.length) { + formattedText += getLocaleNumberSymbol(locale, NumberSymbol.Decimal) + decimals.join(''); + } + + if (exponent) { + formattedText += getLocaleNumberSymbol(locale, NumberSymbol.Exponential) + '+' + exponent; + } + } + + if (num < 0 && !isZero) { + formattedText = pattern.negPre + formattedText + pattern.negSuf; + } else { + formattedText = pattern.posPre + formattedText + pattern.posSuf; + } + + if (style === NumberFormatStyle.Currency && currency !== null) { + res.str = formattedText.replace(new RegExp(CURRENCY_CHAR, 'g'), currency); + return res; + } + + if (style === NumberFormatStyle.Percent) { + res.str = formattedText.replace( + new RegExp(PERCENT_CHAR, 'g'), getLocaleNumberSymbol(locale, NumberSymbol.PercentSign)); + return res; + } + + res.str = formattedText; + return res; +} + +interface ParsedNumberFormat { + minInt: number; + // the minimum number of digits required in the fraction part of the number + minFrac: number; + // the maximum number of digits required in the fraction part of the number + maxFrac: number; + // the prefix for a positive number + posPre: string; + // the suffix for a positive number + posSuf: string; + // the prefix for a negative number (e.g. `-` or `(`)) + negPre: string; + // the suffix for a negative number (e.g. `)`) + negSuf: string; + // number of digits in each group of separated digits + gSize: number; + // number of digits in the last group of digits before the decimal separator + lgSize: number; +} + +function parseNumberFormat(format: string, minusSign = '-'): ParsedNumberFormat { + const p = { + minInt: 1, + minFrac: 0, + maxFrac: 0, + posPre: '', + posSuf: '', + negPre: '', + negSuf: '', + gSize: 0, + lgSize: 0 + }; + + const patternParts = format.split(PATTERN_SEP); + const positive = patternParts[0]; + const negative = patternParts[1]; + + const positiveParts = positive.indexOf(DECIMAL_SEP) !== -1 ? + positive.split(DECIMAL_SEP) : + [ + positive.substring(0, positive.lastIndexOf(ZERO_CHAR) + 1), + positive.substring(positive.lastIndexOf(ZERO_CHAR) + 1) + ], + integer = positiveParts[0], fraction = positiveParts[1] || ''; + + p.posPre = integer.substr(0, integer.indexOf(DIGIT_CHAR)); + + for (let i = 0; i < fraction.length; i++) { + const ch = fraction.charAt(i); + if (ch === ZERO_CHAR) { + p.minFrac = p.maxFrac = i + 1; + } else if (ch === DIGIT_CHAR) { + p.maxFrac = i + 1; + } else { + p.posSuf += ch; + } + } + + const groups = integer.split(GROUP_SEP); + p.gSize = groups[1] ? groups[1].length : 0; + p.lgSize = (groups[2] || groups[1]) ? (groups[2] || groups[1]).length : 0; + + if (negative) { + const trunkLen = positive.length - p.posPre.length - p.posSuf.length, + pos = negative.indexOf(DIGIT_CHAR); + + p.negPre = negative.substr(0, pos).replace(/'/g, ''); + p.negSuf = negative.substr(pos + trunkLen).replace(/'/g, ''); + } else { + p.negPre = minusSign + p.posPre; + p.negSuf = p.posSuf; + } + + return p; +} + +interface ParsedNumber { + // an array of digits containing leading zeros as necessary + digits: number[]; + // the exponent for numbers that would need more than `MAX_DIGITS` digits in `d` + exponent: number; + // the number of the digits in `d` that are to the left of the decimal point + integerLen: number; +} + +/** + * Parse a number (as a string) + * Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/ + */ +function parseNumber(numStr: string): ParsedNumber { + let exponent = 0, digits, integerLen; + let i, j, zeros; + + // Decimal point? + if ((integerLen = numStr.indexOf(DECIMAL_SEP)) > -1) { + numStr = numStr.replace(DECIMAL_SEP, ''); + } + + // Exponential form? + if ((i = numStr.search(/e/i)) > 0) { + // Work out the exponent. + if (integerLen < 0) integerLen = i; + integerLen += +numStr.slice(i + 1); + numStr = numStr.substring(0, i); + } else if (integerLen < 0) { + // There was no decimal point or exponent so it is an integer. + integerLen = numStr.length; + } + + // Count the number of leading zeros. + for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ + } + + if (i === (zeros = numStr.length)) { + // The digits are all zero. + digits = [0]; + integerLen = 1; + } else { + // Count the number of trailing zeros + zeros--; + while (numStr.charAt(zeros) === ZERO_CHAR) zeros--; + + // Trailing zeros are insignificant so ignore them + integerLen -= i; + digits = []; + // Convert string to array of digits without leading/trailing zeros. + for (j = 0; i <= zeros; i++, j++) { + digits[j] = +numStr.charAt(i); + } + } + + // If the number overflows the maximum allowed digits then use an exponent. + if (integerLen > MAX_DIGITS) { + digits = digits.splice(0, MAX_DIGITS - 1); + exponent = integerLen - 1; + integerLen = 1; + } + + return {digits, exponent, integerLen}; +} + +/** + * Round the parsed number to the specified number of decimal places + * This function changes the parsedNumber in-place + */ +function roundNumber(parsedNumber: ParsedNumber, minFrac: number, maxFrac: number) { + if (minFrac > maxFrac) { + throw new Error( + `The minimum number of digits after fraction (${minFrac}) is higher than the maximum (${maxFrac}).`); + } + + let digits = parsedNumber.digits; + let fractionLen = digits.length - parsedNumber.integerLen; + const fractionSize = Math.min(Math.max(minFrac, fractionLen), maxFrac); + + // The index of the digit to where rounding is to occur + let roundAt = fractionSize + parsedNumber.integerLen; + let digit = digits[roundAt]; + + if (roundAt > 0) { + // Drop fractional digits beyond `roundAt` + digits.splice(Math.max(parsedNumber.integerLen, roundAt)); + + // Set non-fractional digits beyond `roundAt` to 0 + for (let j = roundAt; j < digits.length; j++) { + digits[j] = 0; + } + } else { + // We rounded to zero so reset the parsedNumber + fractionLen = Math.max(0, fractionLen); + parsedNumber.integerLen = 1; + digits.length = Math.max(1, roundAt = fractionSize + 1); + digits[0] = 0; + for (let i = 1; i < roundAt; i++) digits[i] = 0; + } + + if (digit >= 5) { + if (roundAt - 1 < 0) { + for (let k = 0; k > roundAt; k--) { + digits.unshift(0); + parsedNumber.integerLen++; + } + digits.unshift(1); + parsedNumber.integerLen++; + } else { + digits[roundAt - 1]++; + } + } + + // Pad out with zeros to get the required fraction length + for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0); + + + // Do any carrying, e.g. a digit was rounded up to 10 + const carry = digits.reduceRight(function(carry, d, i, digits) { + d = d + carry; + digits[i] = d % 10; + return Math.floor(d / 10); + }, 0); + if (carry) { + digits.unshift(carry); + parsedNumber.integerLen++; + } +} + +/** @internal */ +export function parseIntAutoRadix(text: string): number { + const result: number = parseInt(text); + if (isNaN(result)) { + throw new Error('Invalid integer literal when parsing ' + text); + } + return result; +} diff --git a/packages/common/src/i18n/locale_data.ts b/packages/common/src/i18n/locale_data.ts new file mode 100644 index 00000000000000..cbcbaf1afe61d6 --- /dev/null +++ b/packages/common/src/i18n/locale_data.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** @experimental */ +export enum Plural { + Zero, + One, + Two, + Few, + Many, + Other, +} + +/** + * @experimental i18n support is experimental. + */ +export const LOCALE_DATA: {[localeId: string]: any} = {}; diff --git a/packages/common/src/i18n/locale_data_api.ts b/packages/common/src/i18n/locale_data_api.ts new file mode 100644 index 00000000000000..06a29fa3127d3f --- /dev/null +++ b/packages/common/src/i18n/locale_data_api.ts @@ -0,0 +1,594 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AVAILABLE_LOCALES} from './available_locales'; +import {CURRENCIES} from './currencies'; +import localeEn from './locale_en'; +import {LOCALE_DATA, Plural} from './locale_data'; + +/** + * The different format styles that can be used to represent numbers. + * Used by the function {@link getLocaleNumberFormat}. + * + * @experimental i18n support is experimental. + */ +export enum NumberFormatStyle { + Decimal, + Percent, + Currency, + Scientific +} + +/** + * Some languages use two different forms of strings (standalone and format) depending on the + * context. + * Typically the standalone version is the nominative form of the word, and the format version is in + * the genitive. + * See [the CLDR website](http://cldr.unicode.org/translation/date-time) for more information. + * + * @experimental i18n support is experimental. + */ +export enum FormStyle { + Format, + Standalone +} + +/** + * Multiple widths are available for translations: narrow (1 character), abbreviated (3 characters), + * wide (full length), and short (2 characters, only for days). + * + * For example the day `Sunday` will be: + * - Narrow: `S` + * - Short: `Su` + * - Abbreviated: `Sun` + * - Wide: `Sunday` + * + * @experimental i18n support is experimental. + */ +export enum TranslationWidth { + Narrow, + Abbreviated, + Wide, + Short +} + +/** + * Multiple widths are available for formats: short (minimal amount of data), medium (small amount + * of data), long (complete amount of data), full (complete amount of data and extra information). + * + * For example the date-time formats for the english locale will be: + * - `'short'`: `'M/d/yy, h:mm a'` (e.g. `6/15/15, 9:03 AM`) + * - `'medium'`: `'MMM d, y, h:mm:ss a'` (e.g. `Jun 15, 2015, 9:03:01 AM`) + * - `'long'`: `'MMMM d, y, h:mm:ss a z'` (e.g. `June 15, 2015 at 9:03:01 AM GMT+1`) + * - `'full'`: `'EEEE, MMMM d, y, h:mm:ss a zzzz'` (e.g. `Monday, June 15, 2015 at + * 9:03:01 AM GMT+01:00`) + * + * @experimental i18n support is experimental. + */ +export enum FormatWidth { + Short, + Medium, + Long, + Full +} + +/** + * Number symbol that can be used to replace placeholders in number patterns. + * The placeholders are based on english values: + * + * | Name | Example for en-US | Meaning | + * |------------------------|-------------------|---------------------------------------------| + * | decimal | 2,345`.`67 | decimal separator | + * | group | 2`,`345.67 | grouping separator, typically for thousands | + * | plusSign | `+`23 | the plus sign used with numbers | + * | minusSign | `-`23 | the minus sign used with numbers | + * | percentSign | 23.4`%` | the percent sign (out of 100) | + * | perMille | 234`‰` | the permille sign (out of 1000) | + * | exponential | 1.2`E`3 | used in computers for 1.2×10³. | + * | superscriptingExponent | 1.2`×`103 | human-readable format of exponential | + * | infinity | `∞` | used in +∞ and -∞. | + * | nan | `NaN` | "not a number". | + * + * @experimental i18n support is experimental. + */ +export enum NumberSymbol { + Decimal, + Group, + List, + PercentSign, + PlusSign, + MinusSign, + Exponential, + SuperscriptingExponent, + PerMille, + Infinity, + NaN, + TimeSeparator, + CurrencyGroup +} + +/** + * The value for each day of the week, based on the en-US locale + * + * @experimental + */ +export enum WeekDay { + Sunday = 0, + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday +} + +/** + * Use this enum to find the index of each type of locale data from the locale data array + */ +enum LocaleDataIndex { + LocaleId = 0, + DayPeriodsFormat, + DayPeriodsStandalone, + DaysFormat, + DaysStandalone, + MonthsFormat, + MonthsStandalone, + Eras, + FirstDayOfWeek, + WeekendRange, + DateFormat, + TimeFormat, + DateTimeFormat, + NumberSymbols, + NumberFormats, + CurrencySymbol, + CurrencyName, + PluralCase, + ExtraData +} + +/** + * Use this enum to find the index of each type of locale data from the extra locale data array + */ +enum ExtraLocaleDataIndex { + ExtraDayPeriodsRules = 0, + ExtraDayPeriodFormats, + ExtraDayPeriodStandalone +} + +/** + * The locale id for the chosen locale (e.g `en-GB`). + * + * @experimental i18n support is experimental. + */ +export function getLocaleId(locale: string): string { + return findLocaleData(locale)[LocaleDataIndex.LocaleId]; +} + +/** + * Periods of the day (e.g. `[AM, PM]` for en-US). + * + * @experimental i18n support is experimental. + */ +export function getLocaleDayPeriods( + locale: string, formStyle: FormStyle, width: TranslationWidth): [string, string] { + const data = findLocaleData(locale); + const amPmData = <[ + string, string + ][][]>[data[LocaleDataIndex.DayPeriodsFormat], data[LocaleDataIndex.DayPeriodsStandalone]]; + const amPm = getLastDefinedValue(amPmData, formStyle); + return getLastDefinedValue(amPm, width); +} + +/** + * Days of the week for the Gregorian calendar (e.g. `[Sunday, Monday, ... Saturday]` for en-US). + * + * @experimental i18n support is experimental. + */ +export function getLocaleDayNames( + locale: string, formStyle: FormStyle, width: TranslationWidth): string[] { + const data = findLocaleData(locale); + const daysData = + [data[LocaleDataIndex.DaysFormat], data[LocaleDataIndex.DaysStandalone]]; + const days = getLastDefinedValue(daysData, formStyle); + return getLastDefinedValue(days, width); +} + +/** + * Months of the year for the Gregorian calendar (e.g. `[January, February, ...]` for en-US). + * + * @experimental i18n support is experimental. + */ +export function getLocaleMonthNames( + locale: string, formStyle: FormStyle, width: TranslationWidth): string[] { + const data = findLocaleData(locale); + const monthsData = + [data[LocaleDataIndex.MonthsFormat], data[LocaleDataIndex.MonthsStandalone]]; + const months = getLastDefinedValue(monthsData, formStyle); + return getLastDefinedValue(months, width); +} + +/** + * Eras for the Gregorian calendar (e.g. AD/BC). + * + * @experimental i18n support is experimental. + */ +export function getLocaleEraNames(locale: string, width: TranslationWidth): [string, string] { + const data = findLocaleData(locale); + const erasData = <[string, string][]>data[LocaleDataIndex.Eras]; + return getLastDefinedValue(erasData, width); +} + +/** + * First day of the week for this locale, based on english days (Sunday = 0, Monday = 1, ...). + * For example in french the value would be 1 because the first day of the week is Monday. + * + * @experimental i18n support is experimental. + */ +export function getLocaleFirstDayOfWeek(locale: string): WeekDay { + const data = findLocaleData(locale); + return data[LocaleDataIndex.FirstDayOfWeek]; +} + +/** + * Range of days in the week that represent the week-end for this locale, based on english days + * (Sunday = 0, Monday = 1, ...). + * For example in english the value would be [6,0] for Saturday to Sunday. + * + * @experimental i18n support is experimental. + */ +export function getLocaleWeekEndRange(locale: string): [WeekDay, WeekDay] { + const data = findLocaleData(locale); + return data[LocaleDataIndex.WeekendRange]; +} + +/** + * Date format that depends on the locale. + * + * There are four basic date formats: + * - `full` should contain long-weekday (EEEE), year (y), long-month (MMMM), day (d). + * + * For example, English uses `EEEE, MMMM d, y`, corresponding to a date like + * "Tuesday, September 14, 1999". + * + * - `long` should contain year, long-month, day. + * + * For example, `MMMM d, y`, corresponding to a date like "September 14, 1999". + * + * - `medium` should contain year, abbreviated-month (MMM), day. + * + * For example, `MMM d, y`, corresponding to a date like "Sep 14, 1999". + * For languages that do not use abbreviated months, use the numeric month (MM/M). For example, + * `y/MM/dd`, corresponding to a date like "1999/09/14". + * + * - `short` should contain year, numeric-month (MM/M), and day. + * + * For example, `M/d/yy`, corresponding to a date like "9/14/99". + * + * @experimental i18n support is experimental. + */ +export function getLocaleDateFormat(locale: string, width: FormatWidth): string { + const data = findLocaleData(locale); + return data[LocaleDataIndex.DateFormat][width]; +} + +/** + * Time format that depends on the locale. + * + * The standard formats include four basic time formats: + * - `full` should contain hour (h/H), minute (mm), second (ss), and zone (zzzz). + * - `long` should contain hour, minute, second, and zone (z) + * - `medium` should contain hour, minute, second. + * - `short` should contain hour, minute. + * + * Note: The patterns depend on whether the main country using your language uses 12-hour time or + * not: + * - For 12-hour time, use a pattern like `hh:mm a` using h to mean a 12-hour clock cycle running + * 1 through 12 (midnight plus 1 minute is 12:01), or using K to mean a 12-hour clock cycle + * running 0 through 11 (midnight plus 1 minute is 0:01). + * - For 24-hour time, use a pattern like `HH:mm` using H to mean a 24-hour clock cycle running 0 + * through 23 (midnight plus 1 minute is 0:01), or using k to mean a 24-hour clock cycle running + * 1 through 24 (midnight plus 1 minute is 24:01). + * + * @experimental i18n support is experimental. + */ +export function getLocaleTimeFormat(locale: string, width: FormatWidth): string { + const data = findLocaleData(locale); + return data[LocaleDataIndex.TimeFormat][width]; +} + +/** + * Date-time format that depends on the locale. + * + * The date-time pattern shows how to combine separate patterns for date (represented by {1}) + * and time (represented by {0}) into a single pattern. It usually doesn't need to be changed. + * What you want to pay attention to are: + * - possibly removing a space for languages that don't use it, such as many East Asian languages + * - possibly adding a comma, other punctuation, or a combining word + * + * For example: + * - English uses `{1} 'at' {0}` or `{1}, {0}` (depending on date style), while Japanese uses + * `{1}{0}`. + * - An English formatted date-time using the combining pattern `{1}, {0}` could be + * `Dec 10, 2010, 3:59:49 PM`. Notice the comma and space between the date portion and the time + * portion. + * + * There are four formats (`full`, `long`, `medium`, `short`); the determination of which to use + * is normally based on the date style. For example, if the date has a full month and weekday + * name, the full combining pattern will be used to combine that with a time. If the date has + * numeric month, the short version of the combining pattern will be used to combine that with a + * time. English uses `{1} 'at' {0}` for full and long styles, and `{1}, {0}` for medium and short + * styles. + * + * @experimental i18n support is experimental. + */ +export function getLocaleDateTimeFormat(locale: string, width: FormatWidth): string { + const data = findLocaleData(locale); + const dateTimeFormatData = data[LocaleDataIndex.DateTimeFormat]; + return getLastDefinedValue(dateTimeFormatData, width); +} + +/** + * Number symbol that can be used to replace placeholders in number formats. + * See {@link NumberSymbol} for more information. + * + * @experimental i18n support is experimental. + */ +export function getLocaleNumberSymbol(locale: string, symbol: NumberSymbol): string { + const data = findLocaleData(locale); + return data[LocaleDataIndex.NumberSymbols][symbol]; +} + +/** + * Number format that depends on the locale. + * + * Numbers are formatted using patterns, like `#,###.00`. For example, the pattern `#,###.00` + * when used to format the number 12345.678 could result in "12'345,67". That would happen if the + * grouping separator for your language is an apostrophe, and the decimal separator is a comma. + * + * Important: The characters `.` `,` `0` `#` (and others below) are special placeholders; + * they stand for the decimal separator, and so on, and are NOT real characters. + * You must NOT "translate" the placeholders; for example, don't change `.` to `,` even though in + * your language the decimal point is written with a comma. The symbols should be replaced by the + * local equivalents, using the Number Symbols for your language. + * + * Here are the special characters used in number patterns: + * + * | Symbol | Meaning | + * |--------|---------| + * | . | Replaced automatically by the character used for the decimal point. | + * | , | Replaced by the "grouping" (thousands) separator. | + * | 0 | Replaced by a digit (or zero if there aren't enough digits). | + * | # | Replaced by a digit (or nothing if there aren't enough). | + * | ¤ | This will be replaced by a currency symbol, such as $ or USD. | + * | % | This marks a percent format. The % symbol may change position, but must be retained. | + * | E | This marks a scientific format. The E symbol may change position, but must be retained. | + * | ' | Special characters used as literal characters are quoted with ASCII single quotes. | + * + * You can find more information + * [on the CLDR website](http://cldr.unicode.org/translation/number-patterns) + * + * @experimental i18n support is experimental. + */ +export function getLocaleNumberFormat(locale: string, type: NumberFormatStyle): string { + const data = findLocaleData(locale); + return data[LocaleDataIndex.NumberFormats][type]; +} + +/** + * The symbol used to represent the currency for the main country using this locale (e.g. $ for + * the locale en-US). + * The symbol will be `null` if the main country cannot be determined. + * + * @experimental i18n support is experimental. + */ +export function getLocaleCurrencySymbol(locale: string): string|null { + const data = findLocaleData(locale); + return data[LocaleDataIndex.CurrencySymbol] || null; +} + +/** + * The name of the currency for the main country using this locale (e.g. USD for the locale + * en-US). + * The name will be `null` if the main country cannot be determined. + * + * @experimental i18n support is experimental. + */ +export function getLocaleCurrencyName(locale: string): string|null { + const data = findLocaleData(locale); + return data[LocaleDataIndex.CurrencyName] || null; +} + +/** + * The locale plural function used by ICU expressions to determine the plural case to use. + * See {@link NgPlural} for more information. + * + * @experimental i18n support is experimental. + */ +export function getLocalePluralCase(locale: string): (value: number) => Plural { + const data = findLocaleData(locale); + return data[LocaleDataIndex.PluralCase]; +} + +function checkFullData(data: any) { + if (!data[LocaleDataIndex.ExtraData]) { + throw new Error( + `Missing extra locale data for the locale "${data[LocaleDataIndex.LocaleId]}". Use "registerLocaleData" to load new data. See the "I18n guide" on angular.io to know more.`); + } +} + +/** + * Rules used to determine which day period to use (See `dayPeriods` below). + * The rules can either be an array or a single value. If it's an array, consider it as "from" + * and "to". If it's a single value then it means that the period is only valid at this exact + * value. + * There is always the same number of rules as the number of day periods, which means that the + * first rule is applied to the first day period and so on. + * You should fallback to AM/PM when there are no rules available. + * + * Note: this is only available if you load the full locale data. + * See the {@linkDocs guide/i18n#i18n-pipes "I18n guide"} to know how to import additional locale + * data. + * + * @experimental i18n support is experimental. + */ +export function getLocaleExtraDayPeriodRules(locale: string): (Time | [Time, Time])[] { + const data = findLocaleData(locale); + checkFullData(data); + const rules = data[LocaleDataIndex.ExtraData][ExtraLocaleDataIndex.ExtraDayPeriodsRules] || []; + return rules.map((rule: string | [string, string]) => { + if (typeof rule === 'string') { + return extractTime(rule); + } + return [extractTime(rule[0]), extractTime(rule[1])]; + }); +} + +/** + * Day Periods indicate roughly how the day is broken up in different languages (e.g. morning, + * noon, afternoon, midnight, ...). + * You should use the function {@link getLocaleExtraDayPeriodRules} to determine which period to + * use. + * You should fallback to AM/PM when there are no day periods available. + * + * Note: this is only available if you load the full locale data. + * See the {@linkDocs guide/i18n#i18n-pipes "I18n guide"} to know how to import additional locale + * data. + * + * @experimental i18n support is experimental. + */ +export function getLocaleExtraDayPeriods( + locale: string, formStyle: FormStyle, width: TranslationWidth): string[] { + const data = findLocaleData(locale); + checkFullData(data); + const dayPeriodsData = [ + data[LocaleDataIndex.ExtraData][ExtraLocaleDataIndex.ExtraDayPeriodFormats], + data[LocaleDataIndex.ExtraData][ExtraLocaleDataIndex.ExtraDayPeriodStandalone] + ]; + const dayPeriods = getLastDefinedValue(dayPeriodsData, formStyle) || []; + return getLastDefinedValue(dayPeriods, width) || []; +} + +/** + * Returns the first value that is defined in an array, going backwards. + * + * To avoid repeating the same data (e.g. when "format" and "standalone" are the same) we only + * add the first one to the locale data arrays, the other ones are only defined when different. + * We use this function to retrieve the first defined value. + * + * @experimental i18n support is experimental. + */ +function getLastDefinedValue(data: T[], index: number): T { + for (let i = index; i > -1; i--) { + if (typeof data[i] !== 'undefined') { + return data[i]; + } + } + throw new Error('Locale data API: locale data undefined'); +} + +/** + * A representation of the time with hours and minutes + * + * @experimental i18n support is experimental. + */ +export type Time = { + hours: number, + minutes: number +}; + +/** + * Extract the hours and minutes from a string like "15:45" + */ +function extractTime(time: string): Time { + const [h, m] = time.split(':'); + return {hours: +h, minutes: +m}; +} + +/** + * Finds the locale data for a locale id + * + * @experimental i18n support is experimental. + */ +export function findLocaleData(locale: string): any { + const normalizedLocale = getNormalizedLocale(locale); + + if (normalizedLocale === 'en') { + return LOCALE_DATA['en'] || localeEn; + } + + const match = LOCALE_DATA[toCamelCase(normalizedLocale)]; + if (match) { + return match; + } + + throw new Error( + `Missing locale data for the locale "${locale}". Use "registerLocaleData" to load new data. See the "I18n guide" on angular.io to know more.`); +} + +const NORMALIZED_LOCALES: any = {}; + +/** + * Returns the closest matching locale that exists or throw + * e.g.: "en-US" will return "en", and "fr_ca" will return "fr-CA" + * Rules for locale id equivalences are defined in + * http://cldr.unicode.org/index/cldr-spec/language-tag-equivalences + * and in https://tools.ietf.org/html/rfc4647#section-3.4 + */ +function getNormalizedLocale(locale: string): string { + if (NORMALIZED_LOCALES[locale]) { + return NORMALIZED_LOCALES[locale]; + } + + const normalizedLocale = locale.toLowerCase().replace(/_/g, '-'); + const match = AVAILABLE_LOCALES.find((l: string) => l.toLowerCase() === normalizedLocale); + + if (match) { + NORMALIZED_LOCALES[locale] = match; + return match; + } + + const parentLocale = normalizedLocale.split('-')[0]; + if (AVAILABLE_LOCALES.find((l: string) => l.toLowerCase() === parentLocale)) { + NORMALIZED_LOCALES[locale] = parentLocale; + return parentLocale; + } + + throw new Error( + `"${locale}" is not a valid LOCALE_ID value. See https://github.com/unicode-cldr/cldr-core/blob/master/availableLocales.json for a list of valid locales`); +} + +function toCamelCase(str: string): string { + return str.replace(/-+([a-z0-9A-Z])/g, (...m: string[]) => m[1].toUpperCase()); +} + +/** + * Return the currency symbol for a given currency code, or the code if no symbol available + * (e.g.: $, US$, or USD) + * + * @internal + */ +export function findCurrencySymbol(code: string, format: 'wide' | 'narrow') { + const currency = CURRENCIES[code] || {}; + const symbol = currency[0] || code; + return format === 'wide' ? symbol : currency[1] || symbol; +} + +/** + * Register global data to be used internally by Angular. See the + * {@linkDocs guide/i18n#i18n-pipes "I18n guide"} to know how to import additional locale data. + * + * @experimental i18n support is experimental. + */ +export function registerLocaleData(data: any, extraData?: any) { + const localeId = toCamelCase(data[LocaleDataIndex.LocaleId]); + LOCALE_DATA[localeId] = data; + if (extraData) { + LOCALE_DATA[localeId][LocaleDataIndex.ExtraData] = extraData; + } +} diff --git a/packages/common/src/i18n/localization.ts b/packages/common/src/i18n/localization.ts new file mode 100644 index 00000000000000..2ac5a1a0e1139e --- /dev/null +++ b/packages/common/src/i18n/localization.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, Injectable, LOCALE_ID} from '@angular/core'; +import {Plural} from './locale_data'; +import {getLocalePluralCase} from './locale_data_api'; + +/** + * @experimental + */ +export abstract class NgLocalization { + abstract getPluralCategory(value: any, locale?: string): string; +} + + +/** + * Returns the plural category for a given value. + * - "=value" when the case exists, + * - the plural category otherwise + * + * @internal + */ +export function getPluralCategory( + value: number, cases: string[], ngLocalization: NgLocalization, locale?: string): string { + let key = `=${value}`; + + if (cases.indexOf(key) > -1) { + return key; + } + + key = ngLocalization.getPluralCategory(value, locale); + + if (cases.indexOf(key) > -1) { + return key; + } + + if (cases.indexOf('other') > -1) { + return 'other'; + } + + throw new Error(`No plural message found for value "${value}"`); +} + +/** + * Returns the plural case based on the locale + * + * @experimental + */ +@Injectable() +export class NgLocaleLocalization extends NgLocalization { + constructor(@Inject(LOCALE_ID) protected locale: string) { super(); } + + getPluralCategory(value: any, locale?: string): string { + const plural = getLocalePluralCase(locale || this.locale)(value); + + switch (plural) { + case Plural.Zero: + return 'zero'; + case Plural.One: + return 'one'; + case Plural.Two: + return 'two'; + case Plural.Few: + return 'few'; + case Plural.Many: + return 'many'; + default: + return 'other'; + } + } +} diff --git a/packages/common/src/localization.ts b/packages/common/src/localization.ts deleted file mode 100644 index c2deb745b524c9..00000000000000 --- a/packages/common/src/localization.ts +++ /dev/null @@ -1,401 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {Inject, Injectable, LOCALE_ID} from '@angular/core'; - -/** - * @experimental - */ -export abstract class NgLocalization { abstract getPluralCategory(value: any): string; } - - -/** - * Returns the plural category for a given value. - * - "=value" when the case exists, - * - the plural category otherwise - * - * @internal - */ -export function getPluralCategory( - value: number, cases: string[], ngLocalization: NgLocalization): string { - let key = `=${value}`; - - if (cases.indexOf(key) > -1) { - return key; - } - - key = ngLocalization.getPluralCategory(value); - - if (cases.indexOf(key) > -1) { - return key; - } - - if (cases.indexOf('other') > -1) { - return 'other'; - } - - throw new Error(`No plural message found for value "${value}"`); -} - -/** - * Returns the plural case based on the locale - * - * @experimental - */ -@Injectable() -export class NgLocaleLocalization extends NgLocalization { - constructor(@Inject(LOCALE_ID) protected locale: string) { super(); } - - getPluralCategory(value: any): string { - const plural = getPluralCase(this.locale, value); - - switch (plural) { - case Plural.Zero: - return 'zero'; - case Plural.One: - return 'one'; - case Plural.Two: - return 'two'; - case Plural.Few: - return 'few'; - case Plural.Many: - return 'many'; - default: - return 'other'; - } - } -} - -// This is generated code DO NOT MODIFY -// see angular/script/cldr/gen_plural_rules.js - -/** @experimental */ -export enum Plural { - Zero, - One, - Two, - Few, - Many, - Other, -} - -/** - * Returns the plural case based on the locale - * - * @experimental - */ -export function getPluralCase(locale: string, nLike: number | string): Plural { - // TODO(vicb): lazy compute - if (typeof nLike === 'string') { - nLike = parseInt(nLike, 10); - } - const n: number = nLike as number; - const nDecimal = n.toString().replace(/^[^.]*\.?/, ''); - const i = Math.floor(Math.abs(n)); - const v = nDecimal.length; - const f = parseInt(nDecimal, 10); - const t = parseInt(n.toString().replace(/^[^.]*\.?|0+$/g, ''), 10) || 0; - - const lang = locale.split('-')[0].toLowerCase(); - - switch (lang) { - case 'af': - case 'asa': - case 'az': - case 'bem': - case 'bez': - case 'bg': - case 'brx': - case 'ce': - case 'cgg': - case 'chr': - case 'ckb': - case 'ee': - case 'el': - case 'eo': - case 'es': - case 'eu': - case 'fo': - case 'fur': - case 'gsw': - case 'ha': - case 'haw': - case 'hu': - case 'jgo': - case 'jmc': - case 'ka': - case 'kk': - case 'kkj': - case 'kl': - case 'ks': - case 'ksb': - case 'ky': - case 'lb': - case 'lg': - case 'mas': - case 'mgo': - case 'ml': - case 'mn': - case 'nb': - case 'nd': - case 'ne': - case 'nn': - case 'nnh': - case 'nyn': - case 'om': - case 'or': - case 'os': - case 'ps': - case 'rm': - case 'rof': - case 'rwk': - case 'saq': - case 'seh': - case 'sn': - case 'so': - case 'sq': - case 'ta': - case 'te': - case 'teo': - case 'tk': - case 'tr': - case 'ug': - case 'uz': - case 'vo': - case 'vun': - case 'wae': - case 'xog': - if (n === 1) return Plural.One; - return Plural.Other; - case 'ak': - case 'ln': - case 'mg': - case 'pa': - case 'ti': - if (n === Math.floor(n) && n >= 0 && n <= 1) return Plural.One; - return Plural.Other; - case 'am': - case 'as': - case 'bn': - case 'fa': - case 'gu': - case 'hi': - case 'kn': - case 'mr': - case 'zu': - if (i === 0 || n === 1) return Plural.One; - return Plural.Other; - case 'ar': - if (n === 0) return Plural.Zero; - if (n === 1) return Plural.One; - if (n === 2) return Plural.Two; - if (n % 100 === Math.floor(n % 100) && n % 100 >= 3 && n % 100 <= 10) return Plural.Few; - if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 99) return Plural.Many; - return Plural.Other; - case 'ast': - case 'ca': - case 'de': - case 'en': - case 'et': - case 'fi': - case 'fy': - case 'gl': - case 'it': - case 'nl': - case 'sv': - case 'sw': - case 'ur': - case 'yi': - if (i === 1 && v === 0) return Plural.One; - return Plural.Other; - case 'be': - if (n % 10 === 1 && !(n % 100 === 11)) return Plural.One; - if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 4 && - !(n % 100 >= 12 && n % 100 <= 14)) - return Plural.Few; - if (n % 10 === 0 || n % 10 === Math.floor(n % 10) && n % 10 >= 5 && n % 10 <= 9 || - n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 14) - return Plural.Many; - return Plural.Other; - case 'br': - if (n % 10 === 1 && !(n % 100 === 11 || n % 100 === 71 || n % 100 === 91)) return Plural.One; - if (n % 10 === 2 && !(n % 100 === 12 || n % 100 === 72 || n % 100 === 92)) return Plural.Two; - if (n % 10 === Math.floor(n % 10) && (n % 10 >= 3 && n % 10 <= 4 || n % 10 === 9) && - !(n % 100 >= 10 && n % 100 <= 19 || n % 100 >= 70 && n % 100 <= 79 || - n % 100 >= 90 && n % 100 <= 99)) - return Plural.Few; - if (!(n === 0) && n % 1e6 === 0) return Plural.Many; - return Plural.Other; - case 'bs': - case 'hr': - case 'sr': - if (v === 0 && i % 10 === 1 && !(i % 100 === 11) || f % 10 === 1 && !(f % 100 === 11)) - return Plural.One; - if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && - !(i % 100 >= 12 && i % 100 <= 14) || - f % 10 === Math.floor(f % 10) && f % 10 >= 2 && f % 10 <= 4 && - !(f % 100 >= 12 && f % 100 <= 14)) - return Plural.Few; - return Plural.Other; - case 'cs': - case 'sk': - if (i === 1 && v === 0) return Plural.One; - if (i === Math.floor(i) && i >= 2 && i <= 4 && v === 0) return Plural.Few; - if (!(v === 0)) return Plural.Many; - return Plural.Other; - case 'cy': - if (n === 0) return Plural.Zero; - if (n === 1) return Plural.One; - if (n === 2) return Plural.Two; - if (n === 3) return Plural.Few; - if (n === 6) return Plural.Many; - return Plural.Other; - case 'da': - if (n === 1 || !(t === 0) && (i === 0 || i === 1)) return Plural.One; - return Plural.Other; - case 'dsb': - case 'hsb': - if (v === 0 && i % 100 === 1 || f % 100 === 1) return Plural.One; - if (v === 0 && i % 100 === 2 || f % 100 === 2) return Plural.Two; - if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || - f % 100 === Math.floor(f % 100) && f % 100 >= 3 && f % 100 <= 4) - return Plural.Few; - return Plural.Other; - case 'ff': - case 'fr': - case 'hy': - case 'kab': - if (i === 0 || i === 1) return Plural.One; - return Plural.Other; - case 'fil': - if (v === 0 && (i === 1 || i === 2 || i === 3) || - v === 0 && !(i % 10 === 4 || i % 10 === 6 || i % 10 === 9) || - !(v === 0) && !(f % 10 === 4 || f % 10 === 6 || f % 10 === 9)) - return Plural.One; - return Plural.Other; - case 'ga': - if (n === 1) return Plural.One; - if (n === 2) return Plural.Two; - if (n === Math.floor(n) && n >= 3 && n <= 6) return Plural.Few; - if (n === Math.floor(n) && n >= 7 && n <= 10) return Plural.Many; - return Plural.Other; - case 'gd': - if (n === 1 || n === 11) return Plural.One; - if (n === 2 || n === 12) return Plural.Two; - if (n === Math.floor(n) && (n >= 3 && n <= 10 || n >= 13 && n <= 19)) return Plural.Few; - return Plural.Other; - case 'gv': - if (v === 0 && i % 10 === 1) return Plural.One; - if (v === 0 && i % 10 === 2) return Plural.Two; - if (v === 0 && - (i % 100 === 0 || i % 100 === 20 || i % 100 === 40 || i % 100 === 60 || i % 100 === 80)) - return Plural.Few; - if (!(v === 0)) return Plural.Many; - return Plural.Other; - case 'he': - if (i === 1 && v === 0) return Plural.One; - if (i === 2 && v === 0) return Plural.Two; - if (v === 0 && !(n >= 0 && n <= 10) && n % 10 === 0) return Plural.Many; - return Plural.Other; - case 'is': - if (t === 0 && i % 10 === 1 && !(i % 100 === 11) || !(t === 0)) return Plural.One; - return Plural.Other; - case 'ksh': - if (n === 0) return Plural.Zero; - if (n === 1) return Plural.One; - return Plural.Other; - case 'kw': - case 'naq': - case 'se': - case 'smn': - if (n === 1) return Plural.One; - if (n === 2) return Plural.Two; - return Plural.Other; - case 'lag': - if (n === 0) return Plural.Zero; - if ((i === 0 || i === 1) && !(n === 0)) return Plural.One; - return Plural.Other; - case 'lt': - if (n % 10 === 1 && !(n % 100 >= 11 && n % 100 <= 19)) return Plural.One; - if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 9 && - !(n % 100 >= 11 && n % 100 <= 19)) - return Plural.Few; - if (!(f === 0)) return Plural.Many; - return Plural.Other; - case 'lv': - case 'prg': - if (n % 10 === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19 || - v === 2 && f % 100 === Math.floor(f % 100) && f % 100 >= 11 && f % 100 <= 19) - return Plural.Zero; - if (n % 10 === 1 && !(n % 100 === 11) || v === 2 && f % 10 === 1 && !(f % 100 === 11) || - !(v === 2) && f % 10 === 1) - return Plural.One; - return Plural.Other; - case 'mk': - if (v === 0 && i % 10 === 1 || f % 10 === 1) return Plural.One; - return Plural.Other; - case 'mt': - if (n === 1) return Plural.One; - if (n === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 2 && n % 100 <= 10) - return Plural.Few; - if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19) return Plural.Many; - return Plural.Other; - case 'pl': - if (i === 1 && v === 0) return Plural.One; - if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && - !(i % 100 >= 12 && i % 100 <= 14)) - return Plural.Few; - if (v === 0 && !(i === 1) && i % 10 === Math.floor(i % 10) && i % 10 >= 0 && i % 10 <= 1 || - v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || - v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 12 && i % 100 <= 14) - return Plural.Many; - return Plural.Other; - case 'pt': - if (n === Math.floor(n) && n >= 0 && n <= 2 && !(n === 2)) return Plural.One; - return Plural.Other; - case 'ro': - if (i === 1 && v === 0) return Plural.One; - if (!(v === 0) || n === 0 || - !(n === 1) && n % 100 === Math.floor(n % 100) && n % 100 >= 1 && n % 100 <= 19) - return Plural.Few; - return Plural.Other; - case 'ru': - case 'uk': - if (v === 0 && i % 10 === 1 && !(i % 100 === 11)) return Plural.One; - if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && - !(i % 100 >= 12 && i % 100 <= 14)) - return Plural.Few; - if (v === 0 && i % 10 === 0 || - v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || - v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 11 && i % 100 <= 14) - return Plural.Many; - return Plural.Other; - case 'shi': - if (i === 0 || n === 1) return Plural.One; - if (n === Math.floor(n) && n >= 2 && n <= 10) return Plural.Few; - return Plural.Other; - case 'si': - if (n === 0 || n === 1 || i === 0 && f === 1) return Plural.One; - return Plural.Other; - case 'sl': - if (v === 0 && i % 100 === 1) return Plural.One; - if (v === 0 && i % 100 === 2) return Plural.Two; - if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || !(v === 0)) - return Plural.Few; - return Plural.Other; - case 'tzm': - if (n === Math.floor(n) && n >= 0 && n <= 1 || n === Math.floor(n) && n >= 11 && n <= 99) - return Plural.One; - return Plural.Other; - // When there is no specification, the default is always "other" - // Spec: http://cldr.unicode.org/index/cldr-spec/plural-rules - // > other (required—general plural form — also used if the language only has a single form) - default: - return Plural.Other; - } -} diff --git a/packages/common/src/pipes/date_pipe.ts b/packages/common/src/pipes/date_pipe.ts index aec4cee8355e7c..2129c06d08a597 100644 --- a/packages/common/src/pipes/date_pipe.ts +++ b/packages/common/src/pipes/date_pipe.ts @@ -7,66 +7,118 @@ */ import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; -import {DateFormatter} from './intl'; +import {formatDate} from '../i18n/format_date'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; -import {isNumeric} from './number_pipe'; -const ISO8601_DATE_REGEX = +export const ISO8601_DATE_REGEX = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; // 1 2 3 4 5 6 7 8 9 10 11 +// clang-format off /** * @ngModule CommonModule * @whatItDoes Formats a date according to locale rules. - * @howToUse `date_expression | date[:format]` + * @howToUse `date_expression | date[:format[:timezone[:locale]]]` * @description * * Where: * - `expression` is a date object or a number (milliseconds since UTC epoch) or an ISO string * (https://www.w3.org/TR/NOTE-datetime). * - `format` indicates which date/time components to include. The format can be predefined as - * shown below or custom as shown in the table. - * - `'medium'`: equivalent to `'yMMMdjms'` (e.g. `Sep 3, 2010, 12:05:08 PM` for `en-US`) - * - `'short'`: equivalent to `'yMdjm'` (e.g. `9/3/2010, 12:05 PM` for `en-US`) - * - `'fullDate'`: equivalent to `'yMMMMEEEEd'` (e.g. `Friday, September 3, 2010` for `en-US`) - * - `'longDate'`: equivalent to `'yMMMMd'` (e.g. `September 3, 2010` for `en-US`) - * - `'mediumDate'`: equivalent to `'yMMMd'` (e.g. `Sep 3, 2010` for `en-US`) - * - `'shortDate'`: equivalent to `'yMd'` (e.g. `9/3/2010` for `en-US`) - * - `'mediumTime'`: equivalent to `'jms'` (e.g. `12:05:08 PM` for `en-US`) - * - `'shortTime'`: equivalent to `'jm'` (e.g. `12:05 PM` for `en-US`) + * shown below (all examples are given for `en-US`) or custom as shown in the table. + * - `'short'`: equivalent to `'M/d/yy, h:mm a'` (e.g. `6/15/15, 9:03 AM`) + * - `'medium'`: equivalent to `'MMM d, y, h:mm:ss a'` (e.g. `Jun 15, 2015, 9:03:01 AM`) + * - `'long'`: equivalent to `'MMMM d, y, h:mm:ss a z'` (e.g. `June 15, 2015 at 9:03:01 AM GMT+1`) + * - `'full'`: equivalent to `'EEEE, MMMM d, y, h:mm:ss a zzzz'` (e.g. `Monday, June 15, 2015 at + * 9:03:01 AM GMT+01:00`) + * - `'shortDate'`: equivalent to `'M/d/yy'` (e.g. `6/15/15`) + * - `'mediumDate'`: equivalent to `'MMM d, y'` (e.g. `Jun 15, 2015`) + * - `'longDate'`: equivalent to `'MMMM d, y'` (e.g. `June 15, 2015`) + * - `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` (e.g. `Monday, June 15, 2015`) + * - `'shortTime'`: equivalent to `'h:mm a'` (e.g. `9:03 AM`) + * - `'mediumTime'`: equivalent to `'h:mm:ss a'` (e.g. `9:03:01 AM`) + * - `'longTime'`: equivalent to `'h:mm:ss a z'` (e.g. `9:03:01 AM GMT+1`) + * - `'fullTime'`: equivalent to `'h:mm:ss a zzzz'` (e.g. `9:03:01 AM GMT+01:00`) + * - `timezone` to be used for formatting. It understands UTC/GMT and the continental US time zone + * abbreviations, but for general use, use a time zone offset, for example, + * `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) + * If not specified, the local system timezone of the end-user's browser will be used. + * - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by + * default) * * - * | Component | Symbol | Narrow | Short Form | Long Form | Numeric | 2-digit | - * |-----------|:------:|--------|--------------|-------------------|-----------|-----------| - * | era | G | G (A) | GGG (AD) | GGGG (Anno Domini)| - | - | - * | year | y | - | - | - | y (2015) | yy (15) | - * | month | M | L (S) | MMM (Sep) | MMMM (September) | M (9) | MM (09) | - * | day | d | - | - | - | d (3) | dd (03) | - * | weekday | E | E (S) | EEE (Sun) | EEEE (Sunday) | - | - | - * | hour | j | - | - | - | j (1 PM) | jj (1 PM) | - * | hour12 | h | - | - | - | h (1) | hh (01) | - * | hour24 | H | - | - | - | H (13) | HH (13) | - * | minute | m | - | - | - | m (5) | mm (05) | - * | second | s | - | - | - | s (9) | ss (09) | - * | timezone | z | - | - | z (Pacific Standard Time)| - | - | - * | timezone | Z | - | Z (GMT-8:00) | - | - | - | - * | timezone | a | - | a (PM) | - | - | - | + * | Field Type | Format | Description | Example Value | + * |--------------------|-------------|---------------------------------------------------------------|------------------------------------------------------------| + * | Era | G, GG & GGG | Abbreviated | AD | + * | | GGGG | Wide | Anno Domini | + * | | GGGGG | Narrow | A | + * | Year | y | Numeric: minimum digits | 2, 20, 201, 2017, 20173 | + * | | yy | Numeric: 2 digits + zero padded | 02, 20, 01, 17, 73 | + * | | yyy | Numeric: 3 digits + zero padded | 002, 020, 201, 2017, 20173 | + * | | yyyy | Numeric: 4 digits or more + zero padded | 0002, 0020, 0201, 2017, 20173 | + * | Month | M | Numeric: 1 digit | 9, 12 | + * | | MM | Numeric: 2 digits + zero padded | 09, 12 | + * | | MMM | Abbreviated | Sep | + * | | MMMM | Wide | September | + * | | MMMMM | Narrow | S | + * | Month standalone | L | Numeric: 1 digit | 9, 12 | + * | | LL | Numeric: 2 digits + zero padded | 09, 12 | + * | | LLL | Abbreviated | Sep | + * | | LLLL | Wide | September | + * | | LLLLL | Narrow | S | + * | Week of year | w | Numeric: minimum digits | 1... 53 | + * | | ww | Numeric: 2 digits + zero padded | 01... 53 | + * | Week of month | W | Numeric: 1 digit | 1... 5 | + * | Day of month | d | Numeric: minimum digits | 1 | + * | | dd | Numeric: 2 digits + zero padded | 1 | + * | Week day | E, EE & EEE | Abbreviated | Tue | + * | | EEEE | Wide | Tuesday | + * | | EEEEE | Narrow | T | + * | | EEEEEE | Short | Tu | + * | Period | a, aa & aaa | Abbreviated | am/pm or AM/PM | + * | | aaaa | Wide (fallback to `a` when missing) | ante meridiem/post meridiem | + * | | aaaaa | Narrow | a/p | + * | Period* | B, BB & BBB | Abbreviated | mid. | + * | | BBBB | Wide | am, pm, midnight, noon, morning, afternoon, evening, night | + * | | BBBBB | Narrow | md | + * | Period standalone* | b, bb & bbb | Abbreviated | mid. | + * | | bbbb | Wide | am, pm, midnight, noon, morning, afternoon, evening, night | + * | | bbbbb | Narrow | md | + * | Hour 1-12 | h | Numeric: minimum digits | 1, 12 | + * | | hh | Numeric: 2 digits + zero padded | 01, 12 | + * | Hour 0-23 | H | Numeric: minimum digits | 0, 23 | + * | | HH | Numeric: 2 digits + zero padded | 00, 23 | + * | Minute | m | Numeric: minimum digits | 8, 59 | + * | | mm | Numeric: 2 digits + zero padded | 08, 59 | + * | Second | s | Numeric: minimum digits | 0... 59 | + * | | ss | Numeric: 2 digits + zero padded | 00... 59 | + * | Fractional seconds | S | Numeric: 1 digit | 0... 9 | + * | | SS | Numeric: 2 digits + zero padded | 00... 99 | + * | | SSS | Numeric: 3 digits + zero padded (= milliseconds) | 000... 999 | + * | Zone | z, zz & zzz | Short specific non location format (fallback to O) | GMT-8 | + * | | zzzz | Long specific non location format (fallback to OOOO) | GMT-08:00 | + * | | Z, ZZ & ZZZ | ISO8601 basic format | -0800 | + * | | ZZZZ | Long localized GMT format | GMT-8:00 | + * | | ZZZZZ | ISO8601 extended format + Z indicator for offset 0 (= XXXXX) | -08:00 | + * | | O, OO & OOO | Short localized GMT format | GMT-8 | + * | | OOOO | Long localized GMT format | GMT-08:00 | * - * In javascript, only the components specified will be respected (not the ordering, - * punctuations, ...) and details of the formatting will be dependent on the locale. - * - * Timezone of the formatted text will be the local system timezone of the end-user's machine. * * When the expression is a ISO string without time (e.g. 2016-09-19) the time zone offset is not * applied and the formatted text will have the same day, month and year of the expression. * * WARNINGS: + * - this pipe has only access to en-US locale data by default. If you want to localize the dates + * in another language, you will have to import data for other locales. + * See the {@linkDocs guide/i18n#i18n-pipes "I18n guide"} to know how to import additional locale + * data. + * - Fields suffixed with * are only available in the extra dataset. + * See the {@linkDocs guide/i18n#i18n-pipes "I18n guide"} to know how to import extra locale + * data. * - this pipe is marked as pure hence it will not be re-evaluated when the input is mutated. * Instead users should treat the date as an immutable object and change the reference when the * pipe needs to re-run (this is to avoid reformatting the date on every change detection run * which would be an expensive operation). - * - this pipe uses the Internationalization API. Therefore it is only reliable in Chrome and Opera - * browsers. * * ### Examples * @@ -77,41 +129,29 @@ const ISO8601_DATE_REGEX = * {{ dateObj | date }} // output is 'Jun 15, 2015' * {{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM' * {{ dateObj | date:'shortTime' }} // output is '9:43 PM' - * {{ dateObj | date:'mmss' }} // output is '43:11' + * {{ dateObj | date:'hh:mm:ss a' }} // output is '09:43:11 PM' * ``` * * {@example common/pipes/ts/date_pipe.ts region='DatePipe'} * * @stable */ +// clang-format on @Pipe({name: 'date', pure: true}) export class DatePipe implements PipeTransform { - /** @internal */ - static _ALIASES: {[key: string]: string} = { - 'medium': 'yMMMdjms', - 'short': 'yMdjm', - 'fullDate': 'yMMMMEEEEd', - 'longDate': 'yMMMMd', - 'mediumDate': 'yMMMd', - 'shortDate': 'yMd', - 'mediumTime': 'jms', - 'shortTime': 'jm' - }; - - constructor(@Inject(LOCALE_ID) private _locale: string) {} + constructor(@Inject(LOCALE_ID) private locale: string) {} - transform(value: any, pattern: string = 'mediumDate'): string|null { - let date: Date; - - if (isBlank(value) || value !== value) return null; + transform(value: any, format = 'mediumDate', timezone?: string, locale?: string): string|null { + if (value == null || value === '' || value !== value) return null; if (typeof value === 'string') { value = value.trim(); } + let date: Date; if (isDate(value)) { date = value; - } else if (isNumeric(value)) { + } else if (!isNaN(value - parseFloat(value))) { date = new Date(parseFloat(value)); } else if (typeof value === 'string' && /^(\d{4}-\d{1,2}-\d{1,2})$/.test(value)) { /** @@ -123,7 +163,7 @@ export class DatePipe implements PipeTransform { * is applied * Note: ISO months are 0 for January, 1 for February, ... */ - const [y, m, d] = value.split('-').map((val: string) => parseInt(val, 10)); + const [y, m, d] = value.split('-').map((val: string) => +val); date = new Date(y, m - 1, d); } else { date = new Date(value); @@ -138,19 +178,12 @@ export class DatePipe implements PipeTransform { } } - return DateFormatter.format(date, this._locale, DatePipe._ALIASES[pattern] || pattern); + return formatDate(date, format, locale || this.locale, timezone); } } -function isBlank(obj: any): boolean { - return obj == null || obj === ''; -} - -function isDate(obj: any): obj is Date { - return obj instanceof Date && !isNaN(obj.valueOf()); -} - -function isoStringToDate(match: RegExpMatchArray): Date { +/** @internal */ +export function isoStringToDate(match: RegExpMatchArray): Date { const date = new Date(0); let tzHour = 0; let tzMin = 0; @@ -158,18 +191,18 @@ function isoStringToDate(match: RegExpMatchArray): Date { const timeSetter = match[8] ? date.setUTCHours : date.setHours; if (match[9]) { - tzHour = toInt(match[9] + match[10]); - tzMin = toInt(match[9] + match[11]); + tzHour = +(match[9] + match[10]); + tzMin = +(match[9] + match[11]); } - dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); - const h = toInt(match[4] || '0') - tzHour; - const m = toInt(match[5] || '0') - tzMin; - const s = toInt(match[6] || '0'); + dateSetter.call(date, +(match[1]), +(match[2]) - 1, +(match[3])); + const h = +(match[4] || '0') - tzHour; + const m = +(match[5] || '0') - tzMin; + const s = +(match[6] || '0'); const ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); timeSetter.call(date, h, m, s, ms); return date; } -function toInt(str: string): number { - return parseInt(str, 10); +function isDate(value: any): value is Date { + return value instanceof Date && !isNaN(value.valueOf()); } diff --git a/packages/common/src/pipes/deprecated/date_pipe.ts b/packages/common/src/pipes/deprecated/date_pipe.ts new file mode 100644 index 00000000000000..2b500897cac5f0 --- /dev/null +++ b/packages/common/src/pipes/deprecated/date_pipe.ts @@ -0,0 +1,145 @@ +/** +* @license +* Copyright Google Inc. All Rights Reserved. +* +* Use of this source code is governed by an MIT-style license that can be +* found in the LICENSE file at https://angular.io/license + */ + +import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; +import {ISO8601_DATE_REGEX, isoStringToDate} from '../date_pipe'; +import {invalidPipeArgumentError} from '../invalid_pipe_argument_error'; +import {DateFormatter} from './intl'; + +/** + * @ngModule CommonModule + * @whatItDoes Formats a date according to locale rules. + * @howToUse `date_expression | date[:format]` + * @description + * + * Where: + * - `expression` is a date object or a number (milliseconds since UTC epoch) or an ISO string + * (https://www.w3.org/TR/NOTE-datetime). + * - `format` indicates which date/time components to include. The format can be predefined as + * shown below or custom as shown in the table. + * - `'medium'`: equivalent to `'yMMMdjms'` (e.g. `Sep 3, 2010, 12:05:08 PM` for `en-US`) + * - `'short'`: equivalent to `'yMdjm'` (e.g. `9/3/2010, 12:05 PM` for `en-US`) + * - `'fullDate'`: equivalent to `'yMMMMEEEEd'` (e.g. `Friday, September 3, 2010` for `en-US`) + * - `'longDate'`: equivalent to `'yMMMMd'` (e.g. `September 3, 2010` for `en-US`) + * - `'mediumDate'`: equivalent to `'yMMMd'` (e.g. `Sep 3, 2010` for `en-US`) + * - `'shortDate'`: equivalent to `'yMd'` (e.g. `9/3/2010` for `en-US`) + * - `'mediumTime'`: equivalent to `'jms'` (e.g. `12:05:08 PM` for `en-US`) + * - `'shortTime'`: equivalent to `'jm'` (e.g. `12:05 PM` for `en-US`) + * + * + * | Component | Symbol | Narrow | Short Form | Long Form | Numeric | 2-digit | + * |-----------|:------:|--------|--------------|-------------------|-----------|-----------| + * | era | G | G (A) | GGG (AD) | GGGG (Anno Domini)| - | - | + * | year | y | - | - | - | y (2015) | yy (15) | + * | month | M | L (S) | MMM (Sep) | MMMM (September) | M (9) | MM (09) | + * | day | d | - | - | - | d (3) | dd (03) | + * | weekday | E | E (S) | EEE (Sun) | EEEE (Sunday) | - | - | + * | hour | j | - | - | - | j (13) | jj (13) | + * | hour12 | h | - | - | - | h (1 PM) | hh (01 PM)| + * | hour24 | H | - | - | - | H (13) | HH (13) | + * | minute | m | - | - | - | m (5) | mm (05) | + * | second | s | - | - | - | s (9) | ss (09) | + * | timezone | z | - | - | z (Pacific Standard Time)| - | - | + * | timezone | Z | - | Z (GMT-8:00) | - | - | - | + * | timezone | a | - | a (PM) | - | - | - | + * + * In javascript, only the components specified will be respected (not the ordering, + * punctuations, ...) and details of the formatting will be dependent on the locale. + * + * Timezone of the formatted text will be the local system timezone of the end-user's machine. + * + * When the expression is a ISO string without time (e.g. 2016-09-19) the time zone offset is not + * applied and the formatted text will have the same day, month and year of the expression. + * + * WARNINGS: + * - this pipe is marked as pure hence it will not be re-evaluated when the input is mutated. + * Instead users should treat the date as an immutable object and change the reference when the + * pipe needs to re-run (this is to avoid reformatting the date on every change detection run + * which would be an expensive operation). + * - this pipe uses the Internationalization API. Therefore it is only reliable in Chrome and Opera + * browsers. + * + * ### Examples + * + * Assuming `dateObj` is (year: 2015, month: 6, day: 15, hour: 21, minute: 43, second: 11) + * in the _local_ time and locale is 'en-US': + * + * ``` + * {{ dateObj | date }} // output is 'Jun 15, 2015' + * {{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM' + * {{ dateObj | date:'shortTime' }} // output is '9:43 PM' + * {{ dateObj | date:'mmss' }} // output is '43:11' + * ``` + * + * {@example common/pipes/ts/date_pipe.ts region='DatePipe'} + * + * @stable + */ +@Pipe({name: 'date', pure: true}) +export class DeprecatedDatePipe implements PipeTransform { + /** @internal */ + static _ALIASES: {[key: string]: string} = { + 'medium': 'yMMMdjms', + 'short': 'yMdjm', + 'fullDate': 'yMMMMEEEEd', + 'longDate': 'yMMMMd', + 'mediumDate': 'yMMMd', + 'shortDate': 'yMd', + 'mediumTime': 'jms', + 'shortTime': 'jm' + }; + + constructor(@Inject(LOCALE_ID) private _locale: string) {} + + transform(value: any, pattern: string = 'mediumDate'): string|null { + if (value == null || value === '' || value !== value) return null; + + let date: Date; + + if (typeof value === 'string') { + value = value.trim(); + } + + if (isDate(value)) { + date = value; + } else if (!isNaN(value - parseFloat(value))) { + date = new Date(parseFloat(value)); + } else if (typeof value === 'string' && /^(\d{4}-\d{1,2}-\d{1,2})$/.test(value)) { + /** + * For ISO Strings without time the day, month and year must be extracted from the ISO String + * before Date creation to avoid time offset and errors in the new Date. + * If we only replace '-' with ',' in the ISO String ("2015,01,01"), and try to create a new + * date, some browsers (e.g. IE 9) will throw an invalid Date error + * If we leave the '-' ("2015-01-01") and try to create a new Date("2015-01-01") the + * timeoffset + * is applied + * Note: ISO months are 0 for January, 1 for February, ... + */ + const [y, m, d] = value.split('-').map((val: string) => parseInt(val, 10)); + date = new Date(y, m - 1, d); + } else { + date = new Date(value); + } + + if (!isDate(date)) { + let match: RegExpMatchArray|null; + if ((typeof value === 'string') && (match = value.match(ISO8601_DATE_REGEX))) { + date = isoStringToDate(match); + } else { + throw invalidPipeArgumentError(DeprecatedDatePipe, value); + } + } + + return DateFormatter.format( + date, this._locale, DeprecatedDatePipe._ALIASES[pattern] || pattern); + } +} + +function isDate(value: any): value is Date { + return value instanceof Date && !isNaN(value.valueOf()); +} diff --git a/packages/common/src/pipes/deprecated/index.ts b/packages/common/src/pipes/deprecated/index.ts new file mode 100644 index 00000000000000..935583234bcbc1 --- /dev/null +++ b/packages/common/src/pipes/deprecated/index.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Provider} from '@angular/core'; +import {DeprecatedDatePipe} from './date_pipe'; +import {DeprecatedCurrencyPipe, DeprecatedDecimalPipe, DeprecatedPercentPipe} from './number_pipe'; + +export { + DeprecatedCurrencyPipe, + DeprecatedDatePipe, + DeprecatedDecimalPipe, + DeprecatedPercentPipe, +}; + + +/** + * A collection of deprecated i18n pipes that require intl api + * + * @deprecated + */ +export const COMMON_DEPRECATED_I18N_PIPES: Provider[] = + [DeprecatedDecimalPipe, DeprecatedPercentPipe, DeprecatedCurrencyPipe, DeprecatedDatePipe]; diff --git a/packages/common/src/pipes/intl.ts b/packages/common/src/pipes/deprecated/intl.ts similarity index 99% rename from packages/common/src/pipes/intl.ts rename to packages/common/src/pipes/deprecated/intl.ts index 483e89fd8c84c8..ff7fa9808a9ed5 100644 --- a/packages/common/src/pipes/intl.ts +++ b/packages/common/src/pipes/deprecated/intl.ts @@ -5,12 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -export enum NumberFormatStyle { - Decimal, - Percent, - Currency, -} +import {NumberFormatStyle} from '../../i18n/locale_data_api'; export class NumberFormatter { static format(num: number, locale: string, style: NumberFormatStyle, opts: { diff --git a/packages/common/src/pipes/deprecated/number_pipe.ts b/packages/common/src/pipes/deprecated/number_pipe.ts new file mode 100644 index 00000000000000..c2b2aa62a86b8d --- /dev/null +++ b/packages/common/src/pipes/deprecated/number_pipe.ts @@ -0,0 +1,164 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, LOCALE_ID, Pipe, PipeTransform, Type} from '@angular/core'; +import {NUMBER_FORMAT_REGEXP, parseIntAutoRadix} from '../../i18n/format_number'; +import {NumberFormatStyle} from '../../i18n/locale_data_api'; +import {invalidPipeArgumentError} from '../invalid_pipe_argument_error'; +import {NumberFormatter} from './intl'; + +function formatNumber( + pipe: Type, locale: string, value: number | string, style: NumberFormatStyle, + digits?: string | null, currency: string | null = null, + currencyAsSymbol: boolean = false): string|null { + if (value == null) return null; + + // Convert strings to numbers + value = typeof value === 'string' && !isNaN(+value - parseFloat(value)) ? +value : value; + if (typeof value !== 'number') { + throw invalidPipeArgumentError(pipe, value); + } + + let minInt: number|undefined; + let minFraction: number|undefined; + let maxFraction: number|undefined; + if (style !== NumberFormatStyle.Currency) { + // rely on Intl default for currency + minInt = 1; + minFraction = 0; + maxFraction = 3; + } + + if (digits) { + const parts = digits.match(NUMBER_FORMAT_REGEXP); + if (parts === null) { + throw new Error(`${digits} is not a valid digit info for number pipes`); + } + if (parts[1] != null) { // min integer digits + minInt = parseIntAutoRadix(parts[1]); + } + if (parts[3] != null) { // min fraction digits + minFraction = parseIntAutoRadix(parts[3]); + } + if (parts[5] != null) { // max fraction digits + maxFraction = parseIntAutoRadix(parts[5]); + } + } + + return NumberFormatter.format(value as number, locale, style, { + minimumIntegerDigits: minInt, + minimumFractionDigits: minFraction, + maximumFractionDigits: maxFraction, + currency: currency, + currencyAsSymbol: currencyAsSymbol, + }); +} + +/** + * @ngModule CommonModule + * @whatItDoes Formats a number according to locale rules. + * @howToUse `number_expression | number[:digitInfo]` + * + * Formats a number as text. Group sizing and separator and other locale-specific + * configurations are based on the active locale. + * + * where `expression` is a number: + * - `digitInfo` is a `string` which has a following format:
+ * {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits} + * - `minIntegerDigits` is the minimum number of integer digits to use. Defaults to `1`. + * - `minFractionDigits` is the minimum number of digits after fraction. Defaults to `0`. + * - `maxFractionDigits` is the maximum number of digits after fraction. Defaults to `3`. + * + * For more information on the acceptable range for each of these numbers and other + * details see your native internationalization library. + * + * WARNING: this pipe uses the Internationalization API which is not yet available in all browsers + * and may require a polyfill. See [Browser Support](guide/browser-support) for details. + * + * ### Example + * + * {@example common/pipes/ts/number_pipe.ts region='NumberPipe'} + * + * @stable + */ +@Pipe({name: 'number'}) +export class DeprecatedDecimalPipe implements PipeTransform { + constructor(@Inject(LOCALE_ID) private _locale: string) {} + + transform(value: any, digits?: string): string|null { + return formatNumber( + DeprecatedDecimalPipe, this._locale, value, NumberFormatStyle.Decimal, digits); + } +} + +/** + * @ngModule CommonModule + * @whatItDoes Formats a number as a percentage according to locale rules. + * @howToUse `number_expression | percent[:digitInfo]` + * + * @description + * + * Formats a number as percentage. + * + * - `digitInfo` See {@link DecimalPipe} for detailed description. + * + * WARNING: this pipe uses the Internationalization API which is not yet available in all browsers + * and may require a polyfill. See [Browser Support](guide/browser-support) for details. + * + * ### Example + * + * {@example common/pipes/ts/number_pipe.ts region='PercentPipe'} + * + * @stable + */ +@Pipe({name: 'percent'}) +export class DeprecatedPercentPipe implements PipeTransform { + constructor(@Inject(LOCALE_ID) private _locale: string) {} + + transform(value: any, digits?: string): string|null { + return formatNumber( + DeprecatedPercentPipe, this._locale, value, NumberFormatStyle.Percent, digits); + } +} + +/** + * @ngModule CommonModule + * @whatItDoes Formats a number as currency using locale rules. + * @howToUse `number_expression | currency[:currencyCode[:symbolDisplay[:digitInfo]]]` + * @description + * + * Use `currency` to format a number as currency. + * + * - `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. + * - `symbolDisplay` is a boolean indicating whether to use the currency symbol or code. + * - `true`: use symbol (e.g. `$`). + * - `false`(default): use code (e.g. `USD`). + * - `digitInfo` See {@link DecimalPipe} for detailed description. + * + * WARNING: this pipe uses the Internationalization API which is not yet available in all browsers + * and may require a polyfill. See [Browser Support](guide/browser-support) for details. + * + * ### Example + * + * {@example common/pipes/ts/number_pipe.ts region='CurrencyPipe'} + * + * @stable + */ +@Pipe({name: 'currency'}) +export class DeprecatedCurrencyPipe implements PipeTransform { + constructor(@Inject(LOCALE_ID) private _locale: string) {} + + transform( + value: any, currencyCode: string = 'USD', symbolDisplay: boolean = false, + digits?: string): string|null { + return formatNumber( + DeprecatedCurrencyPipe, this._locale, value, NumberFormatStyle.Currency, digits, + currencyCode, symbolDisplay); + } +} diff --git a/packages/common/src/pipes/i18n_plural_pipe.ts b/packages/common/src/pipes/i18n_plural_pipe.ts index dc0909d1a5e16e..0d3ca1b65ea065 100644 --- a/packages/common/src/pipes/i18n_plural_pipe.ts +++ b/packages/common/src/pipes/i18n_plural_pipe.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Pipe, PipeTransform} from '@angular/core'; -import {NgLocalization, getPluralCategory} from '../localization'; +import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; + +import {NgLocalization, getPluralCategory} from '../i18n/localization'; + import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; const _INTERPOLATION_REGEXP: RegExp = /#/g; @@ -15,13 +17,15 @@ const _INTERPOLATION_REGEXP: RegExp = /#/g; /** * @ngModule CommonModule * @whatItDoes Maps a value to a string that pluralizes the value according to locale rules. - * @howToUse `expression | i18nPlural:mapping` + * @howToUse `expression | i18nPlural:mapping[:locale]` * @description * * Where: * - `expression` is a number. * - `mapping` is an object that mimics the ICU format, see * http://userguide.icu-project.org/formatparse/messages + * - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by + * default) * * ## Example * @@ -33,14 +37,14 @@ const _INTERPOLATION_REGEXP: RegExp = /#/g; export class I18nPluralPipe implements PipeTransform { constructor(private _localization: NgLocalization) {} - transform(value: number, pluralMap: {[count: string]: string}): string { + transform(value: number, pluralMap: {[count: string]: string}, locale?: string): string { if (value == null) return ''; if (typeof pluralMap !== 'object' || pluralMap === null) { throw invalidPipeArgumentError(I18nPluralPipe, pluralMap); } - const key = getPluralCategory(value, Object.keys(pluralMap), this._localization); + const key = getPluralCategory(value, Object.keys(pluralMap), this._localization, locale); return pluralMap[key].replace(_INTERPOLATION_REGEXP, value.toString()); } diff --git a/packages/common/src/pipes/number_pipe.ts b/packages/common/src/pipes/number_pipe.ts index ae170ddc5f6eb7..1738a269794973 100644 --- a/packages/common/src/pipes/number_pipe.ts +++ b/packages/common/src/pipes/number_pipe.ts @@ -6,63 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, LOCALE_ID, Pipe, PipeTransform, Type} from '@angular/core'; -import {NumberFormatStyle, NumberFormatter} from './intl'; +import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; +import {formatNumber} from '../i18n/format_number'; +import {NumberFormatStyle, findCurrencySymbol, getLocaleCurrencyName, getLocaleCurrencySymbol} from '../i18n/locale_data_api'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; -const _NUMBER_FORMAT_REGEXP = /^(\d+)?\.((\d+)(-(\d+))?)?$/; - -function formatNumber( - pipe: Type, locale: string, value: number | string, style: NumberFormatStyle, - digits?: string | null, currency: string | null = null, - currencyAsSymbol: boolean = false): string|null { - if (value == null) return null; - - // Convert strings to numbers - value = typeof value === 'string' && isNumeric(value) ? +value : value; - if (typeof value !== 'number') { - throw invalidPipeArgumentError(pipe, value); - } - - let minInt: number|undefined = undefined; - let minFraction: number|undefined = undefined; - let maxFraction: number|undefined = undefined; - if (style !== NumberFormatStyle.Currency) { - // rely on Intl default for currency - minInt = 1; - minFraction = 0; - maxFraction = 3; - } - - if (digits) { - const parts = digits.match(_NUMBER_FORMAT_REGEXP); - if (parts === null) { - throw new Error(`${digits} is not a valid digit info for number pipes`); - } - if (parts[1] != null) { // min integer digits - minInt = parseIntAutoRadix(parts[1]); - } - if (parts[3] != null) { // min fraction digits - minFraction = parseIntAutoRadix(parts[3]); - } - if (parts[5] != null) { // max fraction digits - maxFraction = parseIntAutoRadix(parts[5]); - } - } - - return NumberFormatter.format(value as number, locale, style, { - minimumIntegerDigits: minInt, - minimumFractionDigits: minFraction, - maximumFractionDigits: maxFraction, - currency: currency, - currencyAsSymbol: currencyAsSymbol, - }); -} - /** * @ngModule CommonModule * @whatItDoes Formats a number according to locale rules. - * @howToUse `number_expression | number[:digitInfo]` + * @howToUse `number_expression | number[:digitInfo[:locale]]` * * Formats a number as text. Group sizing and separator and other locale-specific * configurations are based on the active locale. @@ -73,13 +25,12 @@ function formatNumber( * - `minIntegerDigits` is the minimum number of integer digits to use. Defaults to `1`. * - `minFractionDigits` is the minimum number of digits after fraction. Defaults to `0`. * - `maxFractionDigits` is the maximum number of digits after fraction. Defaults to `3`. + * - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by + * default) * * For more information on the acceptable range for each of these numbers and other * details see your native internationalization library. * - * WARNING: this pipe uses the Internationalization API which is not yet available in all browsers - * and may require a polyfill. See [Browser Support](guide/browser-support) for details. - * * ### Example * * {@example common/pipes/ts/number_pipe.ts region='NumberPipe'} @@ -90,24 +41,33 @@ function formatNumber( export class DecimalPipe implements PipeTransform { constructor(@Inject(LOCALE_ID) private _locale: string) {} - transform(value: any, digits?: string): string|null { - return formatNumber(DecimalPipe, this._locale, value, NumberFormatStyle.Decimal, digits); + transform(value: any, digits?: 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(CurrencyPipe, error); + } + + return str; } } /** * @ngModule CommonModule * @whatItDoes Formats a number as a percentage according to locale rules. - * @howToUse `number_expression | percent[:digitInfo]` + * @howToUse `number_expression | percent[:digitInfo[:locale]]` * * @description * * Formats a number as percentage. * * - `digitInfo` See {@link DecimalPipe} for detailed description. - * - * WARNING: this pipe uses the Internationalization API which is not yet available in all browsers - * and may require a polyfill. See [Browser Support](guide/browser-support) for details. + * - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by + * default) * * ### Example * @@ -119,28 +79,40 @@ export class DecimalPipe implements PipeTransform { export class PercentPipe implements PipeTransform { constructor(@Inject(LOCALE_ID) private _locale: string) {} - transform(value: any, digits?: string): string|null { - return formatNumber(PercentPipe, this._locale, value, NumberFormatStyle.Percent, digits); + transform(value: any, digits?: 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(CurrencyPipe, error); + } + + return str; } } /** * @ngModule CommonModule * @whatItDoes Formats a number as currency using locale rules. - * @howToUse `number_expression | currency[:currencyCode[:symbolDisplay[:digitInfo]]]` + * @howToUse `number_expression | currency[:currencyCode[:symbolDisplay[:digitInfo[:locale]]]]` * @description * * Use `currency` to format a number as currency. * * - `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. - * - `symbolDisplay` is a boolean indicating whether to use the currency symbol or code. - * - `true`: use symbol (e.g. `$`). - * - `false`(default): use code (e.g. `USD`). + * - `display` indicates whether to show the currency symbol or the code. + * - `code`(default): use code (e.g. `USD`). + * - `symbol`: 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 `$`). + * If there is no narrow symbol for the chosen currency, the regular symbol will be used. * - `digitInfo` See {@link DecimalPipe} for detailed description. - * - * WARNING: this pipe uses the Internationalization API which is not yet available in all browsers - * and may require a polyfill. See [Browser Support](guide/browser-support) for details. + * - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by + * default) * * ### Example * @@ -153,22 +125,35 @@ export class CurrencyPipe implements PipeTransform { constructor(@Inject(LOCALE_ID) private _locale: string) {} transform( - value: any, currencyCode: string = 'USD', symbolDisplay: boolean = false, - digits?: string): string|null { - return formatNumber( - CurrencyPipe, this._locale, value, NumberFormatStyle.Currency, digits, currencyCode, - symbolDisplay); - } -} + value: any, currencyCode?: string, symbolDisplay: 'code'|'symbol'|'symbol-narrow' = 'symbol', + digits?: string, locale?: string): string|null { + if (isEmpty(value)) return null; + + locale = locale || this._locale; + + if (typeof symbolDisplay === 'boolean') { + if (console && console.warn) { + console.warn( + `Warning: the currency pipe has been changed in Angular v5. The symbolDisplay option (third parameter) is now a string instead of a boolean. The accepted values are "code", "symbol" or "symbol-narrow".`); + } + symbolDisplay = symbolDisplay ? 'symbol' : 'code'; + } + + let currency = currencyCode || 'USD'; + if (symbolDisplay !== 'code') { + currency = findCurrencySymbol(currency, symbolDisplay === 'symbol' ? 'wide' : 'narrow'); + } + + const {str, error} = formatNumber(value, locale, NumberFormatStyle.Currency, digits, currency); + + if (error) { + throw invalidPipeArgumentError(CurrencyPipe, error); + } -function parseIntAutoRadix(text: string): number { - const result: number = parseInt(text); - if (isNaN(result)) { - throw new Error('Invalid integer literal when parsing ' + text); + return str; } - return result; } -export function isNumeric(value: any): boolean { - return !isNaN(value - parseFloat(value)); +function isEmpty(value: any): boolean { + return value == null || value === '' || value !== value; } diff --git a/packages/common/test/i18n/locale_data_api_spec.ts b/packages/common/test/i18n/locale_data_api_spec.ts new file mode 100644 index 00000000000000..44620c022ef842 --- /dev/null +++ b/packages/common/test/i18n/locale_data_api_spec.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import localeCaESVALENCIA from '../../i18n_data/locale_ca-ES-VALENCIA'; +import localeEn from '../../i18n_data/locale_en'; +import localeFr from '../../i18n_data/locale_fr'; +import localeFrCA from '../../i18n_data/locale_fr-CA'; +import {registerLocaleData, findLocaleData} from '../../src/i18n/locale_data_api'; + +export function main() { + describe('locale data api', () => { + beforeAll(() => { + registerLocaleData(localeCaESVALENCIA); + registerLocaleData(localeEn); + registerLocaleData(localeFr); + registerLocaleData(localeFrCA); + }); + + describe('findLocaleData', () => { + it('should throw if the locale provided is not a valid LOCALE_ID', () => { + expect(() => findLocaleData('invalid')) + .toThrow(new Error( + `"invalid" is not a valid LOCALE_ID value. See https://github.com/unicode-cldr/cldr-core/blob/master/availableLocales.json for a list of valid locales`)); + }); + + it('should throw if the LOCALE_DATA for the chosen locale if not available', () => { + expect(() => findLocaleData('fr-BE')) + .toThrowError(/Missing locale data for the locale "fr-BE"/); + }); + + it('should return english data if the locale is en-US', + () => { expect(findLocaleData('en-US')).toEqual(localeEn); }); + + it('should return the exact LOCALE_DATA if it is available', + () => { expect(findLocaleData('fr-CA')).toEqual(localeFrCA); }); + + it('should return the parent LOCALE_DATA if it exists and exact locale is not available', + () => { expect(findLocaleData('fr-FR')).toEqual(localeFr); }); + + it(`should find the LOCALE_DATA even if the locale id is badly formatted`, () => { + expect(findLocaleData('ca-ES-VALENCIA')).toEqual(localeCaESVALENCIA); + expect(findLocaleData('CA_es_Valencia')).toEqual(localeCaESVALENCIA); + }); + }); + }); +} diff --git a/packages/common/test/localization_spec.ts b/packages/common/test/i18n/localization_spec.ts similarity index 93% rename from packages/common/test/localization_spec.ts rename to packages/common/test/i18n/localization_spec.ts index 08e7792e8e4777..069b963492a7b7 100644 --- a/packages/common/test/localization_spec.ts +++ b/packages/common/test/i18n/localization_spec.ts @@ -6,13 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ +import localeRo from '../../i18n_data/locale_ro'; +import localeSr from '../../i18n_data/locale_sr'; +import localeZgh from '../../i18n_data/locale_zgh'; +import localeFr from '../../i18n_data/locale_fr'; import {LOCALE_ID} from '@angular/core'; import {TestBed, inject} from '@angular/core/testing'; - -import {NgLocaleLocalization, NgLocalization, getPluralCategory} from '../src/localization'; +import {NgLocaleLocalization, NgLocalization, getPluralCategory} from '../../src/i18n/localization'; +import {registerLocaleData} from '../../src/i18n/locale_data_api'; export function main() { describe('l10n', () => { + beforeAll(() => { + registerLocaleData(localeRo); + registerLocaleData(localeSr); + registerLocaleData(localeZgh); + registerLocaleData(localeFr); + }); describe('NgLocalization', () => { describe('ro', () => { diff --git a/packages/common/test/pipes/date_pipe_spec.ts b/packages/common/test/pipes/date_pipe_spec.ts index 85a997bcc13c0c..2094d35bd8b854 100644 --- a/packages/common/test/pipes/date_pipe_spec.ts +++ b/packages/common/test/pipes/date_pipe_spec.ts @@ -6,10 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {DatePipe} from '@angular/common'; +import {DatePipe, registerLocaleData} from '@angular/common'; import {PipeResolver} from '@angular/compiler/src/pipe_resolver'; import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector'; -import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; +import localeEn from '../../i18n_data/locale_en'; +import localeEnExtra from '../../i18n_data/extra/locale_en'; +import localeDe from '../../i18n_data/locale_de'; +import localeHu from '../../i18n_data/locale_hu'; +import localeSr from '../../i18n_data/locale_sr'; +import localeTh from '../../i18n_data/locale_th'; export function main() { describe('DatePipe', () => { @@ -22,16 +27,16 @@ export function main() { expect(pipe.transform(date, pattern)).toEqual(output); } - // TODO: reactivate the disabled expectations once emulators are fixed in SauceLabs - // In some old versions of Chrome in Android emulators, time formatting returns dates in the - // timezone of the VM host, - // instead of the device timezone. Same symptoms as - // https://bugs.chromium.org/p/chromium/issues/detail?id=406382 - // This happens locally and in SauceLabs, so some checks are disabled to avoid failures. - // Tracking issue: https://github.com/angular/angular/issues/11187 + beforeAll(() => { + registerLocaleData(localeEn, localeEnExtra); + registerLocaleData(localeDe); + registerLocaleData(localeHu); + registerLocaleData(localeSr); + registerLocaleData(localeTh); + }); beforeEach(() => { - date = new Date(2015, 5, 15, 9, 3, 1); + date = new Date(2015, 5, 15, 9, 3, 1, 550); pipe = new DatePipe('en-US'); }); @@ -67,71 +72,149 @@ export function main() { describe('transform', () => { it('should format each component correctly', () => { const dateFixtures: any = { - 'y': '2015', - 'yy': '15', - 'M': '6', - 'MM': '06', - 'MMM': 'Jun', - 'MMMM': 'June', - 'd': '15', - 'dd': '15', - 'EEE': 'Mon', - 'EEEE': 'Monday' + G: 'AD', + GG: 'AD', + GGG: 'AD', + GGGG: 'Anno Domini', + GGGGG: 'A', + y: '2015', + yy: '15', + yyy: '2015', + yyyy: '2015', + M: '6', + MM: '06', + MMM: 'Jun', + MMMM: 'June', + MMMMM: 'J', + L: '6', + LL: '06', + LLL: 'Jun', + LLLL: 'June', + LLLLL: 'J', + w: '25', + ww: '25', + W: '3', + d: '15', + dd: '15', + E: 'Mon', + EE: 'Mon', + EEE: 'Mon', + EEEE: 'Monday', + EEEEEE: 'Mo', + h: '9', + hh: '09', + H: '9', + HH: '09', + m: '3', + mm: '03', + s: '1', + ss: '01', + S: '6', + SS: '55', + SSS: '550', + a: 'AM', + aa: 'AM', + aaa: 'AM', + aaaa: 'AM', + aaaaa: 'a', + b: 'morning', + bb: 'morning', + bbb: 'morning', + bbbb: 'morning', + bbbbb: 'morning', + B: 'in the morning', + BB: 'in the morning', + BBB: 'in the morning', + BBBB: 'in the morning', + BBBBB: 'in the morning', }; const isoStringWithoutTimeFixtures: any = { - 'y': '2015', - 'yy': '15', - 'M': '1', - 'MM': '01', - 'MMM': 'Jan', - 'MMMM': 'January', - 'd': '1', - 'dd': '01', - 'EEE': 'Thu', - 'EEEE': 'Thursday' + G: 'AD', + GG: 'AD', + GGG: 'AD', + GGGG: 'Anno Domini', + GGGGG: 'A', + y: '2015', + yy: '15', + yyy: '2015', + yyyy: '2015', + M: '1', + MM: '01', + MMM: 'Jan', + MMMM: 'January', + MMMMM: 'J', + L: '1', + LL: '01', + LLL: 'Jan', + LLLL: 'January', + LLLLL: 'J', + w: '1', + ww: '01', + W: '1', + d: '1', + dd: '01', + E: 'Thu', + EE: 'Thu', + EEE: 'Thu', + EEEE: 'Thursday', + EEEEE: 'T', + EEEEEE: 'Th', + h: '12', + hh: '12', + H: '0', + HH: '00', + m: '0', + mm: '00', + s: '0', + ss: '00', + S: '0', + SS: '00', + SSS: '000', + a: 'AM', + aa: 'AM', + aaa: 'AM', + aaaa: 'AM', + aaaaa: 'a', + b: 'midnight', + bb: 'midnight', + bbb: 'midnight', + bbbb: 'midnight', + bbbbb: 'midnight', + B: 'midnight', + BB: 'midnight', + BBB: 'midnight', + BBBB: 'midnight', + BBBBB: 'mi', }; - if (!browserDetection.isOldChrome) { - dateFixtures['h'] = '9'; - dateFixtures['hh'] = '09'; - dateFixtures['j'] = '9 AM'; - isoStringWithoutTimeFixtures['h'] = '12'; - isoStringWithoutTimeFixtures['hh'] = '12'; - isoStringWithoutTimeFixtures['j'] = '12 AM'; - } - - // IE and Edge can't format a date to minutes and seconds without hours - if (!browserDetection.isEdge && !browserDetection.isIE || - !browserDetection.supportsNativeIntlApi) { - if (!browserDetection.isOldChrome) { - dateFixtures['HH'] = '09'; - isoStringWithoutTimeFixtures['HH'] = '00'; - } - dateFixtures['E'] = 'M'; - dateFixtures['L'] = 'J'; - dateFixtures['m'] = '3'; - dateFixtures['s'] = '1'; - dateFixtures['mm'] = '03'; - dateFixtures['ss'] = '01'; - isoStringWithoutTimeFixtures['m'] = '0'; - isoStringWithoutTimeFixtures['s'] = '0'; - isoStringWithoutTimeFixtures['mm'] = '00'; - isoStringWithoutTimeFixtures['ss'] = '00'; - } - Object.keys(dateFixtures).forEach((pattern: string) => { expectDateFormatAs(date, pattern, dateFixtures[pattern]); }); - if (!browserDetection.isOldChrome) { - Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => { - expectDateFormatAs( - isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]); - }); - } + Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => { + expectDateFormatAs(isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]); + }); + }); - expect(pipe.transform(date, 'Z')).toBeDefined(); + it('should format with timezones', () => { + const dateFixtures: any = { + z: /GMT(\+|-)\d/, + zz: /GMT(\+|-)\d/, + zzz: /GMT(\+|-)\d/, + zzzz: /GMT(\+|-)\d{2}\:30/, + Z: /(\+|-)\d{2}30/, + ZZ: /(\+|-)\d{2}30/, + ZZZ: /(\+|-)\d{2}30/, + ZZZZ: /GMT(\+|-)\d{2}\:30/, + ZZZZZ: /(\+|-)\d{2}\:30/, + O: /GMT(\+|-)\d/, + OOOO: /GMT(\+|-)\d{2}\:30/, + }; + + Object.keys(dateFixtures).forEach((pattern: string) => { + expect(pipe.transform(date, pattern, '+0430')).toMatch(dateFixtures[pattern]); + }); }); it('should format common multi component patterns', () => { @@ -144,19 +227,13 @@ export function main() { 'yMEEEd': '20156Mon15', 'MEEEd': '6Mon15', 'MMMd': 'Jun15', - 'yMMMMEEEEd': 'Monday, June 15, 2015' + 'EEEE, MMMM d, y': 'Monday, June 15, 2015', + 'H:mm a': '9:03 AM', + 'ms': '31', + 'MM/dd/yy hh:mm': '06/15/15 09:03', + 'MM/dd/y': '06/15/2015' }; - // IE and Edge can't format a date to minutes and seconds without hours - if (!browserDetection.isEdge && !browserDetection.isIE || - !browserDetection.supportsNativeIntlApi) { - dateFixtures['ms'] = '31'; - } - - if (!browserDetection.isOldChrome) { - dateFixtures['jm'] = '9:03 AM'; - } - Object.keys(dateFixtures).forEach((pattern: string) => { expectDateFormatAs(date, pattern, dateFixtures[pattern]); }); @@ -166,33 +243,23 @@ export function main() { it('should format with pattern aliases', () => { const dateFixtures: any = { 'MM/dd/yyyy': '06/15/2015', - 'fullDate': 'Monday, June 15, 2015', - 'longDate': 'June 15, 2015', - 'mediumDate': 'Jun 15, 2015', - 'shortDate': '6/15/2015' + shortDate: '6/15/15', + mediumDate: 'Jun 15, 2015', + longDate: 'June 15, 2015', + fullDate: 'Monday, June 15, 2015', + short: '6/15/15, 9:03 AM', + medium: 'Jun 15, 2015, 9:03:01 AM', + long: /June 15, 2015 at 9:03:01 AM GMT(\+|-)\d/, + full: /Monday, June 15, 2015 at 9:03:01 AM GMT(\+|-)\d{2}:\d{2}/, + shortTime: '9:03 AM', + mediumTime: '9:03:01 AM', + longTime: /9:03:01 AM GMT(\+|-)\d/, + fullTime: /9:03:01 AM GMT(\+|-)\d{2}:\d{2}/, }; - if (!browserDetection.isOldChrome) { - // IE and Edge do not add a coma after the year in these 2 cases - if ((browserDetection.isEdge || browserDetection.isIE) && - browserDetection.supportsNativeIntlApi) { - dateFixtures['medium'] = 'Jun 15, 2015 9:03:01 AM'; - dateFixtures['short'] = '6/15/2015 9:03 AM'; - } else { - dateFixtures['medium'] = 'Jun 15, 2015, 9:03:01 AM'; - dateFixtures['short'] = '6/15/2015, 9:03 AM'; - } - } - - if (!browserDetection.isOldChrome) { - dateFixtures['mediumTime'] = '9:03:01 AM'; - dateFixtures['shortTime'] = '9:03 AM'; - } - Object.keys(dateFixtures).forEach((pattern: string) => { - expectDateFormatAs(date, pattern, dateFixtures[pattern]); + expect(pipe.transform(date, pattern)).toMatch(dateFixtures[pattern]); }); - }); it('should format invalid in IE ISO date', @@ -201,8 +268,39 @@ export function main() { it('should format invalid in Safari ISO date', () => expect(pipe.transform('2017-01-20T19:00:00+0000')).toEqual('Jan 20, 2017')); + // test for the following bugs: + // https://github.com/angular/angular/issues/9524 + // https://github.com/angular/angular/issues/9524 + it('should format correctly with iso strings that contain time', + () => expect(pipe.transform('2017-05-07T22:14:39', 'dd-MM-yyyy HH:mm')) + .toMatch(/07-05-2017 \d{2}:\d{2}/)); + + // test for the following bugs: + // https://github.com/angular/angular/issues/16624 + // https://github.com/angular/angular/issues/17478 + it('should show the correct time when the timezone is fixed', () => { + expect(pipe.transform('2017-06-13T10:14:39+0000', 'shortTime', '+0000')) + .toEqual('10:14 AM'); + expect(pipe.transform('2017-06-13T10:14:39+0000', 'h:mm a', '+0000')).toEqual('10:14 AM'); + }); + it('should remove bidi control characters', () => expect(pipe.transform(date, 'MM/dd/yyyy') !.length).toEqual(10)); + + it(`should format the date correctly in various locales`, () => { + expect(new DatePipe('de').transform(date, 'short')).toEqual('15.06.15, 09:03'); + expect(new DatePipe('th').transform(date, 'dd-MM-yy')).toEqual('15-06-15'); + expect(new DatePipe('hu').transform(date, 'a')).toEqual('de.'); + expect(new DatePipe('sr').transform(date, 'a')).toEqual('пре подне'); + + // TODO(ocombe): activate this test when we support local numbers + // expect(new DatePipe('mr', [localeMr]).transform(date, 'hh')).toEqual('०९'); + }); + + it('should throw if we use getExtraDayPeriods without loading extra locale data', () => { + expect(() => new DatePipe('de').transform(date, 'b')) + .toThrowError(/Missing extra locale data for the locale "de"/); + }); }); }); } diff --git a/packages/common/test/pipes/deprecated/date_pipe_spec.ts b/packages/common/test/pipes/deprecated/date_pipe_spec.ts new file mode 100644 index 00000000000000..52bcd91b4ffa68 --- /dev/null +++ b/packages/common/test/pipes/deprecated/date_pipe_spec.ts @@ -0,0 +1,208 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {DeprecatedDatePipe} from '@angular/common'; +import {JitReflector} from '@angular/compiler'; +import {PipeResolver} from '@angular/compiler/src/pipe_resolver'; +import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; + +export function main() { + describe('DeprecatedDatePipe', () => { + let date: Date; + const isoStringWithoutTime = '2015-01-01'; + let pipe: DeprecatedDatePipe; + + // Check the transformation of a date into a pattern + function expectDateFormatAs(date: Date | string, pattern: any, output: string): void { + expect(pipe.transform(date, pattern)).toEqual(output); + } + + // TODO: reactivate the disabled expectations once emulators are fixed in SauceLabs + // In some old versions of Chrome in Android emulators, time formatting returns dates in the + // timezone of the VM host, + // instead of the device timezone. Same symptoms as + // https://bugs.chromium.org/p/chromium/issues/detail?id=406382 + // This happens locally and in SauceLabs, so some checks are disabled to avoid failures. + // Tracking issue: https://github.com/angular/angular/issues/11187 + + beforeEach(() => { + date = new Date(2015, 5, 15, 9, 3, 1); + pipe = new DeprecatedDatePipe('en-US'); + }); + + it('should be marked as pure', () => { + expect(new PipeResolver(new JitReflector()).resolve(DeprecatedDatePipe) !.pure).toEqual(true); + }); + + describe('supports', () => { + it('should support date', () => { expect(() => pipe.transform(date)).not.toThrow(); }); + + it('should support int', () => { expect(() => pipe.transform(123456789)).not.toThrow(); }); + + it('should support numeric strings', + () => { expect(() => pipe.transform('123456789')).not.toThrow(); }); + + it('should support decimal strings', + () => { expect(() => pipe.transform('123456789.11')).not.toThrow(); }); + + it('should support ISO string', + () => expect(() => pipe.transform('2015-06-15T21:43:11Z')).not.toThrow()); + + it('should return null for empty string', () => expect(pipe.transform('')).toEqual(null)); + + it('should return null for NaN', () => expect(pipe.transform(Number.NaN)).toEqual(null)); + + it('should support ISO string without time', + () => { expect(() => pipe.transform(isoStringWithoutTime)).not.toThrow(); }); + + it('should not support other objects', + () => expect(() => pipe.transform({})).toThrowError(/InvalidPipeArgument/)); + }); + + describe('transform', () => { + it('should format each component correctly', () => { + const dateFixtures: any = { + 'y': '2015', + 'yy': '15', + 'M': '6', + 'MM': '06', + 'MMM': 'Jun', + 'MMMM': 'June', + 'd': '15', + 'dd': '15', + 'EEE': 'Mon', + 'EEEE': 'Monday' + }; + + const isoStringWithoutTimeFixtures: any = { + 'y': '2015', + 'yy': '15', + 'M': '1', + 'MM': '01', + 'MMM': 'Jan', + 'MMMM': 'January', + 'd': '1', + 'dd': '01', + 'EEE': 'Thu', + 'EEEE': 'Thursday' + }; + + if (!browserDetection.isOldChrome) { + dateFixtures['h'] = '9'; + dateFixtures['hh'] = '09'; + dateFixtures['j'] = '9 AM'; + isoStringWithoutTimeFixtures['h'] = '12'; + isoStringWithoutTimeFixtures['hh'] = '12'; + isoStringWithoutTimeFixtures['j'] = '12 AM'; + } + + // IE and Edge can't format a date to minutes and seconds without hours + if (!browserDetection.isEdge && !browserDetection.isIE || + !browserDetection.supportsNativeIntlApi) { + if (!browserDetection.isOldChrome) { + dateFixtures['HH'] = '09'; + isoStringWithoutTimeFixtures['HH'] = '00'; + } + dateFixtures['E'] = 'M'; + dateFixtures['L'] = 'J'; + dateFixtures['m'] = '3'; + dateFixtures['s'] = '1'; + dateFixtures['mm'] = '03'; + dateFixtures['ss'] = '01'; + isoStringWithoutTimeFixtures['m'] = '0'; + isoStringWithoutTimeFixtures['s'] = '0'; + isoStringWithoutTimeFixtures['mm'] = '00'; + isoStringWithoutTimeFixtures['ss'] = '00'; + } + + Object.keys(dateFixtures).forEach((pattern: string) => { + expectDateFormatAs(date, pattern, dateFixtures[pattern]); + }); + + if (!browserDetection.isOldChrome) { + Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => { + expectDateFormatAs( + isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]); + }); + } + + expect(pipe.transform(date, 'Z')).toBeDefined(); + }); + + it('should format common multi component patterns', () => { + const dateFixtures: any = { + 'EEE, M/d/y': 'Mon, 6/15/2015', + 'EEE, M/d': 'Mon, 6/15', + 'MMM d': 'Jun 15', + 'dd/MM/yyyy': '15/06/2015', + 'MM/dd/yyyy': '06/15/2015', + 'yMEEEd': '20156Mon15', + 'MEEEd': '6Mon15', + 'MMMd': 'Jun15', + 'yMMMMEEEEd': 'Monday, June 15, 2015' + }; + + // IE and Edge can't format a date to minutes and seconds without hours + if (!browserDetection.isEdge && !browserDetection.isIE || + !browserDetection.supportsNativeIntlApi) { + dateFixtures['ms'] = '31'; + } + + if (!browserDetection.isOldChrome) { + dateFixtures['jm'] = '9:03 AM'; + } + + Object.keys(dateFixtures).forEach((pattern: string) => { + expectDateFormatAs(date, pattern, dateFixtures[pattern]); + }); + + }); + + it('should format with pattern aliases', () => { + const dateFixtures: any = { + 'MM/dd/yyyy': '06/15/2015', + 'fullDate': 'Monday, June 15, 2015', + 'longDate': 'June 15, 2015', + 'mediumDate': 'Jun 15, 2015', + 'shortDate': '6/15/2015' + }; + + if (!browserDetection.isOldChrome) { + // IE and Edge do not add a coma after the year in these 2 cases + if ((browserDetection.isEdge || browserDetection.isIE) && + browserDetection.supportsNativeIntlApi) { + dateFixtures['medium'] = 'Jun 15, 2015 9:03:01 AM'; + dateFixtures['short'] = '6/15/2015 9:03 AM'; + } else { + dateFixtures['medium'] = 'Jun 15, 2015, 9:03:01 AM'; + dateFixtures['short'] = '6/15/2015, 9:03 AM'; + } + } + + if (!browserDetection.isOldChrome) { + dateFixtures['mediumTime'] = '9:03:01 AM'; + dateFixtures['shortTime'] = '9:03 AM'; + } + + Object.keys(dateFixtures).forEach((pattern: string) => { + expectDateFormatAs(date, pattern, dateFixtures[pattern]); + }); + + }); + + it('should format invalid in IE ISO date', + () => expect(pipe.transform('2017-01-11T09:25:14.014-0500')).toEqual('Jan 11, 2017')); + + it('should format invalid in Safari ISO date', + () => expect(pipe.transform('2017-01-20T19:00:00+0000')).toEqual('Jan 20, 2017')); + + it('should remove bidi control characters', + () => expect(pipe.transform(date, 'MM/dd/yyyy') !.length).toEqual(10)); + }); + }); +} diff --git a/packages/common/test/pipes/deprecated/number_pipe_spec.ts b/packages/common/test/pipes/deprecated/number_pipe_spec.ts new file mode 100644 index 00000000000000..a0fb5edfc7cad1 --- /dev/null +++ b/packages/common/test/pipes/deprecated/number_pipe_spec.ts @@ -0,0 +1,109 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {DeprecatedCurrencyPipe, DeprecatedDecimalPipe, DeprecatedPercentPipe} from '@angular/common'; +import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal'; +import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; + +export function main() { + function isNumeric(value: any): boolean { return !isNaN(value - parseFloat(value)); } + + // Between the symbol and the number, Edge adds a no breaking space and IE11 adds a standard space + function normalize(s: string): string { return s.replace(/\u00A0| /g, ''); } + + describe('Number pipes', () => { + describe('DeprecatedDecimalPipe', () => { + let pipe: DeprecatedDecimalPipe; + + beforeEach(() => { pipe = new DeprecatedDecimalPipe('en-US'); }); + + describe('transform', () => { + it('should return correct value for numbers', () => { + expect(pipe.transform(12345)).toEqual('12,345'); + expect(pipe.transform(123, '.2')).toEqual('123.00'); + expect(pipe.transform(1, '3.')).toEqual('001'); + expect(pipe.transform(1.1, '3.4-5')).toEqual('001.1000'); + expect(pipe.transform(1.123456, '3.4-5')).toEqual('001.12346'); + expect(pipe.transform(1.1234)).toEqual('1.123'); + }); + + it('should support strings', () => { + expect(pipe.transform('12345')).toEqual('12,345'); + expect(pipe.transform('123', '.2')).toEqual('123.00'); + expect(pipe.transform('1', '3.')).toEqual('001'); + expect(pipe.transform('1.1', '3.4-5')).toEqual('001.1000'); + expect(pipe.transform('1.123456', '3.4-5')).toEqual('001.12346'); + expect(pipe.transform('1.1234')).toEqual('1.123'); + }); + + it('should not support other objects', () => { + expect(() => pipe.transform(new Object())).toThrowError(); + expect(() => pipe.transform('123abc')).toThrowError(); + }); + }); + }); + + describe('DeprecatedPercentPipe', () => { + let pipe: DeprecatedPercentPipe; + + beforeEach(() => { pipe = new DeprecatedPercentPipe('en-US'); }); + + describe('transform', () => { + it('should return correct value for numbers', () => { + expect(normalize(pipe.transform(1.23) !)).toEqual('123%'); + expect(normalize(pipe.transform(1.2, '.2') !)).toEqual('120.00%'); + }); + + it('should not support other objects', + () => { expect(() => pipe.transform(new Object())).toThrowError(); }); + }); + }); + + describe('DeprecatedCurrencyPipe', () => { + let pipe: DeprecatedCurrencyPipe; + + beforeEach(() => { pipe = new DeprecatedCurrencyPipe('en-US'); }); + + describe('transform', () => { + it('should return correct value for numbers', () => { + // In old Chrome, default formatiing for USD is different + if (browserDetection.isOldChrome) { + expect(normalize(pipe.transform(123) !)).toEqual('USD123'); + } else { + expect(normalize(pipe.transform(123) !)).toEqual('USD123.00'); + } + expect(normalize(pipe.transform(12, 'EUR', false, '.1') !)).toEqual('EUR12.0'); + expect(normalize(pipe.transform(5.1234, 'USD', false, '.0-3') !)).toEqual('USD5.123'); + }); + + it('should not support other objects', + () => { expect(() => pipe.transform(new Object())).toThrowError(); }); + }); + }); + + describe('isNumeric', () => { + it('should return true when passing correct numeric string', + () => { expect(isNumeric('2')).toBe(true); }); + + it('should return true when passing correct double string', + () => { expect(isNumeric('1.123')).toBe(true); }); + + it('should return true when passing correct negative string', + () => { expect(isNumeric('-2')).toBe(true); }); + + it('should return true when passing correct scientific notation string', + () => { expect(isNumeric('1e5')).toBe(true); }); + + it('should return false when passing incorrect numeric', + () => { expect(isNumeric('a')).toBe(false); }); + + it('should return false when passing parseable but non numeric', + () => { expect(isNumeric('2a')).toBe(false); }); + }); + }); +} diff --git a/packages/common/test/pipes/number_pipe_spec.ts b/packages/common/test/pipes/number_pipe_spec.ts index 85cd2515278c23..9721613b5eb3e3 100644 --- a/packages/common/test/pipes/number_pipe_spec.ts +++ b/packages/common/test/pipes/number_pipe_spec.ts @@ -6,19 +6,25 @@ * found in the LICENSE file at https://angular.io/license */ -import {CurrencyPipe, DecimalPipe, PercentPipe} from '@angular/common'; -import {isNumeric} from '@angular/common/src/pipes/number_pipe'; +import localeEn from '../../i18n_data/locale_en'; +import localeEsUS from '../../i18n_data/locale_es-US'; +import {registerLocaleData, CurrencyPipe, DecimalPipe, PercentPipe} from '@angular/common'; import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal'; -import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; export function main() { describe('Number pipes', () => { - describe('DecimalPipe', () => { - let pipe: DecimalPipe; + beforeAll(() => { + registerLocaleData(localeEn); + registerLocaleData(localeEsUS); + }); - beforeEach(() => { pipe = new DecimalPipe('en-US'); }); + function isNumeric(value: any): boolean { return !isNaN(value - parseFloat(value)); } + describe('DecimalPipe', () => { describe('transform', () => { + let pipe: DecimalPipe; + beforeEach(() => { pipe = new DecimalPipe('en-US'); }); + it('should return correct value for numbers', () => { expect(pipe.transform(12345)).toEqual('12,345'); expect(pipe.transform(123, '.2')).toEqual('123.00'); @@ -26,6 +32,8 @@ export function main() { expect(pipe.transform(1.1, '3.4-5')).toEqual('001.1000'); expect(pipe.transform(1.123456, '3.4-5')).toEqual('001.12346'); expect(pipe.transform(1.1234)).toEqual('1.123'); + expect(pipe.transform(1.123456, '.2')).toEqual('1.123'); + expect(pipe.transform(1.123456, '.4')).toEqual('1.1235'); }); it('should support strings', () => { @@ -38,9 +46,20 @@ export function main() { }); it('should not support other objects', () => { - expect(() => pipe.transform(new Object())).toThrowError(); + expect(() => pipe.transform({})).toThrowError(); expect(() => pipe.transform('123abc')).toThrowError(); }); + + it('should throw if minFractionDigits is explicitly higher than maxFractionDigits', () => { + expect(() => pipe.transform('1.1', '3.4-2')).toThrowError(/is higher than the maximum/); + }); + }); + + describe('transform with custom locales', () => { + it('should return the correct format for es-US in IE11', () => { + const pipe = new DecimalPipe('es-US'); + expect(pipe.transform('9999999.99', '1.2-2')).toEqual('9,999,999.99'); + }); }); }); @@ -51,12 +70,12 @@ export function main() { describe('transform', () => { it('should return correct value for numbers', () => { - expect(normalize(pipe.transform(1.23) !)).toEqual('123%'); - expect(normalize(pipe.transform(1.2, '.2') !)).toEqual('120.00%'); + expect(pipe.transform(1.23)).toEqual('123%'); + expect(pipe.transform(1.2, '.2')).toEqual('120.00%'); }); it('should not support other objects', - () => { expect(() => pipe.transform(new Object())).toThrowError(); }); + () => { expect(() => pipe.transform({})).toThrowError(); }); }); }); @@ -67,18 +86,24 @@ export function main() { describe('transform', () => { it('should return correct value for numbers', () => { - // In old Chrome, default formatiing for USD is different - if (browserDetection.isOldChrome) { - expect(normalize(pipe.transform(123) !)).toEqual('USD123'); - } else { - expect(normalize(pipe.transform(123) !)).toEqual('USD123.00'); - } - expect(normalize(pipe.transform(12, 'EUR', false, '.1') !)).toEqual('EUR12.0'); - expect(normalize(pipe.transform(5.1234, 'USD', false, '.0-3') !)).toEqual('USD5.123'); + expect(pipe.transform(123)).toEqual('$123.00'); + expect(pipe.transform(12, 'EUR', 'code', '.1')).toEqual('EUR12.0'); + expect(pipe.transform(5.1234, 'USD', 'code', '.0-3')).toEqual('USD5.123'); + expect(pipe.transform(5.1234, 'USD', 'code')).toEqual('USD5.12'); + expect(pipe.transform(5.1234, 'USD', 'symbol')).toEqual('$5.12'); + expect(pipe.transform(5.1234, 'CAD', 'symbol')).toEqual('CA$5.12'); + expect(pipe.transform(5.1234, 'CAD', 'symbol-narrow')).toEqual('$5.12'); }); it('should not support other objects', - () => { expect(() => pipe.transform(new Object())).toThrowError(); }); + () => { expect(() => pipe.transform({})).toThrowError(); }); + + it('should warn if you are using the v4 signature', () => { + const warnSpy = spyOn(console, 'warn'); + pipe.transform(123, 'USD', true); + expect(warnSpy).toHaveBeenCalledWith( + `Warning: the currency pipe has been changed in Angular v5. The symbolDisplay option (third parameter) is now a string instead of a boolean. The accepted values are "code", "symbol" or "symbol-narrow".`); + }); }); }); @@ -103,8 +128,3 @@ export function main() { }); }); } - -// Between the symbol and the number, Edge adds a no breaking space and IE11 adds a standard space -function normalize(s: string): string { - return s.replace(/\u00A0| /g, ''); -} diff --git a/packages/examples/common/pipes/ts/date_pipe.ts b/packages/examples/common/pipes/ts/date_pipe.ts index 5b518524ce58c8..d88b6da9a1c5a3 100644 --- a/packages/examples/common/pipes/ts/date_pipe.ts +++ b/packages/examples/common/pipes/ts/date_pipe.ts @@ -14,7 +14,8 @@ import {Component} from '@angular/core'; template: `

Today is {{today | date}}

Or if you prefer, {{today | date:'fullDate'}}

-

The time is {{today | date:'jmZ'}}

+

The time is {{today | date:'shortTime'}}

+

The custom date is {{today | date:'yyyy-mm-dd HH:mm'}}

` }) export class DatePipeComponent { diff --git a/packages/examples/common/pipes/ts/number_pipe.ts b/packages/examples/common/pipes/ts/number_pipe.ts index aa9d595db1cb10..96056b9ffdcb19 100644 --- a/packages/examples/common/pipes/ts/number_pipe.ts +++ b/packages/examples/common/pipes/ts/number_pipe.ts @@ -42,8 +42,9 @@ export class PercentPipeComponent { @Component({ selector: 'currency-pipe', template: `
-

A: {{a | currency:'USD':false}}

-

B: {{b | currency:'USD':true:'4.2-2'}}

+

A: {{a | currency:'CAD'}}

+

B: {{b | currency:'CAD':'symbol':'4.2-2'}}

+

B: {{b | currency:'CAD':'symbol-narrow':'4.2-2'}}

` }) export class CurrencyPipeComponent { diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 10ec247ee07d15..d951d649f12e3f 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -27,6 +27,7 @@ "exclude": [ "compiler-cli/integrationtest", "platform-server/integrationtest", - "tsc-wrapped" + "tsc-wrapped", + "common/i18n_data" ] } diff --git a/tools/gulp-tasks/cldr/extract.js b/tools/gulp-tasks/cldr/extract.js index c5045ddff95679..93f12e27eeb6d0 100644 --- a/tools/gulp-tasks/cldr/extract.js +++ b/tools/gulp-tasks/cldr/extract.js @@ -436,6 +436,24 @@ function stringify(obj) { return util.inspect(obj, {depth: null, maxArrayLength: null}) } +/** + * To create smaller locale files, we remove duplicated data. + * To be make this work we need to store similar data in arrays, if some value in an array + * is undefined, we can take the previous defined value instead, because it means that it has + * been deduplicated. + * e.g.: [x, y, undefined, z, undefined, undefined] + * The first undefined is equivalent to y, the second and third are equivalent to z + * Note that the first value in an array is always defined. + * + * Also since we need to know which data is assumed similar, it is important that we store those + * similar data in arrays to mark the delimitation between values that have different meanings + * (e.g. months and days). + * + * For further size improvements, "undefined" values will be replaced by empty values in the arrays + * as the last step of the file generation (in generateLocale and generateLocaleExtra). + * e.g.: [x, y, undefined, z, undefined, undefined] will be [x, y, , z, , ] + * This is possible because empty values are considered undefined in arrays. + */ function removeDuplicates(data) { const dedup = [data[0]]; for(let i = 1; i < data.length; i++) { diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index cdefdf3d960e5e..f41ae58da9919b 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -11,24 +11,60 @@ export declare class AsyncPipe implements OnDestroy, PipeTransform { transform(obj: null): null; } +/** @experimental */ +export declare const AVAILABLE_LOCALES: string[]; + /** @stable */ export declare class CommonModule { } +/** @experimental */ +export declare const CURRENCIES: { + [code: string]: (string | undefined)[]; +}; + /** @stable */ export declare class CurrencyPipe implements PipeTransform { constructor(_locale: string); - transform(value: any, currencyCode?: string, symbolDisplay?: boolean, digits?: string): string | null; + transform(value: any, currencyCode?: string, symbolDisplay?: 'code' | 'symbol' | 'symbol-narrow', digits?: string, locale?: string): string | null; } /** @stable */ export declare class DatePipe implements PipeTransform { + constructor(locale: string); + transform(value: any, format?: string, timezone?: string, locale?: string): string | null; +} + +/** @stable */ +export declare class DecimalPipe implements PipeTransform { + constructor(_locale: string); + transform(value: any, digits?: string, locale?: string): string | null; +} + +/** @stable */ +export declare class DeprecatedCurrencyPipe implements PipeTransform { + constructor(_locale: string); + transform(value: any, currencyCode?: string, symbolDisplay?: boolean, digits?: string): string | null; +} + +/** @stable */ +export declare class DeprecatedDatePipe implements PipeTransform { constructor(_locale: string); transform(value: any, pattern?: string): string | null; } /** @stable */ -export declare class DecimalPipe implements PipeTransform { +export declare class DeprecatedDecimalPipe implements PipeTransform { + constructor(_locale: string); + transform(value: any, digits?: string): string | null; +} + +/** @deprecated */ +export declare class DeprecatedI18NPipesModule { +} + +/** @stable */ +export declare class DeprecatedPercentPipe implements PipeTransform { constructor(_locale: string); transform(value: any, digits?: string): string | null; } @@ -40,6 +76,74 @@ export declare class DeprecatedI18NPipesModule { /** @stable */ export declare const DOCUMENT: InjectionToken; +/** @experimental */ +export declare function findLocaleData(locale: string): any; + +/** @experimental */ +export declare enum FormatWidth { + Short = 0, + Medium = 1, + Long = 2, + Full = 3, +} + +/** @experimental */ +export declare enum FormStyle { + Format = 0, + Standalone = 1, +} + +/** @experimental */ +export declare function getLocaleCurrencyName(locale: string): string | null; + +/** @experimental */ +export declare function getLocaleCurrencySymbol(locale: string): string | null; + +/** @experimental */ +export declare function getLocaleDateFormat(locale: string, width: FormatWidth): string; + +/** @experimental */ +export declare function getLocaleDateTimeFormat(locale: string, width: FormatWidth): string; + +/** @experimental */ +export declare function getLocaleDayNames(locale: string, formStyle: FormStyle, width: TranslationWidth): string[]; + +/** @experimental */ +export declare function getLocaleDayPeriods(locale: string, formStyle: FormStyle, width: TranslationWidth): [string, string]; + +/** @experimental */ +export declare function getLocaleEraNames(locale: string, width: TranslationWidth): [string, string]; + +/** @experimental */ +export declare function getLocaleExtraDayPeriodRules(locale: string): (Time | [Time, Time])[]; + +/** @experimental */ +export declare function getLocaleExtraDayPeriods(locale: string, formStyle: FormStyle, width: TranslationWidth): string[]; + +/** @experimental */ +export declare function getLocaleFirstDayOfWeek(locale: string): WeekDay; + +/** @experimental */ +export declare function getLocaleId(locale: string): string; + +/** @experimental */ +export declare function getLocaleMonthNames(locale: string, formStyle: FormStyle, width: TranslationWidth): string[]; + +/** @experimental */ +export declare function getLocaleNumberFormat(locale: string, type: NumberFormatStyle): string; + +/** @experimental */ +export declare function getLocaleNumberSymbol(locale: string, symbol: NumberSymbol): string; + +/** @experimental */ +export declare function getLocalePluralCase(locale: string): (value: number) => Plural; + +/** @experimental */ +export declare function getLocaleTimeFormat(locale: string, width: FormatWidth): string; + +/** @experimental */ +export declare function getLocaleWeekEndRange(locale: string): [WeekDay, WeekDay]; + /** @stable */ export declare class HashLocationStrategy extends LocationStrategy { constructor(_platformLocation: PlatformLocation, _baseHref?: string); @@ -58,7 +162,7 @@ export declare class I18nPluralPipe implements PipeTransform { constructor(_localization: NgLocalization); transform(value: number, pluralMap: { [count: string]: string; - }): string; + }, locale?: string): string; } /** @experimental */ @@ -85,6 +189,11 @@ export declare class JsonPipe implements PipeTransform { transform(value: any): string; } +/** @experimental */ +export declare const LOCALE_DATA: { + [localeId: string]: any; +}; + /** @stable */ export declare class Location { constructor(platformStrategy: LocationStrategy); @@ -197,12 +306,12 @@ export declare class NgIfContext { export declare class NgLocaleLocalization extends NgLocalization { protected locale: string; constructor(locale: string); - getPluralCategory(value: any): string; + getPluralCategory(value: any, locale?: string): string; } /** @experimental */ export declare abstract class NgLocalization { - abstract getPluralCategory(value: any): string; + abstract getPluralCategory(value: any, locale?: string): string; } /** @experimental */ @@ -253,6 +362,31 @@ export declare class NgTemplateOutlet implements OnChanges { ngOnChanges(changes: SimpleChanges): void; } +/** @experimental */ +export declare enum NumberFormatStyle { + Decimal = 0, + Percent = 1, + Currency = 2, + Scientific = 3, +} + +/** @experimental */ +export declare enum NumberSymbol { + Decimal = 0, + Group = 1, + List = 2, + PercentSign = 3, + PlusSign = 4, + MinusSign = 5, + Exponential = 6, + SuperscriptingExponent = 7, + PerMille = 8, + Infinity = 9, + NaN = 10, + TimeSeparator = 11, + CurrencyGroup = 12, +} + /** @stable */ export declare class PathLocationStrategy extends LocationStrategy { constructor(_platformLocation: PlatformLocation, href?: string); @@ -269,7 +403,7 @@ export declare class PathLocationStrategy extends LocationStrategy { /** @stable */ export declare class PercentPipe implements PipeTransform { constructor(_locale: string); - transform(value: any, digits?: string): string | null; + transform(value: any, digits?: string, locale?: string): string | null; } /** @stable */ @@ -286,6 +420,16 @@ export declare abstract class PlatformLocation { abstract replaceState(state: any, title: string, url: string): void; } +/** @experimental */ +export declare enum Plural { + Zero = 0, + One = 1, + Two = 2, + Few = 3, + Many = 4, + Other = 5, +} + /** @experimental */ export interface PopStateEvent { pop?: boolean; @@ -293,16 +437,33 @@ export interface PopStateEvent { url?: string; } +/** @experimental */ +export declare function registerLocaleData(data: any, extraData?: any): void; + /** @stable */ export declare class SlicePipe implements PipeTransform { transform(value: any, start: number, end?: number): any; } +/** @experimental */ +export declare type Time = { + hours: number; + minutes: number; +}; + /** @stable */ export declare class TitleCasePipe implements PipeTransform { transform(value: string): string; } +/** @experimental */ +export declare enum TranslationWidth { + Narrow = 0, + Abbreviated = 1, + Wide = 2, + Short = 3, +} + /** @stable */ export declare class UpperCasePipe implements PipeTransform { transform(value: string): string; @@ -310,3 +471,14 @@ export declare class UpperCasePipe implements PipeTransform { /** @stable */ export declare const VERSION: Version; + +/** @experimental */ +export declare enum WeekDay { + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6, +}