refactor(i18n): remove circular dep
This commit is contained in:
parent
8c9c0986e9
commit
74b57dfa7d
|
@ -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 tag name="${ph.startName}"/>` :
|
||||||
|
`<ph tag name="${ph.startName}">${ph.children.map(child => child.visit(this)).join(', ')}</ph name="${ph.closeName}">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitPlaceholder(ph: i18n.Placeholder, context: any): any {
|
||||||
|
return `<ph name="${ph.name}">${ph.value}</ph>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
|
||||||
|
return `<ph icu name="${ph.name}">${ph.value.visit(this)}</ph>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializerVisitor = new _SerializerVisitor();
|
||||||
|
|
||||||
|
export function serializeNodes(nodes: i18n.Node[]): string[] {
|
||||||
|
return nodes.map(a => a.visit(serializerVisitor, null));
|
||||||
|
}
|
|
@ -9,9 +9,9 @@
|
||||||
import * as html from '../ml_parser/ast';
|
import * as html from '../ml_parser/ast';
|
||||||
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||||
|
|
||||||
|
import {digestMessage} from './digest';
|
||||||
import * as i18n from './i18n_ast';
|
import * as i18n from './i18n_ast';
|
||||||
import * as i18nParser from './i18n_parser';
|
import {createI18nMessageFactory} from './i18n_parser';
|
||||||
import * as msgBundle from './message_bundle';
|
|
||||||
import {I18nError} from './parse_util';
|
import {I18nError} from './parse_util';
|
||||||
import {TranslationBundle} from './translation_bundle';
|
import {TranslationBundle} from './translation_bundle';
|
||||||
|
|
||||||
|
@ -299,7 +299,7 @@ class _Visitor implements html.Visitor {
|
||||||
this._msgCountAtSectionStart = void 0;
|
this._msgCountAtSectionStart = void 0;
|
||||||
this._errors = [];
|
this._errors = [];
|
||||||
this._messages = [];
|
this._messages = [];
|
||||||
this._createI18nMessage = i18nParser.createI18nMessageFactory(interpolationConfig);
|
this._createI18nMessage = createI18nMessageFactory(interpolationConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// looks for translatable attributes
|
// looks for translatable attributes
|
||||||
|
@ -338,7 +338,7 @@ class _Visitor implements html.Visitor {
|
||||||
// translate the given message given the `TranslationBundle`
|
// translate the given message given the `TranslationBundle`
|
||||||
private _translateMessage(el: html.Node, message: i18n.Message): html.Node[] {
|
private _translateMessage(el: html.Node, message: i18n.Message): html.Node[] {
|
||||||
if (message && this._mode === _VisitorMode.Merge) {
|
if (message && this._mode === _VisitorMode.Merge) {
|
||||||
const id = msgBundle.digestMessage(message);
|
const id = digestMessage(message);
|
||||||
const nodes = this._translations.get(id);
|
const nodes = this._translations.get(id);
|
||||||
|
|
||||||
if (nodes) {
|
if (nodes) {
|
||||||
|
@ -374,15 +374,19 @@ class _Visitor implements html.Visitor {
|
||||||
if (i18nAttributeMeanings.hasOwnProperty(attr.name)) {
|
if (i18nAttributeMeanings.hasOwnProperty(attr.name)) {
|
||||||
const meaning = i18nAttributeMeanings[attr.name];
|
const meaning = i18nAttributeMeanings[attr.name];
|
||||||
const message: i18n.Message = this._createI18nMessage([attr], meaning, '');
|
const message: i18n.Message = this._createI18nMessage([attr], meaning, '');
|
||||||
const id = msgBundle.digestMessage(message);
|
const id = digestMessage(message);
|
||||||
const nodes = this._translations.get(id);
|
const nodes = this._translations.get(id);
|
||||||
if (!nodes) {
|
if (nodes) {
|
||||||
this._reportError(
|
|
||||||
el, `Translation unavailable for attribute "${attr.name}" (id="${id}")`);
|
|
||||||
}
|
|
||||||
if (nodes[0] instanceof html.Text) {
|
if (nodes[0] instanceof html.Text) {
|
||||||
const value = (nodes[0] as html.Text).value;
|
const value = (nodes[0] as html.Text).value;
|
||||||
translatedAttributes.push(new html.Attribute(attr.name, value, attr.sourceSpan));
|
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}")`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
translatedAttributes.push(attr);
|
translatedAttributes.push(attr);
|
||||||
|
|
|
@ -10,15 +10,17 @@ import {HtmlParser} from '../ml_parser/html_parser';
|
||||||
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||||
import {ParseError} from '../parse_util';
|
import {ParseError} from '../parse_util';
|
||||||
|
|
||||||
import * as extractor from './extractor_merger';
|
import {digestMessage} from './digest';
|
||||||
import * as i18n from './i18n_ast';
|
import {extractMessages} from './extractor_merger';
|
||||||
|
import {Message} from './i18n_ast';
|
||||||
import {Serializer} from './serializers/serializer';
|
import {Serializer} from './serializers/serializer';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A container for message extracted from the templates.
|
* A container for message extracted from the templates.
|
||||||
*/
|
*/
|
||||||
export class MessageBundle {
|
export class MessageBundle {
|
||||||
private _messageMap: {[id: string]: i18n.Message} = {};
|
private _messageMap: {[id: string]: Message} = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _htmlParser: HtmlParser, private _implicitTags: string[],
|
private _htmlParser: HtmlParser, private _implicitTags: string[],
|
||||||
|
@ -32,7 +34,7 @@ export class MessageBundle {
|
||||||
return htmlParserResult.errors;
|
return htmlParserResult.errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18nParserResult = extractor.extractMessages(
|
const i18nParserResult = extractMessages(
|
||||||
htmlParserResult.rootNodes, interpolationConfig, this._implicitTags, this._implicitAttrs);
|
htmlParserResult.rootNodes, interpolationConfig, this._implicitTags, this._implicitAttrs);
|
||||||
|
|
||||||
if (i18nParserResult.errors.length) {
|
if (i18nParserResult.errors.length) {
|
||||||
|
@ -45,69 +47,3 @@ export class MessageBundle {
|
||||||
|
|
||||||
write(serializer: Serializer): string { return serializer.write(this._messageMap); }
|
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 tag name="${ph.startName}"/>` :
|
|
||||||
`<ph tag name="${ph.startName}">${ph.children.map(child => child.visit(this)).join(', ')}</ph name="${ph.closeName}">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
visitPlaceholder(ph: i18n.Placeholder, context: any): any {
|
|
||||||
return `<ph name="${ph.name}">${ph.value}</ph>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
|
|
||||||
return `<ph icu name="${ph.name}">${ph.value.visit(this)}</ph>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializerVisitor = new _SerializerVisitor();
|
|
||||||
|
|
||||||
export function serializeNodes(nodes: i18n.Node[]): string[] {
|
|
||||||
return nodes.map(a => a.visit(serializerVisitor, null));
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
|
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 {extractMessages, mergeTranslations} from '../../src/i18n/extractor_merger';
|
||||||
import * as i18n from '../../src/i18n/i18n_ast';
|
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 {TranslationBundle} from '../../src/i18n/translation_bundle';
|
||||||
import * as html from '../../src/ml_parser/ast';
|
import * as html from '../../src/ml_parser/ast';
|
||||||
import {HtmlParser} from '../../src/ml_parser/html_parser';
|
import {HtmlParser} from '../../src/ml_parser/html_parser';
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {extractMessages} from '@angular/compiler/src/i18n/extractor_merger';
|
||||||
import {Message} from '@angular/compiler/src/i18n/i18n_ast';
|
import {Message} from '@angular/compiler/src/i18n/i18n_ast';
|
||||||
import {ddescribe, describe, expect, iit, it} from '@angular/core/testing/testing_internal';
|
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 {HtmlParser} from '../../src/ml_parser/html_parser';
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
|
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ import * as i18n from '@angular/compiler/src/i18n/i18n_ast';
|
||||||
import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer';
|
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 {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 {HtmlParser} from '../../src/ml_parser/html_parser';
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
|
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,15 +56,3 @@ class _TestSerializer implements Serializer {
|
||||||
function humanizeMessages(catalog: MessageBundle): string[] {
|
function humanizeMessages(catalog: MessageBundle): string[] {
|
||||||
return catalog.write(new _TestSerializer()).split('//');
|
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);
|
|
||||||
}
|
|
Loading…
Reference in New Issue