feat(ivy): use i18n locale data to determine the plural form of ICU expressions (#29249)

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
This commit is contained in:
Olivier Combe 2019-05-17 16:13:31 +02:00 committed by Matias Niemelä
parent f5b0c8a323
commit 5e0f982961
26 changed files with 288 additions and 450 deletions

View File

@ -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
*/

View File

@ -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.

View File

@ -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', () => {

View File

@ -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';
@ -18,7 +19,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';
@ -43,7 +43,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry,
private referencesRegistry: ReferencesRegistry, private isCore: boolean,
private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter,
private defaultImportRecorder: DefaultImportRecorder) {}
private defaultImportRecorder: DefaultImportRecorder, private localeId?: string) {}
readonly precedence = HandlerPrecedence.PRIMARY;
@ -258,7 +258,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
ngModuleStatements.push(callExpr.toStmt());
}
}
return [
const res: CompileResult[] = [
{
name: 'ngModuleDef',
initializer: ngModuleDef.expression,
@ -270,8 +270,19 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
initializer: ngInjectorDef.expression,
statements: ngInjectorDef.statements,
type: ngInjectorDef.type,
},
}
];
if (this.localeId) {
res.push({
name: 'ngLocaleIdDef',
initializer: new LiteralExpr(this.localeId),
statements: [],
type: STRING_TYPE
});
}
return res;
}
private _toR3Reference(

View File

@ -501,7 +501,8 @@ export class NgtscProgram implements api.Program {
this.options.strictInjectionParameters || false),
new NgModuleDecoratorHandler(
this.reflector, evaluator, metaRegistry, scopeRegistry, referencesRegistry, this.isCore,
this.routeAnalyzer, this.refEmitter, this.defaultImportTracker),
this.routeAnalyzer, this.refEmitter, this.defaultImportTracker,
this.options.i18nInLocale),
new PipeDecoratorHandler(
this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
];

View File

@ -15,6 +15,7 @@ import {getCompilerFacade} from './compiler/compiler_facade';
import {Console} from './console';
import {Injectable, InjectionToken, Injector, StaticProvider} from './di';
import {ErrorHandler} from './error_handler';
import {LOCALE_ID} from './i18n/tokens';
import {Type} from './interface/type';
import {COMPILER_OPTIONS, CompilerFactory, CompilerOptions} from './linker/compiler';
import {ComponentFactory, ComponentRef} from './linker/component_factory';
@ -25,6 +26,7 @@ import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from
import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
import {assertNgModuleType} from './render3/assert';
import {ComponentFactory as R3ComponentFactory} from './render3/component_ref';
import {DEFAULT_LOCALE_ID, setLocaleId} from './render3/i18n';
import {NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref';
import {Testability, TestabilityRegistry} from './testability/testability';
import {isDevMode} from './util/is_dev_mode';
@ -261,6 +263,9 @@ export class PlatformRef {
if (!exceptionHandler) {
throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?');
}
// If the `LOCALE_ID` provider is defined at bootstrap we set the value for runtime i18n (ivy)
const localeId = moduleRef.injector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
setLocaleId(localeId);
moduleRef.onDestroy(() => remove(this._modules, moduleRef));
ngZone !.runOutsideAngular(
() => ngZone !.onError.subscribe(

View File

@ -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';

View File

@ -162,6 +162,8 @@ export {
ɵɵi18nPostprocess,
i18nConfigureLocalize as ɵi18nConfigureLocalize,
ɵɵi18nLocalize,
setLocaleId as ɵsetLocaleId,
DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID,
setClassMetadata as ɵsetClassMetadata,
ɵɵresolveWindow,
ɵɵresolveDocument,

View File

@ -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
}

View File

@ -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}".`);
}

View File

@ -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';
}
}

View File

@ -61,7 +61,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.

View File

