From 6a8cca797536dc69e18266741d70790eccb2121d Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Thu, 11 Apr 2019 17:52:55 +0200 Subject: [PATCH] feat(ivy): use i18n locale data to determine the plural form of ICU expressions (#29249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural ICU expressions depend on the locale (different languages have different plural forms). Until now the locale was hard coded as `en-US`. For compatibility reasons, if you use ivy with AOT and bootstrap your app with `bootstrapModule` then the `LOCALE_ID` token will be set automatically for ivy, which is then used to get the correct plural form. If you use JIT, you need to define the `LOCALE_ID` provider on the module that you bootstrap. For `TestBed` you can use either `configureTestingModule` or `overrideProvider` to define that provider. If you don't use the compat mode and start your app with `renderComponent` you need to call `ɵsetLocaleId` manually to define the `LOCALE_ID` before bootstrap. We expect this to change once we start adding the new i18n APIs, so don't rely on this function (there's a reason why it's a private export). PR Close #29249 --- packages/common/src/i18n/locale_data.ts | 31 +- packages/common/src/i18n/locale_data_api.ts | 51 +-- .../common/test/i18n/locale_data_api_spec.ts | 3 +- .../src/ngtsc/annotations/src/ng_module.ts | 21 +- packages/compiler-cli/src/ngtsc/program.ts | 3 +- packages/core/src/application_ref.ts | 5 + packages/core/src/core_private_export.ts | 2 + .../core/src/core_render3_private_export.ts | 2 + packages/core/src/i18n/locale_data.ts | 38 ++ packages/core/src/i18n/locale_data_api.ts | 53 +++ .../{common => core}/src/i18n/locale_en.ts | 0 packages/core/src/i18n/localization.ts | 31 ++ packages/core/src/render3/component.ts | 2 +- packages/core/src/render3/definition.ts | 6 +- packages/core/src/render3/fields.ts | 1 + packages/core/src/render3/i18n.ts | 382 ++---------------- packages/core/src/render3/index.ts | 3 + packages/core/src/render3/jit/module.ts | 1 - packages/core/src/render3/ng_module_ref.ts | 8 +- packages/core/test/BUILD.bazel | 1 + packages/core/test/application_ref_spec.ts | 21 +- packages/core/test/i18n_integration_spec.ts | 45 ++- packages/core/test/render3/BUILD.bazel | 1 + packages/core/test/render3/i18n_spec.ts | 60 ++- .../core/testing/src/r3_test_bed_compiler.ts | 9 +- tools/gulp-tasks/cldr/extract.js | 15 +- tools/public_api_guard/common/common.d.ts | 2 +- 27 files changed, 346 insertions(+), 451 deletions(-) create mode 100644 packages/core/src/i18n/locale_data.ts create mode 100644 packages/core/src/i18n/locale_data_api.ts rename packages/{common => core}/src/i18n/locale_en.ts (100%) create mode 100644 packages/core/src/i18n/localization.ts diff --git a/packages/common/src/i18n/locale_data.ts b/packages/common/src/i18n/locale_data.ts index 1d3301999a..975dcc0b69 100644 --- a/packages/common/src/i18n/locale_data.ts +++ b/packages/common/src/i18n/locale_data.ts @@ -6,10 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -/** - * @publicApi - */ -export const LOCALE_DATA: {[localeId: string]: any} = {}; +import {ɵLOCALE_DATA as LOCALE_DATA, ɵLocaleDataIndex as LocaleDataIndex} from '@angular/core'; /** * Register global data to be used internally by Angular. See the @@ -33,32 +30,6 @@ export function registerLocaleData(data: any, localeId?: string | any, extraData } } -/** - * Index of each type of locale data from the locale data array - */ -export const enum LocaleDataIndex { - LocaleId = 0, - DayPeriodsFormat, - DayPeriodsStandalone, - DaysFormat, - DaysStandalone, - MonthsFormat, - MonthsStandalone, - Eras, - FirstDayOfWeek, - WeekendRange, - DateFormat, - TimeFormat, - DateTimeFormat, - NumberSymbols, - NumberFormats, - CurrencySymbol, - CurrencyName, - Currencies, - PluralCase, - ExtraData -} - /** * Index of each type of locale data from the extra locale data array */ diff --git a/packages/common/src/i18n/locale_data_api.ts b/packages/common/src/i18n/locale_data_api.ts index 46d02421cc..072add5903 100644 --- a/packages/common/src/i18n/locale_data_api.ts +++ b/packages/common/src/i18n/locale_data_api.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import localeEn from './locale_en'; -import {LOCALE_DATA, LocaleDataIndex, ExtraLocaleDataIndex, CurrencyIndex} from './locale_data'; +import {ɵLocaleDataIndex as LocaleDataIndex, ɵfindLocaleData as findLocaleData, ɵgetLocalePluralCase} from '@angular/core'; import {CURRENCIES_EN, CurrenciesSymbols} from './currencies'; +import {CurrencyIndex, ExtraLocaleDataIndex} from './locale_data'; /** * Format styles that can be used to represent numbers. @@ -31,7 +31,8 @@ export enum NumberFormatStyle { * @see `NgPluralCase` * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) * - * @publicApi */ + * @publicApi + */ export enum Plural { Zero = 0, One = 1, @@ -485,19 +486,11 @@ function getLocaleCurrencies(locale: string): {[code: string]: CurrenciesSymbols } /** - * Retrieves the plural function used by ICU expressions to determine the plural case to use - * for a given locale. - * @param locale A locale code for the locale format rules to use. - * @returns The plural function for the locale. - * @see `NgPlural` - * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) - * + * @alias core/ɵgetLocalePluralCase * @publicApi */ -export function getLocalePluralCase(locale: string): (value: number) => Plural { - const data = findLocaleData(locale); - return data[LocaleDataIndex.PluralCase]; -} +export const getLocalePluralCase: (locale: string) => ((value: number) => Plural) = + ɵgetLocalePluralCase; function checkFullData(data: any) { if (!data[LocaleDataIndex.ExtraData]) { @@ -609,37 +602,7 @@ function extractTime(time: string): Time { return {hours: +h, minutes: +m}; } -/** - * Finds the locale data for a given locale. - * - * @param locale The locale code. - * @returns The locale data. - * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) - * - * @publicApi - */ -export function findLocaleData(locale: string): any { - const normalizedLocale = locale.toLowerCase().replace(/_/g, '-'); - let match = LOCALE_DATA[normalizedLocale]; - if (match) { - return match; - } - - // let's try to find a parent locale - const parentLocale = normalizedLocale.split('-')[0]; - match = LOCALE_DATA[parentLocale]; - - if (match) { - return match; - } - - if (parentLocale === 'en') { - return localeEn; - } - - throw new Error(`Missing locale data for the locale "${locale}".`); -} /** * Retrieves the currency symbol for a given currency code. diff --git a/packages/common/test/i18n/locale_data_api_spec.ts b/packages/common/test/i18n/locale_data_api_spec.ts index 9fee387070..6babf0bd20 100644 --- a/packages/common/test/i18n/locale_data_api_spec.ts +++ b/packages/common/test/i18n/locale_data_api_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ɵfindLocaleData as findLocaleData} from '@angular/core'; import localeCaESVALENCIA from '@angular/common/locales/ca-ES-VALENCIA'; import localeEn from '@angular/common/locales/en'; import localeFr from '@angular/common/locales/fr'; @@ -13,7 +14,7 @@ import localeZh from '@angular/common/locales/zh'; import localeFrCA from '@angular/common/locales/fr-CA'; import localeEnAU from '@angular/common/locales/en-AU'; import {registerLocaleData} from '../../src/i18n/locale_data'; -import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNumberOfCurrencyDigits} from '../../src/i18n/locale_data_api'; +import {getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNumberOfCurrencyDigits} from '../../src/i18n/locale_data_api'; { describe('locale data api', () => { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index f82c2db834..de599637a3 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Expression, ExternalExpr, InvokeFunctionExpr, LiteralArrayExpr, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, Statement, WrappedNodeExpr, compileInjector, compileNgModule} from '@angular/compiler'; +import {Expression, ExternalExpr, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, Statement, WrappedNodeExpr, compileInjector, compileNgModule} from '@angular/compiler'; +import {STRING_TYPE} from '@angular/compiler/src/output/output_ast'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; @@ -17,7 +18,6 @@ import {NgModuleRouteAnalyzer} from '../../routing'; import {LocalModuleScopeRegistry, ScopeData} from '../../scope'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform'; import {getSourceFile} from '../../util/src/typescript'; - import {generateSetClassMetadataCall} from './metadata'; import {ReferencesRegistry} from './references_registry'; import {combineResolvers, findAngularDecorator, forwardRefResolver, getValidConstructorDependencies, isExpressionForwardReference, toR3Reference, unwrapExpression} from './util'; @@ -41,7 +41,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler remove(this._modules, moduleRef)); ngZone !.runOutsideAngular( () => ngZone !.onError.subscribe( diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 1a446cbee5..a97e2d467e 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -34,3 +34,5 @@ export {makeDecorator as ɵmakeDecorator} from './util/decorators'; export {isObservable as ɵisObservable, isPromise as ɵisPromise} from './util/lang'; export {clearOverrides as ɵclearOverrides, initServicesIfNeeded as ɵinitServicesIfNeeded, overrideComponentView as ɵoverrideComponentView, overrideProvider as ɵoverrideProvider} from './view/index'; export {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from './view/provider'; +export {getLocalePluralCase as ɵgetLocalePluralCase, findLocaleData as ɵfindLocaleData} from './i18n/locale_data_api'; +export {LOCALE_DATA as ɵLOCALE_DATA, LocaleDataIndex as ɵLocaleDataIndex} from './i18n/locale_data'; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 179735a15e..ec94366dc0 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -136,6 +136,8 @@ export { ɵɵi18nPostprocess, i18nConfigureLocalize as ɵi18nConfigureLocalize, ɵɵi18nLocalize, + setLocaleId as ɵsetLocaleId, + DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID, setClassMetadata as ɵsetClassMetadata, ɵɵresolveWindow, ɵɵresolveDocument, diff --git a/packages/core/src/i18n/locale_data.ts b/packages/core/src/i18n/locale_data.ts new file mode 100644 index 0000000000..734cc16680 --- /dev/null +++ b/packages/core/src/i18n/locale_data.ts @@ -0,0 +1,38 @@ +/** + * @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 + */ + +/** + * This const is used to store the locale data registered with `registerLocaleData` + */ +export const LOCALE_DATA: {[localeId: string]: any} = {}; + +/** + * Index of each type of locale data from the locale data array + */ +export enum LocaleDataIndex { + LocaleId = 0, + DayPeriodsFormat, + DayPeriodsStandalone, + DaysFormat, + DaysStandalone, + MonthsFormat, + MonthsStandalone, + Eras, + FirstDayOfWeek, + WeekendRange, + DateFormat, + TimeFormat, + DateTimeFormat, + NumberSymbols, + NumberFormats, + CurrencySymbol, + CurrencyName, + Currencies, + PluralCase, + ExtraData +} diff --git a/packages/core/src/i18n/locale_data_api.ts b/packages/core/src/i18n/locale_data_api.ts new file mode 100644 index 0000000000..cc30c20348 --- /dev/null +++ b/packages/core/src/i18n/locale_data_api.ts @@ -0,0 +1,53 @@ +/** + * @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 {LOCALE_DATA, LocaleDataIndex} from './locale_data'; +import localeEn from './locale_en'; + +/** + * Retrieves the plural function used by ICU expressions to determine the plural case to use + * for a given locale. + * @param locale A locale code for the locale format rules to use. + * @returns The plural function for the locale. + * @see `NgPlural` + * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) + */ +export function getLocalePluralCase(locale: string): (value: number) => number { + const data = findLocaleData(locale); + return data[LocaleDataIndex.PluralCase]; +} + +/** + * Finds the locale data for a given locale. + * + * @param locale The locale code. + * @returns The locale data. + * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) + */ +export function findLocaleData(locale: string): any { + const normalizedLocale = locale.toLowerCase().replace(/_/g, '-'); + + let match = LOCALE_DATA[normalizedLocale]; + if (match) { + return match; + } + + // let's try to find a parent locale + const parentLocale = normalizedLocale.split('-')[0]; + match = LOCALE_DATA[parentLocale]; + + if (match) { + return match; + } + + if (parentLocale === 'en') { + return localeEn; + } + + throw new Error(`Missing locale data for the locale "${locale}".`); +} diff --git a/packages/common/src/i18n/locale_en.ts b/packages/core/src/i18n/locale_en.ts similarity index 100% rename from packages/common/src/i18n/locale_en.ts rename to packages/core/src/i18n/locale_en.ts diff --git a/packages/core/src/i18n/localization.ts b/packages/core/src/i18n/localization.ts new file mode 100644 index 0000000000..0cb492f18b --- /dev/null +++ b/packages/core/src/i18n/localization.ts @@ -0,0 +1,31 @@ +/** + * @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 {getLocalePluralCase} from './locale_data_api'; + +/** + * Returns the plural case based on the locale + */ +export function getPluralCase(value: any, locale: string): string { + const plural = getLocalePluralCase(locale)(value); + + switch (plural) { + case 0: + return 'zero'; + case 1: + return 'one'; + case 2: + return 'two'; + case 3: + return 'few'; + case 4: + return 'many'; + default: + return 'other'; + } +} diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index c93d8a10b0..f03645f88f 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -60,7 +60,7 @@ export interface CreateComponentOptions { * Typically, the features in this list are features that cannot be added to the * other features list in the component definition because they rely on other factors. * - * Example: `RootLifecycleHooks` is a function that adds lifecycle hook capabilities + * Example: `LifecycleHooksFeature` is a function that adds lifecycle hook capabilities * to root components in a tree-shakable way. It cannot be added to the component * features list because there's no way of knowing when the component will be used as * a root component. diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 5c1fe79af0..a9a8f97299 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -17,7 +17,7 @@ import {noSideEffects} from '../util/closure'; import {stringify} from '../util/stringify'; import {EMPTY_ARRAY, EMPTY_OBJ} from './empty'; -import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields'; +import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_LOCALE_ID_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields'; import {ComponentDef, ComponentDefFeature, ComponentTemplate, ComponentType, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, DirectiveType, DirectiveTypesOrFactory, FactoryFn, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory, ViewQueriesFunction, ɵɵBaseDef} from './interfaces/definition'; // while SelectorFlags is unused here, it's required so that types don't get resolved lazily // see: https://github.com/Microsoft/web-build-tools/issues/1050 @@ -738,3 +738,7 @@ export function getNgModuleDef(type: any, throwNotFound?: boolean): NgModuleD } return ngModuleDef; } + +export function getNgLocaleIdDef(type: any): string|null { + return (type as any)[NG_LOCALE_ID_DEF] || null; +} diff --git a/packages/core/src/render3/fields.ts b/packages/core/src/render3/fields.ts index 26b29af9da..92642b417f 100644 --- a/packages/core/src/render3/fields.ts +++ b/packages/core/src/render3/fields.ts @@ -12,6 +12,7 @@ export const NG_COMPONENT_DEF = getClosureSafeProperty({ngComponentDef: getClosu export const NG_DIRECTIVE_DEF = getClosureSafeProperty({ngDirectiveDef: getClosureSafeProperty}); export const NG_PIPE_DEF = getClosureSafeProperty({ngPipeDef: getClosureSafeProperty}); export const NG_MODULE_DEF = getClosureSafeProperty({ngModuleDef: getClosureSafeProperty}); +export const NG_LOCALE_ID_DEF = getClosureSafeProperty({ngLocaleIdDef: getClosureSafeProperty}); export const NG_BASE_DEF = getClosureSafeProperty({ngBaseDef: getClosureSafeProperty}); /** diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 26e7eae042..9924e1904f 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -7,13 +7,12 @@ */ import '../util/ng_i18n_closure_mode'; - +import {getPluralCase} from '../i18n/localization'; import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer'; import {InertBodyHelper} from '../sanitization/inert_body'; import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer'; import {addAllToArray} from '../util/array_utils'; import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert'; - import {attachPatchData} from './context_discovery'; import {attachI18nOpCodesDebug} from './debug'; import {ɵɵelementAttribute, ɵɵload, ɵɵtextBinding} from './instructions/all'; @@ -1026,351 +1025,6 @@ export function ɵɵi18nApply(index: number) { } } -enum Plural { - Zero = 0, - One = 1, - Two = 2, - Few = 3, - Many = 4, - Other = 5, -} - -/** - * Returns the plural case based on the locale. - * This is a copy of the deprecated function that we used in Angular v4. - * // TODO(ocombe): remove this once we can the real getPluralCase function - * - * @deprecated from v5 the plural case function is in locale data files common/locales/*.ts - */ -function getPluralCase(locale: string, nLike: number | string): Plural { - 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; - } -} - -function getPluralCategory(value: any, locale: string): string { - const plural = getPluralCase(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'; - } -} - /** * Returns the index of the current case of an ICU expression depending on the main binding value * @@ -1382,9 +1036,7 @@ function getCaseIndex(icuExpression: TIcu, bindingValue: string): number { if (index === -1) { switch (icuExpression.type) { case IcuType.plural: { - // TODO(ocombe): replace this hard-coded value by the real LOCALE_ID value - const locale = 'en-US'; - const resolvedCase = getPluralCategory(bindingValue, locale); + const resolvedCase = getPluralCase(bindingValue, getLocaleId()); index = icuExpression.cases.indexOf(resolvedCase); if (index === -1 && resolvedCase !== 'other') { index = icuExpression.cases.indexOf('other'); @@ -1630,7 +1282,7 @@ const LOCALIZE_PH_REGEXP = /\{\$(.*?)\}/g; * running outside of Closure Compiler. This method will not be needed once runtime translation * service support is introduced. * - * @publicApi + * @codeGenApi * @deprecated this method is temporary & should not be used as it will be removed soon */ export function ɵɵi18nLocalize(input: string, placeholders: {[key: string]: string} = {}) { @@ -1641,3 +1293,31 @@ export function ɵɵi18nLocalize(input: string, placeholders: {[key: string]: st input.replace(LOCALIZE_PH_REGEXP, (match, key) => placeholders[key] || '') : input; } + +/** + * The locale id that the application is currently using (for translations and ICU expressions). + * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine + * but is now defined as a global value. + */ +export const DEFAULT_LOCALE_ID = 'en-US'; +let LOCALE_ID = DEFAULT_LOCALE_ID; + +/** + * Sets the locale id that will be used for translations and ICU expressions. + * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine + * but is now defined as a global value. + * + * @param localeId + */ +export function setLocaleId(localeId: string) { + LOCALE_ID = localeId.toLowerCase().replace(/_/g, '-'); +} + +/** + * Gets the locale id that will be used for translations and ICU expressions. + * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine + * but is now defined as a global value. + */ +export function getLocaleId(): string { + return LOCALE_ID; +} diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 408f786824..4d81e5a017 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -100,6 +100,7 @@ export { } from './state'; export { + DEFAULT_LOCALE_ID, ɵɵi18n, ɵɵi18nAttributes, ɵɵi18nExp, @@ -109,6 +110,8 @@ export { ɵɵi18nPostprocess, i18nConfigureLocalize, ɵɵi18nLocalize, + getLocaleId, + setLocaleId, } from './i18n'; export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref'; diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index c7c758b489..dc9b35f406 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -99,7 +99,6 @@ export function compileNgModuleDefs(moduleType: NgModuleType, ngModule: NgModule ngDevMode && assertDefined(moduleType, 'Required value moduleType'); ngDevMode && assertDefined(ngModule, 'Required value ngModule'); const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); - let ngModuleDef: any = null; Object.defineProperty(moduleType, NG_MODULE_DEF, { configurable: true, diff --git a/packages/core/src/render3/ng_module_ref.ts b/packages/core/src/render3/ng_module_ref.ts index d445ded545..fce592b1d4 100644 --- a/packages/core/src/render3/ng_module_ref.ts +++ b/packages/core/src/render3/ng_module_ref.ts @@ -17,7 +17,8 @@ import {NgModuleDef} from '../metadata/ng_module'; import {assertDefined} from '../util/assert'; import {stringify} from '../util/stringify'; import {ComponentFactoryResolver} from './component_ref'; -import {getNgModuleDef} from './definition'; +import {getNgLocaleIdDef, getNgModuleDef} from './definition'; +import {setLocaleId} from './i18n'; import {maybeUnwrapFn} from './util/misc_utils'; export interface NgModuleType extends Type { ngModuleDef: NgModuleDef; } @@ -44,6 +45,11 @@ export class NgModuleRef extends viewEngine_NgModuleRef implements Interna ngModuleDef, `NgModule '${stringify(ngModuleType)}' is not a subtype of 'NgModuleType'.`); + const ngLocaleIdDef = getNgLocaleIdDef(ngModuleType); + if (ngLocaleIdDef) { + setLocaleId(ngLocaleIdDef); + } + this._bootstrapComponents = maybeUnwrapFn(ngModuleDef !.bootstrap); const additionalProviders: StaticProvider[] = [ { diff --git a/packages/core/test/BUILD.bazel b/packages/core/test/BUILD.bazel index 99fa8a82cc..bc1d670deb 100644 --- a/packages/core/test/BUILD.bazel +++ b/packages/core/test/BUILD.bazel @@ -18,6 +18,7 @@ ts_library( "//packages/animations/browser", "//packages/animations/browser/testing", "//packages/common", + "//packages/common/locales", "//packages/compiler", "//packages/compiler/testing", "//packages/core", diff --git a/packages/core/test/application_ref_spec.ts b/packages/core/test/application_ref_spec.ts index 8a56680ec3..454b72afad 100644 --- a/packages/core/test/application_ref_spec.ts +++ b/packages/core/test/application_ref_spec.ts @@ -8,15 +8,16 @@ import {DOCUMENT} from '@angular/common'; import {ResourceLoader} from '@angular/compiler'; -import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, InjectionToken, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; +import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, InjectionToken, LOCALE_ID, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; import {ApplicationRef} from '@angular/core/src/application_ref'; import {ErrorHandler} from '@angular/core/src/error_handler'; import {ComponentRef} from '@angular/core/src/linker/component_factory'; +import {getLocaleId} from '@angular/core/src/render3'; import {BrowserModule} from '@angular/platform-browser'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {ivyEnabled} from '@angular/private/testing'; +import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; import {NoopNgZone} from '../src/zone/ng_zone'; import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing'; @@ -326,6 +327,22 @@ class SomeComponent { expect(loadResourceSpy).toHaveBeenCalledTimes(1); expect(loadResourceSpy).toHaveBeenCalledWith('/test-template.html'); }); + + onlyInIvy('We only need to define `LOCALE_ID` for runtime i18n') + .it('should define `LOCALE_ID`', async() => { + @Component({ + selector: 'i18n-app', + templateUrl: '', + }) + class I18nComponent { + } + + const testModule = createModule( + {component: I18nComponent, providers: [{provide: LOCALE_ID, useValue: 'ro'}]}); + await defaultPlatform.bootstrapModule(testModule); + + expect(getLocaleId()).toEqual('ro'); + }); }); describe('bootstrapModuleFactory', () => { diff --git a/packages/core/test/i18n_integration_spec.ts b/packages/core/test/i18n_integration_spec.ts index af09a356a7..baa959e024 100644 --- a/packages/core/test/i18n_integration_spec.ts +++ b/packages/core/test/i18n_integration_spec.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ContentChild, ContentChildren, Directive, QueryList, TemplateRef, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; +import {registerLocaleData} from '@angular/common'; +import {Component, ContentChild, ContentChildren, Directive, LOCALE_ID, QueryList, TemplateRef, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; +import localeRo from '@angular/common/locales/ro'; import {TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {onlyInIvy} from '@angular/private/testing'; @@ -637,4 +639,45 @@ onlyInIvy('Ivy i18n logic').describe('i18n', function() { expect(child.innerHTML).toBe(`
  • Section 1
  • Section 2
  • Section 3
  • `); } }); + + it('should return the correct plural form for ICU expressions when using a specific locale', + () => { + registerLocaleData(localeRo); + TestBed.configureTestingModule({providers: [{provide: LOCALE_ID, useValue: 'ro'}]}); + // We could also use `TestBed.overrideProvider(LOCALE_ID, {useValue: 'ro'});` + const template = ` + {count, plural, + =0 {no email} + =one {one email} + =few {a few emails} + =other {lots of emails} + }`; + const fixture = getFixtureWithOverrides({template}); + + expect(fixture.nativeElement.innerHTML).toEqual('a few emails'); + + // Change detection cycle, no model changes + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('a few emails'); + + fixture.componentInstance.count = 0; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('no email'); + + fixture.componentInstance.count = 1; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('one email'); + + fixture.componentInstance.count = 10; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('a few emails'); + + fixture.componentInstance.count = 20; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('lots of emails'); + + fixture.componentInstance.count = 0; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('no email'); + }); }); diff --git a/packages/core/test/render3/BUILD.bazel b/packages/core/test/render3/BUILD.bazel index 2d769b430e..63749d9109 100644 --- a/packages/core/test/render3/BUILD.bazel +++ b/packages/core/test/render3/BUILD.bazel @@ -20,6 +20,7 @@ ts_library( "//packages/animations/browser", "//packages/animations/browser/testing", "//packages/common", + "//packages/common/locales", "//packages/compiler", "//packages/core", "//packages/core/src/di/interface", diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 8f47b2509a..0a687a4d86 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgForOfContext} from '@angular/common'; +import {NgForOfContext, registerLocaleData} from '@angular/common'; +import localeRo from '@angular/common/locales/ro'; import {noop} from '../../../compiler/src/render3/view/util'; -import {Component as _Component} from '../../src/core'; +import {Component as _Component, ɵNgModuleDef as NgModuleDef, ɵɵdefineInjector} from '../../src/core'; import {ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/definition'; import {getTranslationForTemplate, ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n'; import {ɵɵallocHostVars, ɵɵbind, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵnextContext, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; @@ -17,8 +18,8 @@ import {RenderFlags} from '../../src/render3/interfaces/definition'; import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n} from '../../src/render3/interfaces/i18n'; import {AttributeMarker} from '../../src/render3/interfaces/node'; import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view'; +import {NgModuleFactory} from '../../src/render3/ng_module_ref'; import {getNativeByIndex, getTNode} from '../../src/render3/util/view_utils'; - import {NgForOf, NgIf} from './common_with_def'; import {ComponentFixture, TemplateFixture} from './render_util'; @@ -2259,4 +2260,57 @@ describe('Runtime i18n', () => { }); }); }); + + it('should return the correct plural form for ICU expressions when using a specific locale', + () => { + registerLocaleData(localeRo); + const MSG_DIV = `{�0�, plural, + =0 {no email} + =one {one email} + =few {a few emails} + =other {lots of emails} + }`; + const ctx = {value: 0}; + + class MyAppModule { + static ngLocaleIdDef = 'ro'; + static ngInjectorDef = ɵɵdefineInjector({factory: () => new MyAppModule()}); + static ngModuleDef: NgModuleDef = { bootstrap: [] } as any; + } + const myAppModuleFactory = new NgModuleFactory(MyAppModule); + const ngModuleRef = myAppModuleFactory.create(null); + + const fixture = prepareFixture( + () => { + ɵɵelementStart(0, 'div'); + ɵɵi18n(1, MSG_DIV); + ɵɵelementEnd(); + }, + () => { + ɵɵi18nExp(ɵɵbind(ctx.value)); + ɵɵi18nApply(1); + }, + 2, 1); + expect(fixture.html).toEqual('
    no email
    '); + + // Change detection cycle, no model changes + fixture.update(); + expect(fixture.html).toEqual('
    no email
    '); + + ctx.value = 1; + fixture.update(); + expect(fixture.html).toEqual('
    one email
    '); + + ctx.value = 10; + fixture.update(); + expect(fixture.html).toEqual('
    a few emails
    '); + + ctx.value = 20; + fixture.update(); + expect(fixture.html).toEqual('
    lots of emails
    '); + + ctx.value = 0; + fixture.update(); + expect(fixture.html).toEqual('
    no email
    '); + }); }); diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts index aac9429371..39aa31fb62 100644 --- a/packages/core/testing/src/r3_test_bed_compiler.ts +++ b/packages/core/testing/src/r3_test_bed_compiler.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationInitStatus, COMPILER_OPTIONS, Compiler, Component, Directive, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgZone, Injector, Pipe, PlatformRef, Provider, Type, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵgetInjectableDef as getInjectableDef, ɵNG_COMPONENT_DEF as NG_COMPONENT_DEF, ɵNG_DIRECTIVE_DEF as NG_DIRECTIVE_DEF, ɵNG_INJECTOR_DEF as NG_INJECTOR_DEF, ɵNG_MODULE_DEF as NG_MODULE_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵɵInjectableDef as InjectableDef, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵDirectiveDef as DirectiveDef, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵtransitiveScopesFor as transitiveScopesFor,} from '@angular/core'; import {ResourceLoader} from '@angular/compiler'; +import {ApplicationInitStatus, COMPILER_OPTIONS, Compiler, Component, Directive, Injector, LOCALE_ID, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, Type, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDirectiveDef as DirectiveDef, ɵNG_COMPONENT_DEF as NG_COMPONENT_DEF, ɵNG_DIRECTIVE_DEF as NG_DIRECTIVE_DEF, ɵNG_INJECTOR_DEF as NG_INJECTOR_DEF, ɵNG_MODULE_DEF as NG_MODULE_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵgetInjectableDef as getInjectableDef, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵɵInjectableDef as InjectableDef} from '@angular/core'; -import {clearResolutionOfComponentResourcesQueue, restoreComponentResolutionQueue, resolveComponentResources, isComponentDefPendingResolution} from '../../src/metadata/resource_loading'; +import {clearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution, resolveComponentResources, restoreComponentResolutionQueue} from '../../src/metadata/resource_loading'; import {MetadataOverride} from './metadata_override'; import {ComponentResolver, DirectiveResolver, NgModuleResolver, PipeResolver, Resolver} from './resolvers'; @@ -227,6 +227,9 @@ export class R3TestBedCompiler { const parentInjector = this.platform.injector; this.testModuleRef = new NgModuleRef(this.testModuleType, parentInjector); + // Set the locale ID, it can be overridden for the tests + const localeId = this.testModuleRef.injector.get(LOCALE_ID, DEFAULT_LOCALE_ID); + setLocaleId(localeId); // ApplicationInitStatus.runInitializers() is marked @internal to core. // Cast it to any before accessing it. @@ -501,6 +504,8 @@ export class R3TestBedCompiler { this.initialNgDefs.clear(); this.moduleProvidersOverridden.clear(); this.restoreComponentResolutionQueue(); + // Restore the locale ID to the default value, this shouldn't be necessary but we never know + setLocaleId(DEFAULT_LOCALE_ID); } private compileTestModule(): void { diff --git a/tools/gulp-tasks/cldr/extract.js b/tools/gulp-tasks/cldr/extract.js index 3c0e0ce16c..1a92bc8fd2 100644 --- a/tools/gulp-tasks/cldr/extract.js +++ b/tools/gulp-tasks/cldr/extract.js @@ -14,11 +14,14 @@ const cldr = require('cldr'); // used to extract all other cldr data const cldrJs = require('cldrjs'); -const PACKAGE_FOLDER = 'packages/common'; -const I18N_FOLDER = `${PACKAGE_FOLDER}/src/i18n`; -const I18N_DATA_FOLDER = `${PACKAGE_FOLDER}/locales`; +const COMMON_PACKAGE = 'packages/common'; +const CORE_PACKAGE = 'packages/core'; +const I18N_FOLDER = `${COMMON_PACKAGE}/src/i18n`; +const I18N_CORE_FOLDER = `${CORE_PACKAGE}/src/i18n`; +const I18N_DATA_FOLDER = `${COMMON_PACKAGE}/locales`; const I18N_DATA_EXTRA_FOLDER = `${I18N_DATA_FOLDER}/extra`; const RELATIVE_I18N_FOLDER = path.resolve(__dirname, `../../../${I18N_FOLDER}`); +const RELATIVE_I18N_CORE_FOLDER = path.resolve(__dirname, `../../../${I18N_CORE_FOLDER}`); const RELATIVE_I18N_DATA_FOLDER = path.resolve(__dirname, `../../../${I18N_DATA_FOLDER}`); const RELATIVE_I18N_DATA_EXTRA_FOLDER = path.resolve(__dirname, `../../../${I18N_DATA_EXTRA_FOLDER}`); const DEFAULT_RULE = 'function anonymous(n\n/*``*/) {\nreturn"other"\n}'; @@ -60,9 +63,9 @@ module.exports = (gulp, done) => { const baseCurrencies = generateBaseCurrencies(new cldrJs('en')); // additional "en" file that will be included in common - console.log(`Writing file ${I18N_FOLDER}/locale_en.ts`); + console.log(`Writing file ${I18N_CORE_FOLDER}/locale_en.ts`); const localeEnFile = generateLocale('en', new cldrJs('en'), baseCurrencies); - fs.writeFileSync(`${RELATIVE_I18N_FOLDER}/locale_en.ts`, localeEnFile); + fs.writeFileSync(`${RELATIVE_I18N_CORE_FOLDER}/locale_en.ts`, localeEnFile); LOCALES.forEach((locale, index) => { const localeData = new cldrJs(locale); @@ -82,7 +85,7 @@ module.exports = (gulp, done) => { .src([ `${I18N_DATA_FOLDER}/**/*.ts`, `${I18N_FOLDER}/currencies.ts`, - `${I18N_FOLDER}/locale_en.ts` + `${I18N_CORE_FOLDER}/locale_en.ts` ], {base: '.'}) .pipe(format.format('file', clangFormat)) .pipe(gulp.dest('.')); diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index 6ca180685f..9ee2ea35e3 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -103,7 +103,7 @@ export declare function getLocaleNumberFormat(locale: string, type: NumberFormat export declare function getLocaleNumberSymbol(locale: string, symbol: NumberSymbol): string; -export declare function getLocalePluralCase(locale: string): (value: number) => Plural; +export declare const getLocalePluralCase: (locale: string) => ((value: number) => Plural); export declare function getLocaleTimeFormat(locale: string, width: FormatWidth): string;