diff --git a/modules/@angular/common/index.ts b/modules/@angular/common/index.ts index eaef0d1cda..ef9e7c4982 100644 --- a/modules/@angular/common/index.ts +++ b/modules/@angular/common/index.ts @@ -11,3 +11,4 @@ export * from './src/directives'; export * from './src/forms-deprecated'; export * from './src/common_directives'; export * from './src/location'; +export {NgLocalization} from './src/localization'; \ No newline at end of file diff --git a/modules/@angular/common/src/directives.ts b/modules/@angular/common/src/directives.ts index c9ff57ed90..b1cb237d09 100644 --- a/modules/@angular/common/src/directives.ts +++ b/modules/@angular/common/src/directives.ts @@ -15,7 +15,7 @@ export {CORE_DIRECTIVES} from './directives/core_directives'; export {NgClass} from './directives/ng_class'; export {NgFor} from './directives/ng_for'; export {NgIf} from './directives/ng_if'; -export {NgLocalization, NgPlural, NgPluralCase} from './directives/ng_plural'; +export {NgPlural, NgPluralCase} from './directives/ng_plural'; export {NgStyle} from './directives/ng_style'; export {NgSwitch, NgSwitchCase, NgSwitchDefault} from './directives/ng_switch'; export {NgTemplateOutlet} from './directives/ng_template_outlet'; diff --git a/modules/@angular/common/src/directives/ng_plural.ts b/modules/@angular/common/src/directives/ng_plural.ts index e2f75105d1..3577e64b3e 100644 --- a/modules/@angular/common/src/directives/ng_plural.ts +++ b/modules/@angular/common/src/directives/ng_plural.ts @@ -7,19 +7,10 @@ */ import {AfterContentInit, Attribute, ContentChildren, Directive, Input, QueryList, TemplateRef, ViewContainerRef} from '@angular/core'; - -import {Map} from '../facade/collection'; -import {NumberWrapper, isPresent} from '../facade/lang'; - +import {isPresent} from '../facade/lang'; +import {NgLocalization, getPluralCategory} from '../localization'; import {SwitchView} from './ng_switch'; -const _CATEGORY_DEFAULT = 'other'; - -/** - * @experimental - */ -export abstract class NgLocalization { abstract getPluralCategory(value: any): string; } - /** * `ngPlural` is an i18n directive that displays DOM sub-trees that match the switch expression * value, or failing that, DOM sub-trees that match the switch expression's pluralization category. @@ -35,9 +26,6 @@ export abstract class NgLocalization { abstract getPluralCategory(value: any): s * value matches aren't found and the value maps to its category using the `getPluralCategory` * function provided. * - * If no matching views are found for a switch expression, inner elements marked - * `[ngPluralCase]="other"` will be displayed. - * * ```typescript * class MyLocalization extends NgLocalization { * getPluralCategory(value: any) { @@ -88,7 +76,6 @@ export class NgPluralCase { } } - /** * @experimental */ @@ -96,7 +83,7 @@ export class NgPluralCase { export class NgPlural implements AfterContentInit { private _switchValue: number; private _activeView: SwitchView; - private _caseViews = new Map(); + private _caseViews: {[k: string]: SwitchView} = {}; @ContentChildren(NgPluralCase) cases: QueryList = null; constructor(private _localization: NgLocalization) {} @@ -109,7 +96,7 @@ export class NgPlural implements AfterContentInit { ngAfterContentInit() { this.cases.forEach((pluralCase: NgPluralCase): void => { - this._caseViews.set(this._formatValue(pluralCase), pluralCase._view); + this._caseViews[pluralCase.value] = pluralCase._view; }); this._updateView(); } @@ -118,10 +105,9 @@ export class NgPlural implements AfterContentInit { _updateView(): void { this._clearViews(); - var view: SwitchView = this._caseViews.get(this._switchValue); - if (!isPresent(view)) view = this._getCategoryView(this._switchValue); - - this._activateView(view); + var key = getPluralCategory( + this._switchValue, Object.getOwnPropertyNames(this._caseViews), this._localization); + this._activateView(this._caseViews[key]); } /** @internal */ @@ -135,22 +121,4 @@ export class NgPlural implements AfterContentInit { this._activeView = view; this._activeView.create(); } - - /** @internal */ - _getCategoryView(value: number): SwitchView { - var category: string = this._localization.getPluralCategory(value); - var categoryView: SwitchView = this._caseViews.get(category); - return isPresent(categoryView) ? categoryView : this._caseViews.get(_CATEGORY_DEFAULT); - } - - /** @internal */ - _isValueView(pluralCase: NgPluralCase): boolean { return pluralCase.value[0] === '='; } - - /** @internal */ - _formatValue(pluralCase: NgPluralCase): any { - return this._isValueView(pluralCase) ? this._stripValue(pluralCase.value) : pluralCase.value; - } - - /** @internal */ - _stripValue(value: string): number { return NumberWrapper.parseInt(value.substring(1), 10); } } diff --git a/modules/@angular/common/src/localization.ts b/modules/@angular/common/src/localization.ts new file mode 100644 index 0000000000..5f98c744b4 --- /dev/null +++ b/modules/@angular/common/src/localization.ts @@ -0,0 +1,18 @@ +/** + * @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 { + const nbCase = `=${value}`; + + return cases.indexOf(nbCase) > -1 ? nbCase : ngLocalization.getPluralCategory(value); +} diff --git a/modules/@angular/common/src/pipes/i18n_plural_pipe.ts b/modules/@angular/common/src/pipes/i18n_plural_pipe.ts index 385011367b..16fe952115 100644 --- a/modules/@angular/common/src/pipes/i18n_plural_pipe.ts +++ b/modules/@angular/common/src/pipes/i18n_plural_pipe.ts @@ -7,22 +7,21 @@ */ import {Pipe, PipeTransform} from '@angular/core'; -import {StringWrapper, isPresent, isStringMap} from '../facade/lang'; +import {StringWrapper, isBlank, isStringMap} from '../facade/lang'; +import {NgLocalization, getPluralCategory} from '../localization'; import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception'; const _INTERPOLATION_REGEXP: RegExp = /#/g; /** - * * Maps a value to a string that pluralizes the value properly. * * ## Usage * * expression | i18nPlural:mapping * - * where `expression` is a number and `mapping` is an object that indicates the proper text for - * when the `expression` evaluates to 0, 1, or some other number. You can interpolate the actual - * value into the text using the `#` sign. + * where `expression` is a number and `mapping` is an object that mimics the ICU format, + * see http://userguide.icu-project.org/formatparse/messages * * ## Example * @@ -33,7 +32,7 @@ const _INTERPOLATION_REGEXP: RegExp = /#/g; * * class MyApp { * messages: any[]; - * messageMapping: any = { + * messageMapping: {[k:string]: string} = { * '=0': 'No messages.', * '=1': 'One message.', * 'other': '# messages.' @@ -46,17 +45,17 @@ const _INTERPOLATION_REGEXP: RegExp = /#/g; */ @Pipe({name: 'i18nPlural', pure: true}) export class I18nPluralPipe implements PipeTransform { + constructor(private _localization: NgLocalization) {} + transform(value: number, pluralMap: {[count: string]: string}): string { - var key: string; - var valueStr: string; + if (isBlank(value)) return ''; if (!isStringMap(pluralMap)) { throw new InvalidPipeArgumentException(I18nPluralPipe, pluralMap); } - key = value === 0 || value === 1 ? `=${value}` : 'other'; - valueStr = isPresent(value) ? value.toString() : ''; + const key = getPluralCategory(value, Object.getOwnPropertyNames(pluralMap), this._localization); - return StringWrapper.replaceAll(pluralMap[key], _INTERPOLATION_REGEXP, valueStr); + return StringWrapper.replaceAll(pluralMap[key], _INTERPOLATION_REGEXP, value.toString()); } } diff --git a/modules/@angular/common/src/pipes/i18n_select_pipe.ts b/modules/@angular/common/src/pipes/i18n_select_pipe.ts index 3f63c9ffbd..048eab0bc5 100644 --- a/modules/@angular/common/src/pipes/i18n_select_pipe.ts +++ b/modules/@angular/common/src/pipes/i18n_select_pipe.ts @@ -7,11 +7,9 @@ */ import {Pipe, PipeTransform} from '@angular/core'; -import {StringMapWrapper} from '../facade/collection'; -import {isStringMap} from '../facade/lang'; +import {isBlank, isStringMap} from '../facade/lang'; import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception'; - /** * * Generic selector that displays the string that matches the current value. @@ -46,10 +44,12 @@ import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception'; @Pipe({name: 'i18nSelect', pure: true}) export class I18nSelectPipe implements PipeTransform { transform(value: string, mapping: {[key: string]: string}): string { + if (isBlank(value)) return ''; + if (!isStringMap(mapping)) { throw new InvalidPipeArgumentException(I18nSelectPipe, mapping); } - return StringMapWrapper.contains(mapping, value) ? mapping[value] : mapping['other']; + return mapping.hasOwnProperty(value) ? mapping[value] : ''; } } diff --git a/modules/@angular/common/test/directives/ng_plural_spec.ts b/modules/@angular/common/test/directives/ng_plural_spec.ts index 1296a1cef8..179400e6b0 100644 --- a/modules/@angular/common/test/directives/ng_plural_spec.ts +++ b/modules/@angular/common/test/directives/ng_plural_spec.ts @@ -15,7 +15,7 @@ import {NgPlural, NgPluralCase, NgLocalization} from '@angular/common'; export function main() { describe('switch', () => { - beforeEachProviders(() => [{provide: NgLocalization, useClass: TestLocalizationMap}]); + beforeEachProviders(() => [{provide: NgLocalization, useClass: TestLocalization}]); it('should display the template according to the exact value', inject( @@ -142,22 +142,21 @@ export function main() { }); } - @Injectable() -export class TestLocalizationMap extends NgLocalization { +class TestLocalization extends NgLocalization { getPluralCategory(value: number): string { if (value > 1 && value < 4) { return 'few'; - } else if (value >= 4 && value < 10) { - return 'many'; - } else { - return 'other'; } + if (value >= 4 && value < 10) { + return 'many'; + } + + return 'other'; } } - -@Component({selector: 'test-cmp', directives: [NgPluralCase, NgPlural], template: ''}) +@Component({selector: 'test-cmp', directives: [NgPlural, NgPluralCase], template: ''}) class TestComponent { switchValue: number = null; } diff --git a/modules/@angular/common/test/pipes/i18n_plural_pipe_spec.ts b/modules/@angular/common/test/pipes/i18n_plural_pipe_spec.ts index 53329a92dd..5f0198bc13 100644 --- a/modules/@angular/common/test/pipes/i18n_plural_pipe_spec.ts +++ b/modules/@angular/common/test/pipes/i18n_plural_pipe_spec.ts @@ -6,21 +6,26 @@ * found in the LICENSE file at https://angular.io/license */ -import {I18nPluralPipe} from '@angular/common'; +import {I18nPluralPipe, NgLocalization} from '@angular/common'; import {PipeResolver} from '@angular/compiler/src/pipe_resolver'; import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '@angular/core/testing/testing_internal'; export function main() { describe('I18nPluralPipe', () => { + var localization: NgLocalization; var pipe: I18nPluralPipe; - var mapping = {'=0': 'No messages.', '=1': 'One message.', 'other': 'There are some messages.'}; - var interpolatedMapping = { + + var mapping = { '=0': 'No messages.', '=1': 'One message.', - 'other': 'There are # messages, that is #.' + 'many': 'Many messages.', + 'other': 'There are # messages, that is #.', }; - beforeEach(() => { pipe = new I18nPluralPipe(); }); + beforeEach(() => { + localization = new TestLocalization(); + pipe = new I18nPluralPipe(localization); + }); it('should be marked as pure', () => { expect(new PipeResolver().resolve(I18nPluralPipe).pure).toEqual(true); }); @@ -36,19 +41,19 @@ export function main() { expect(val).toEqual('One message.'); }); - it('should return other text if value is anything other than 0 or 1', () => { - var val = pipe.transform(6, mapping); - expect(val).toEqual('There are some messages.'); + it('should return category messages', () => { + var val = pipe.transform(4, mapping); + expect(val).toEqual('Many messages.'); }); it('should interpolate the value into the text where indicated', () => { - var val = pipe.transform(6, interpolatedMapping); + var val = pipe.transform(6, mapping); expect(val).toEqual('There are 6 messages, that is 6.'); }); - it('should use \'other\' if value is undefined', () => { - var val = pipe.transform(void(0), interpolatedMapping); - expect(val).toEqual('There are messages, that is .'); + it('should use "" if value is undefined', () => { + var val = pipe.transform(void(0), mapping); + expect(val).toEqual(''); }); it('should not support bad arguments', @@ -57,3 +62,7 @@ export function main() { }); } + +class TestLocalization extends NgLocalization { + getPluralCategory(value: number): string { return value > 1 && value < 6 ? 'many' : 'other'; } +} diff --git a/modules/@angular/common/test/pipes/i18n_select_pipe_spec.ts b/modules/@angular/common/test/pipes/i18n_select_pipe_spec.ts index 046d56e008..bd4874deda 100644 --- a/modules/@angular/common/test/pipes/i18n_select_pipe_spec.ts +++ b/modules/@angular/common/test/pipes/i18n_select_pipe_spec.ts @@ -31,14 +31,14 @@ export function main() { expect(val).toEqual('Invite her.'); }); - it('should return other text if value is anything other than male or female', () => { + it('should return "" if value is anything other than male or female', () => { var val = pipe.transform('Anything else', mapping); - expect(val).toEqual('Invite them.'); + expect(val).toEqual(''); }); - it('should use \'other\' if value is undefined', () => { + it('should use "" if value is undefined', () => { var val = pipe.transform(void(0), mapping); - expect(val).toEqual('Invite them.'); + expect(val).toEqual(''); }); it('should not support bad arguments', diff --git a/modules/@angular/facade/src/lang.ts b/modules/@angular/facade/src/lang.ts index e5a1fcd9b0..03d288c157 100644 --- a/modules/@angular/facade/src/lang.ts +++ b/modules/@angular/facade/src/lang.ts @@ -120,7 +120,7 @@ export function isType(obj: any): boolean { return isFunction(obj); } -export function isStringMap(obj: any): boolean { +export function isStringMap(obj: any): obj is Object { return typeof obj === 'object' && obj !== null; } diff --git a/tools/public_api_guard/public_api_spec.ts b/tools/public_api_guard/public_api_spec.ts index f6bb5f8384..8e4c4eb722 100644 --- a/tools/public_api_guard/public_api_spec.ts +++ b/tools/public_api_guard/public_api_spec.ts @@ -820,6 +820,7 @@ const COMMON = [ 'HashLocationStrategy.pushState(state:any, title:string, path:string, queryParams:string):any', 'HashLocationStrategy.replaceState(state:any, title:string, path:string, queryParams:string):any', 'I18nPluralPipe', + 'I18nPluralPipe.constructor(_localization:NgLocalization)', 'I18nPluralPipe.transform(value:number, pluralMap:{[count:string]:string}):string', 'I18nSelectPipe', 'I18nSelectPipe.transform(value:string, mapping:{[key:string]:string}):string',