From 74b57dfa7d1aafd79ee385b8274537b1aa83a5c2 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Fri, 5 Aug 2016 12:08:43 -0700 Subject: [PATCH] refactor(i18n): remove circular dep --- modules/@angular/compiler/src/i18n/digest.ts | 75 ++++++++++++++++++ .../compiler/src/i18n/extractor_merger.ts | 24 +++--- .../compiler/src/i18n/message_bundle.ts | 76 ++----------------- .../compiler/test/i18n/digest_spec.ts | 38 ++++++++++ .../test/i18n/extractor_merger_spec.ts | 2 +- .../compiler/test/i18n/i18n_parser_spec.ts | 2 +- .../compiler/test/i18n/message_bundle_spec.ts | 29 +------ 7 files changed, 137 insertions(+), 109 deletions(-) create mode 100644 modules/@angular/compiler/src/i18n/digest.ts create mode 100644 modules/@angular/compiler/test/i18n/digest_spec.ts diff --git a/modules/@angular/compiler/src/i18n/digest.ts b/modules/@angular/compiler/src/i18n/digest.ts new file mode 100644 index 0000000000..798e192dfc --- /dev/null +++ b/modules/@angular/compiler/src/i18n/digest.ts @@ -0,0 +1,75 @@ +/** + * @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 * as i18n from './i18n_ast'; + +export function digestMessage(message: i18n.Message): string { + return strHash(serializeNodes(message.nodes).join('') + `[${message.meaning}]`); +} + +/** + * String hash function similar to java.lang.String.hashCode(). + * The hash code for a string is computed as + * s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1], + * where s[i] is the ith character of the string and n is the length of + * the string. We mod the result to make it between 0 (inclusive) and 2^32 (exclusive). + * + * Based on goog.string.hashCode from the Google Closure library + * https://github.com/google/closure-library/ + * + * @internal + */ +// TODO(vicb): better algo (less collisions) ? +export function strHash(str: string): string { + let result: number = 0; + for (var i = 0; i < str.length; ++i) { + // Normalize to 4 byte range, 0 ... 2^32. + result = (31 * result + str.charCodeAt(i)) >>> 0; + } + return result.toString(16); +} + +/** + * Serialize the i18n ast to something xml-like in order to generate an UID. + * + * The visitor is also used in the i18n parser tests + * + * @internal + */ +class _SerializerVisitor implements i18n.Visitor { + visitText(text: i18n.Text, context: any): any { return text.value; } + + visitContainer(container: i18n.Container, context: any): any { + return `[${container.children.map(child => child.visit(this)).join(', ')}]`; + } + + visitIcu(icu: i18n.Icu, context: any): any { + let strCases = Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`); + return `{${icu.expression}, ${icu.type}, ${strCases.join(', ')}}`; + } + + visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any { + return ph.isVoid ? + `` : + `${ph.children.map(child => child.visit(this)).join(', ')}`; + } + + visitPlaceholder(ph: i18n.Placeholder, context: any): any { + return `${ph.value}`; + } + + visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { + return `${ph.value.visit(this)}`; + } +} + +const serializerVisitor = new _SerializerVisitor(); + +export function serializeNodes(nodes: i18n.Node[]): string[] { + return nodes.map(a => a.visit(serializerVisitor, null)); +} diff --git a/modules/@angular/compiler/src/i18n/extractor_merger.ts b/modules/@angular/compiler/src/i18n/extractor_merger.ts index 9f367e143e..45900b276b 100644 --- a/modules/@angular/compiler/src/i18n/extractor_merger.ts +++ b/modules/@angular/compiler/src/i18n/extractor_merger.ts @@ -9,9 +9,9 @@ import * as html from '../ml_parser/ast'; import {InterpolationConfig} from '../ml_parser/interpolation_config'; +import {digestMessage} from './digest'; import * as i18n from './i18n_ast'; -import * as i18nParser from './i18n_parser'; -import * as msgBundle from './message_bundle'; +import {createI18nMessageFactory} from './i18n_parser'; import {I18nError} from './parse_util'; import {TranslationBundle} from './translation_bundle'; @@ -299,7 +299,7 @@ class _Visitor implements html.Visitor { this._msgCountAtSectionStart = void 0; this._errors = []; this._messages = []; - this._createI18nMessage = i18nParser.createI18nMessageFactory(interpolationConfig); + this._createI18nMessage = createI18nMessageFactory(interpolationConfig); } // looks for translatable attributes @@ -338,7 +338,7 @@ class _Visitor implements html.Visitor { // translate the given message given the `TranslationBundle` private _translateMessage(el: html.Node, message: i18n.Message): html.Node[] { if (message && this._mode === _VisitorMode.Merge) { - const id = msgBundle.digestMessage(message); + const id = digestMessage(message); const nodes = this._translations.get(id); if (nodes) { @@ -374,16 +374,20 @@ class _Visitor implements html.Visitor { if (i18nAttributeMeanings.hasOwnProperty(attr.name)) { const meaning = i18nAttributeMeanings[attr.name]; const message: i18n.Message = this._createI18nMessage([attr], meaning, ''); - const id = msgBundle.digestMessage(message); + const id = digestMessage(message); const nodes = this._translations.get(id); - if (!nodes) { + if (nodes) { + if (nodes[0] instanceof html.Text) { + const value = (nodes[0] as html.Text).value; + translatedAttributes.push(new html.Attribute(attr.name, value, attr.sourceSpan)); + } else { + this._reportError( + el, `Unexpected translation for attribute "${attr.name}" (id="${id}")`); + } + } else { this._reportError( el, `Translation unavailable for attribute "${attr.name}" (id="${id}")`); } - if (nodes[0] instanceof html.Text) { - const value = (nodes[0] as html.Text).value; - translatedAttributes.push(new html.Attribute(attr.name, value, attr.sourceSpan)); - } } else { translatedAttributes.push(attr); } diff --git a/modules/@angular/compiler/src/i18n/message_bundle.ts b/modules/@angular/compiler/src/i18n/message_bundle.ts index 3cd1d09d10..e3b7dbb0d1 100644 --- a/modules/@angular/compiler/src/i18n/message_bundle.ts +++ b/modules/@angular/compiler/src/i18n/message_bundle.ts @@ -10,15 +10,17 @@ import {HtmlParser} from '../ml_parser/html_parser'; import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {ParseError} from '../parse_util'; -import * as extractor from './extractor_merger'; -import * as i18n from './i18n_ast'; +import {digestMessage} from './digest'; +import {extractMessages} from './extractor_merger'; +import {Message} from './i18n_ast'; import {Serializer} from './serializers/serializer'; + /** * A container for message extracted from the templates. */ export class MessageBundle { - private _messageMap: {[id: string]: i18n.Message} = {}; + private _messageMap: {[id: string]: Message} = {}; constructor( private _htmlParser: HtmlParser, private _implicitTags: string[], @@ -32,7 +34,7 @@ export class MessageBundle { return htmlParserResult.errors; } - const i18nParserResult = extractor.extractMessages( + const i18nParserResult = extractMessages( htmlParserResult.rootNodes, interpolationConfig, this._implicitTags, this._implicitAttrs); if (i18nParserResult.errors.length) { @@ -45,69 +47,3 @@ export class MessageBundle { write(serializer: Serializer): string { return serializer.write(this._messageMap); } } - -export function digestMessage(message: i18n.Message): string { - return strHash(serializeNodes(message.nodes).join('') + `[${message.meaning}]`); -} - -/** - * String hash function similar to java.lang.String.hashCode(). - * The hash code for a string is computed as - * s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1], - * where s[i] is the ith character of the string and n is the length of - * the string. We mod the result to make it between 0 (inclusive) and 2^32 (exclusive). - * - * Based on goog.string.hashCode from the Google Closure library - * https://github.com/google/closure-library/ - * - * @internal - */ -// TODO(vicb): better algo (less collisions) ? -export function strHash(str: string): string { - let result: number = 0; - for (var i = 0; i < str.length; ++i) { - // Normalize to 4 byte range, 0 ... 2^32. - result = (31 * result + str.charCodeAt(i)) >>> 0; - } - return result.toString(16); -} - -/** - * Serialize the i18n ast to something xml-like in order to generate an UID. - * - * The visitor is also used in the i18n parser tests - * - * @internal - */ -class _SerializerVisitor implements i18n.Visitor { - visitText(text: i18n.Text, context: any): any { return text.value; } - - visitContainer(container: i18n.Container, context: any): any { - return `[${container.children.map(child => child.visit(this)).join(', ')}]`; - } - - visitIcu(icu: i18n.Icu, context: any): any { - let strCases = Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`); - return `{${icu.expression}, ${icu.type}, ${strCases.join(', ')}}`; - } - - visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any { - return ph.isVoid ? - `` : - `${ph.children.map(child => child.visit(this)).join(', ')}`; - } - - visitPlaceholder(ph: i18n.Placeholder, context: any): any { - return `${ph.value}`; - } - - visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { - return `${ph.value.visit(this)}`; - } -} - -const serializerVisitor = new _SerializerVisitor(); - -export function serializeNodes(nodes: i18n.Node[]): string[] { - return nodes.map(a => a.visit(serializerVisitor, null)); -} diff --git a/modules/@angular/compiler/test/i18n/digest_spec.ts b/modules/@angular/compiler/test/i18n/digest_spec.ts new file mode 100644 index 0000000000..e84477a8ff --- /dev/null +++ b/modules/@angular/compiler/test/i18n/digest_spec.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 + */ + +import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; +import {strHash} from '../../src/i18n/digest'; + +export function main(): void { + describe('strHash', () => { + it('should return a hash value', () => { + // https://github.com/google/closure-library/blob/1fb19a857b96b74e6523f3e9d33080baf25be046/closure/goog/string/string_test.js#L1115 + expectHash('', 0); + expectHash('foo', 101574); + expectHash('\uAAAAfoo', 1301670364); + expectHash('a', 92567585, 5); + expectHash('a', 2869595232, 6); + expectHash('a', 3058106369, 7); + expectHash('a', 312017024, 8); + expectHash('a', 2929737728, 1024); + }); + }); +} + +function expectHash(text: string, decimal: number, repeat: number = 1) { + let acc = text; + for (let i = 1; i < repeat; i++) { + acc += text; + } + + const hash = strHash(acc); + expect(typeof(hash)).toEqual('string'); + expect(hash.length > 0).toBe(true); + expect(parseInt(hash, 16)).toEqual(decimal); +} \ No newline at end of file diff --git a/modules/@angular/compiler/test/i18n/extractor_merger_spec.ts b/modules/@angular/compiler/test/i18n/extractor_merger_spec.ts index 5c61d04d8a..c46b5a9add 100644 --- a/modules/@angular/compiler/test/i18n/extractor_merger_spec.ts +++ b/modules/@angular/compiler/test/i18n/extractor_merger_spec.ts @@ -8,9 +8,9 @@ import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; +import {digestMessage, serializeNodes as serializeI18nNodes} from '../../src/i18n/digest'; import {extractMessages, mergeTranslations} from '../../src/i18n/extractor_merger'; import * as i18n from '../../src/i18n/i18n_ast'; -import {digestMessage, serializeNodes as serializeI18nNodes} from '../../src/i18n/message_bundle'; import {TranslationBundle} from '../../src/i18n/translation_bundle'; import * as html from '../../src/ml_parser/ast'; import {HtmlParser} from '../../src/ml_parser/html_parser'; diff --git a/modules/@angular/compiler/test/i18n/i18n_parser_spec.ts b/modules/@angular/compiler/test/i18n/i18n_parser_spec.ts index 8160b175fc..fbb3e941a9 100644 --- a/modules/@angular/compiler/test/i18n/i18n_parser_spec.ts +++ b/modules/@angular/compiler/test/i18n/i18n_parser_spec.ts @@ -10,7 +10,7 @@ import {extractMessages} from '@angular/compiler/src/i18n/extractor_merger'; import {Message} from '@angular/compiler/src/i18n/i18n_ast'; import {ddescribe, describe, expect, iit, it} from '@angular/core/testing/testing_internal'; -import {serializeNodes} from '../../src/i18n/message_bundle'; +import {serializeNodes} from '../../src/i18n/digest'; import {HtmlParser} from '../../src/ml_parser/html_parser'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config'; diff --git a/modules/@angular/compiler/test/i18n/message_bundle_spec.ts b/modules/@angular/compiler/test/i18n/message_bundle_spec.ts index aeae83ce48..7204ebec04 100644 --- a/modules/@angular/compiler/test/i18n/message_bundle_spec.ts +++ b/modules/@angular/compiler/test/i18n/message_bundle_spec.ts @@ -10,7 +10,8 @@ import * as i18n from '@angular/compiler/src/i18n/i18n_ast'; import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer'; import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; -import {MessageBundle, serializeNodes, strHash} from '../../src/i18n/message_bundle'; +import {serializeNodes} from '../../src/i18n/digest'; +import {MessageBundle} from '../../src/i18n/message_bundle'; import {HtmlParser} from '../../src/ml_parser/html_parser'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config'; @@ -39,20 +40,6 @@ export function main(): void { ]); }); }); - - describe('strHash', () => { - it('should return a hash value', () => { - // https://github.com/google/closure-library/blob/1fb19a857b96b74e6523f3e9d33080baf25be046/closure/goog/string/string_test.js#L1115 - expectHash('', 0); - expectHash('foo', 101574); - expectHash('\uAAAAfoo', 1301670364); - expectHash('a', 92567585, 5); - expectHash('a', 2869595232, 6); - expectHash('a', 3058106369, 7); - expectHash('a', 312017024, 8); - expectHash('a', 2929737728, 1024); - }); - }); }); } @@ -68,16 +55,4 @@ class _TestSerializer implements Serializer { function humanizeMessages(catalog: MessageBundle): string[] { return catalog.write(new _TestSerializer()).split('//'); -} - -function expectHash(text: string, decimal: number, repeat: number = 1) { - let acc = text; - for (let i = 1; i < repeat; i++) { - acc += text; - } - - const hash = strHash(acc); - expect(typeof(hash)).toEqual('string'); - expect(hash.length > 0).toBe(true); - expect(parseInt(hash, 16)).toEqual(decimal); } \ No newline at end of file