@ -7,7 +7,6 @@
*/
import '../util/ng_dev_mode';
import {ChangeDetectionStrategy} from '../change_detection/constants';
import {NG_INJECTABLE_DEF, ɵɵdefineInjectable} from '../di/interface/defs';
import {Mutable, Type} from '../interface/type';
@ -16,9 +15,8 @@ import {SchemaMetadata} from '../metadata/schema';
import {ViewEncapsulation} from '../metadata/view';
import {noSideEffects} from '../util/closure';
import {stringify} from '../util/stringify';
import {EMPTY_ARRAY, EMPTY_OBJ} from './empty';
import {NG_BASE_DEF, NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields';
import {NG_BASE_DEF, 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
@ -779,3 +777,7 @@ export function getNgModuleDef<T>(type: any, throwNotFound?: boolean): NgModuleD
}
return ngModuleDef;
}
export function getNgLocaleIdDef(type: any): string|null {
return (type as any)[NG_LOCALE_ID_DEF] || null;
}

View File

@ -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});
/**

View File

@ -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 {assertDataInRange, assertDefined, assertEqual, assertGreaterThan} from '../util/assert';
import {attachPatchData} from './context_discovery';
import {attachI18nOpCodesDebug} from './debug';
import {ɵɵelementAttribute, ɵɵload, ɵɵtextBinding} from './instructions/all';
@ -1020,351 +1019,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(<string>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
*
@ -1376,9 +1030,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');
@ -1624,7 +1276,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} = {}) {
@ -1635,3 +1287,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;
}

View File

@ -131,6 +131,7 @@ export {
} from './state';
export {
DEFAULT_LOCALE_ID,
ɵɵi18n,
ɵɵi18nAttributes,
ɵɵi18nExp,
@ -140,6 +141,8 @@ export {
ɵɵi18nPostprocess,
i18nConfigureLocalize,
ɵɵi18nLocalize,
getLocaleId,
setLocaleId,
} from './i18n';
export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref';

View File

@ -104,7 +104,6 @@ export function compileNgModuleDefs(
ngDevMode && assertDefined(moduleType, 'Required value moduleType');
ngDevMode && assertDefined(ngModule, 'Required value ngModule');
const declarations: Type<any>[] = flatten(ngModule.declarations || EMPTY_ARRAY);
let ngModuleDef: any = null;
Object.defineProperty(moduleType, NG_MODULE_DEF, {
configurable: true,

View File

@ -20,7 +20,8 @@ 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<T = any> extends Type<T> { ngModuleDef: NgModuleDef<T>; }
@ -47,6 +48,11 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> 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[] = [
{

View File

@ -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",

View File

@ -13,6 +13,7 @@ ts_library(
"//packages/animations/browser",
"//packages/animations/browser/testing",
"//packages/common",
"//packages/common/locales",
"//packages/compiler",
"//packages/compiler/testing",
"//packages/core",

View File

@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, ContentChild, ContentChildren, Directive, HostBinding, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core';
import {registerLocaleData} from '@angular/common';
import localeRo from '@angular/common/locales/ro';
import {Component, ContentChild, ContentChildren, Directive, HostBinding, LOCALE_ID, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
@ -549,6 +551,46 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
expect(fixture.nativeElement.innerHTML)
.toEqual(`<div>4 animaux<!--nested ICU 0-->!<!--ICU 5--></div>`);
});
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 fixture = initWithTemplate(AppComp, `
{count, plural,
=0 {no email}
=one {one email}
=few {a few emails}
=other {lots of emails}
}`);
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 2-->');
// Change detection cycle, no model changes
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 2-->');
fixture.componentInstance.count = 3;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 2-->');
fixture.componentInstance.count = 1;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('one email<!--ICU 2-->');
fixture.componentInstance.count = 10;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 2-->');
fixture.componentInstance.count = 20;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 2-->');
fixture.componentInstance.count = 0;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 2-->');
});
});
describe('should support attributes', () => {

View File

@ -8,14 +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 {onlyInIvy} from '@angular/private/testing';
import {NoopNgZone} from '../src/zone/ng_zone';
import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
@ -325,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', () => {

View File

@ -6,11 +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';
import {TestModuleMetadata} from './test_bed_common';
@ -238,6 +237,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.
@ -523,6 +525,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 {

View File

@ -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('.'));

View File

@ -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;