ast[0]).value) {
+ // Do not create empty messages
+ return;
+ }
+ messages.push(new AstMessage(ast, meaning(meaningAndDesc), description(meaningAndDesc)));
+ }
+
+ /**
+ * Add the node as a child of the block when:
+ * - we are in a block,
+ * - we are not inside a ICU message (those are handled separately),
+ * - the node is a "direct child" of the block
+ */
+ private _mayBeAddBlockChildren(ast: HtmlAst): void {
+ if (this._inI18nBlock && !this._inIcu && this._depth == this._blockStartDepth) {
+ this._blockChildren.push(ast);
+ }
+ }
+
+ /**
+ * Marks the start of a section, see `_endSection`
+ */
+ private _startSection(messages: AstMessage[]): void {
+ if (this._sectionStartIndex !== void 0) {
+ throw new Error('Unexpected section start');
+ }
+
+ this._sectionStartIndex = messages.length;
+ }
+
+ /**
+ * Terminates a section.
+ *
+ * If a section has only one significant children (comments not significant) then we should not
+ * keep the message
+ * from this children:
+ *
+ * `{ICU message}
` would produce two messages:
+ * - one for the content with meaning and description,
+ * - another one for the ICU message.
+ *
+ * In this case the last message is discarded as it contains less information (the AST is
+ * otherwise identical).
+ *
+ * Note that we should still keep messages extracted from attributes inside the section (ie in the
+ * ICU message here)
+ */
+ private _endSection(messages: AstMessage[], directChildren: HtmlAst[]): void {
+ if (this._sectionStartIndex === void 0) {
+ throw new Error('Unexpected section end');
+ }
+
+ const startIndex = this._sectionStartIndex;
+ const significantChildren: number = directChildren.reduce(
+ (count: number, node: HtmlAst): number => count + (node instanceof HtmlCommentAst ? 0 : 1),
+ 0);
+
+ if (significantChildren == 1) {
+ for (let i = startIndex; i < messages.length; i++) {
+ let ast = messages[i].ast;
+ if (!(ast.length == 1 && ast[0] instanceof HtmlAttrAst)) {
+ messages.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ this._sectionStartIndex = void 0;
+ }
+
+ private _reportError(astNode: HtmlAst, msg: string): void {
+ this._errors.push(new I18nError(astNode.sourceSpan, msg));
+ }
+}
+
+export class AstMessage {
+ constructor(public ast: HtmlAst[], public meaning: string, public description: string) {}
+}
diff --git a/modules/@angular/compiler/src/i18n/shared.ts b/modules/@angular/compiler/src/i18n/shared.ts
index 89c80791df..d4036f8002 100644
--- a/modules/@angular/compiler/src/i18n/shared.ts
+++ b/modules/@angular/compiler/src/i18n/shared.ts
@@ -6,11 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {normalizeBlank} from '../../../router-deprecated/src/facade/lang';
import {Parser as ExpressionParser} from '../expression_parser/parser';
import {StringWrapper, isBlank, isPresent} from '../facade/lang';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_ast';
import {InterpolationConfig} from '../interpolation_config';
import {ParseError, ParseSourceSpan} from '../parse_util';
+
import {Message} from './message';
export const I18N_ATTR = 'i18n';
@@ -31,10 +33,10 @@ export function partition(nodes: HtmlAst[], errors: ParseError[], implicitTags:
let node = nodes[i];
let msgNodes: HtmlAst[] = [];
// Nodes between `` and ``
- if (_isOpeningComment(node)) {
+ if (isOpeningComment(node)) {
let i18n = (node).value.replace(/^i18n:?/, '').trim();
- while (++i < nodes.length && !_isClosingComment(nodes[i])) {
+ while (++i < nodes.length && !isClosingComment(nodes[i])) {
msgNodes.push(nodes[i]);
}
@@ -46,7 +48,7 @@ export function partition(nodes: HtmlAst[], errors: ParseError[], implicitTags:
parts.push(new Part(null, null, msgNodes, i18n, true));
} else if (node instanceof HtmlElementAst) {
// Node with an `i18n` attribute
- let i18n = _findI18nAttr(node);
+ let i18n = getI18nAttr(node);
let hasI18n: boolean = isPresent(i18n) || implicitTags.indexOf(node.name) > -1;
parts.push(new Part(node, null, node.children, isPresent(i18n) ? i18n.value : null, hasI18n));
} else if (node instanceof HtmlTextAst) {
@@ -83,33 +85,27 @@ export class Part {
}
}
-function _isOpeningComment(n: HtmlAst): boolean {
+export function isOpeningComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value.startsWith('i18n');
}
-function _isClosingComment(n: HtmlAst): boolean {
+export function isClosingComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value === '/i18n';
}
-function _findI18nAttr(p: HtmlElementAst): HtmlAttrAst {
- let attrs = p.attrs;
- for (let i = 0; i < attrs.length; i++) {
- if (attrs[i].name === I18N_ATTR) {
- return attrs[i];
- }
- }
- return null;
+export function getI18nAttr(p: HtmlElementAst): HtmlAttrAst {
+ return normalizeBlank(p.attrs.find(attr => attr.name === I18N_ATTR));
}
export function meaning(i18n: string): string {
- if (isBlank(i18n) || i18n == '') return null;
+ if (isBlank(i18n) || i18n == '') return '';
return i18n.split('|')[0];
}
export function description(i18n: string): string {
- if (isBlank(i18n) || i18n == '') return null;
+ if (isBlank(i18n) || i18n == '') return '';
let parts = i18n.split('|', 2);
- return parts.length > 1 ? parts[1] : null;
+ return parts.length > 1 ? parts[1] : '';
}
/**
diff --git a/modules/@angular/compiler/test/html_ast_serializer_spec.ts b/modules/@angular/compiler/test/html_ast_serializer_spec.ts
index 30d19602af..4bb7a91083 100644
--- a/modules/@angular/compiler/test/html_ast_serializer_spec.ts
+++ b/modules/@angular/compiler/test/html_ast_serializer_spec.ts
@@ -3,7 +3,7 @@ import {HtmlParser} from '@angular/compiler/src/html_parser';
import {beforeEach, ddescribe, describe, expect, it} from '@angular/core/testing/testing_internal';
export function main() {
- ddescribe('HtmlAst serilaizer', () => {
+ describe('HtmlAst serilaizer', () => {
var parser: HtmlParser;
beforeEach(() => { parser = new HtmlParser(); });
@@ -54,10 +54,10 @@ export function main() {
class _SerializerVisitor implements HtmlAstVisitor {
visitElement(ast: HtmlElementAst, context: any): any {
- return `<${ast.name}${this._visitAll(ast.attrs)}>${this._visitAll(ast.children)}${ast.name}>`;
+ return `<${ast.name}${this._visitAll(ast.attrs, ' ')}>${this._visitAll(ast.children)}${ast.name}>`;
}
- visitAttr(ast: HtmlAttrAst, context: any): any { return ` ${ast.name}="${ast.value}"`; }
+ visitAttr(ast: HtmlAttrAst, context: any): any { return `${ast.name}="${ast.value}"`; }
visitText(ast: HtmlTextAst, context: any): any { return ast.value; }
@@ -71,11 +71,16 @@ class _SerializerVisitor implements HtmlAstVisitor {
return ` ${ast.value} {${this._visitAll(ast.expression)}}`;
}
- private _visitAll(ast: HtmlAst[]) { return ast.map(a => a.visit(this, null)).join(''); }
+ private _visitAll(ast: HtmlAst[], join: string = ''): string {
+ if (ast.length == 0) {
+ return '';
+ }
+ return join + ast.map(a => a.visit(this, null)).join(join);
+ }
}
const serializerVisitor = new _SerializerVisitor();
-export function serializeHtmlAst(ast: HtmlAst[]) {
+export function serializeHtmlAst(ast: HtmlAst[]): string[] {
return ast.map(a => a.visit(serializerVisitor, null));
}
diff --git a/modules/@angular/compiler/test/i18n/extractor_spec.ts b/modules/@angular/compiler/test/i18n/extractor_spec.ts
new file mode 100644
index 0000000000..55b196bce9
--- /dev/null
+++ b/modules/@angular/compiler/test/i18n/extractor_spec.ts
@@ -0,0 +1,295 @@
+/**
+ * @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 {HtmlParser} from '@angular/compiler/src/html_parser';
+import {ExtractionResult, extractAstMessages} from '@angular/compiler/src/i18n/extractor';
+import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
+
+import {serializeHtmlAst} from '../html_ast_serializer_spec'
+
+export function main() {
+ ddescribe(
+ 'MessageExtractor',
+ () => {
+ function getExtractionResult(
+ html: string, implicitTags: string[],
+ implicitAttrs: {[k: string]: string[]}): ExtractionResult {
+ const htmlParser = new HtmlParser();
+ const parseResult = htmlParser.parse(html, 'extractor spec', true);
+ if (parseResult.errors.length > 1) {
+ throw Error(`unexpected parse errors: ${parseResult.errors.join('\n')}`);
+ }
+
+ return extractAstMessages(parseResult.rootNodes, implicitTags, implicitAttrs);
+ }
+
+ function extract(
+ html: string, implicitTags: string[] = [],
+ implicitAttrs: {[k: string]: string[]} = {}): [string[], string, string][] {
+ const messages = getExtractionResult(html, implicitTags, implicitAttrs).messages;
+
+ // clang-format off
+ // https://github.com/angular/clang-format/issues/35
+ return messages.map(
+ message => [serializeHtmlAst(message.ast), message.meaning, message.description, ]) as [string[], string, string][];
+ // clang-format on
+ }
+
+ function extractErrors(
+ html: string, implicitTags: string[] = [],
+ implicitAttrs: {[k: string]: string[]} = {}): any[] {
+ const errors = getExtractionResult(html, implicitTags, implicitAttrs).errors;
+
+ return errors.map((e): [string, string] => [e.msg, e.span.toString()]);
+ }
+
+ describe('elements', () => {
+ it('should extract from elements', () => {
+ expect(extract('textnested
')).toEqual([
+ [['text', 'nested'], 'm', 'd'],
+ ]);
+ });
+
+ it('should not create a message for empty elements',
+ () => { expect(extract('')).toEqual([]); });
+ });
+
+ describe('blocks', () => {
+ it('should extract from blocks', () => {
+ expect(extract(`message1
+ message2
+ message3`))
+ .toEqual([
+ [['message1'], 'meaning1', 'desc1'],
+ [['message2'], 'meaning2', ''],
+ [['message3'], '', ''],
+ ]);
+ });
+
+ it('should extract all siblings', () => {
+ expect(
+ extract(
+ `texthtmlnested
{count, plural, =0 {html}}{{interp}}`))
+ .toEqual([
+ [['{count, plural, =0 {html}}'], '', ''],
+ [
+ [
+ 'text', 'htmlnested
', '{count, plural, =0 {html}}',
+ '{{interp}}'
+ ],
+ '', ''
+ ],
+ ]);
+ });
+
+ it('should ignore other comments', () => {
+ expect(extract(`message1`))
+ .toEqual([
+ [['message1'], 'meaning1', 'desc1'],
+ ]);
+ });
+
+ it('should not create a message for empty blocks',
+ () => { expect(extract(``)).toEqual([]); });
+ });
+
+ describe('ICU messages', () => {
+ it('should extract ICU messages from translatable elements', () => {
+ // single message when ICU is the only children
+ expect(extract('{count, plural, =0 {text}}
')).toEqual([
+ [['{count, plural, =0 {text}}'], 'm', 'd'],
+ ]);
+
+ // one message for the element content and one message for the ICU
+ expect(extract('before{count, plural, =0 {text}}after
')).toEqual([
+ [['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
+ [['{count, plural, =0 {text}}'], '', ''],
+ ]);
+ });
+
+ it('should extract ICU messages from translatable block', () => {
+ // single message when ICU is the only children
+ expect(extract('{count, plural, =0 {text}}')).toEqual([
+ [['{count, plural, =0 {text}}'], 'm', 'd'],
+ ]);
+
+ // one message for the block content and one message for the ICU
+ expect(extract('before{count, plural, =0 {text}}after'))
+ .toEqual([
+ [['{count, plural, =0 {text}}'], '', ''],
+ [['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
+ ]);
+ });
+
+ it('should not extract ICU messages outside of i18n sections',
+ () => { expect(extract('{count, plural, =0 {text}}')).toEqual([]); });
+
+ it('should not extract nested ICU messages', () => {
+ expect(extract('{count, plural, =0 { {sex, gender, =m {m}} }}
'))
+ .toEqual([
+ [['{count, plural, =0 {{sex, gender, =m {m}} }}'], 'm', 'd'],
+ ]);
+ });
+ });
+
+ describe('attributes', () => {
+ it('should extract from attributes outside of translatable section', () => {
+ expect(extract('')).toEqual([
+ [['title="msg"'], 'm', 'd'],
+ ]);
+ });
+
+ it('should extract from attributes in translatable element', () => {
+ expect(extract('')).toEqual([
+ [['
'], '', ''],
+ [['title="msg"'], 'm', 'd'],
+ ]);
+ });
+
+ it('should extract from attributes in translatable block', () => {
+ expect(
+ extract('
'))
+ .toEqual([
+ [['title="msg"'], 'm', 'd'],
+ [['
'], '', ''],
+ ]);
+ });
+
+ it('should extract from attributes in translatable ICU', () => {
+ expect(
+ extract(
+ '{count, plural, =0 {
}}'))
+ .toEqual([
+ [['title="msg"'], 'm', 'd'],
+ [['{count, plural, =0 {
}}'], '', ''],
+ ]);
+ });
+
+ it('should extract from attributes in non translatable ICU', () => {
+ expect(extract('{count, plural, =0 {
}}'))
+ .toEqual([
+ [['title="msg"'], 'm', 'd'],
+ ]);
+ });
+
+ it('should not create a message for empty attributes',
+ () => { expect(extract('')).toEqual([]); });
+ });
+
+ describe('implicit tags', () => {
+ it('should extract from implicit tags', () => {
+ expect(extract('bolditalic', ['b'])).toEqual([
+ [['bold'], '', ''],
+ ]);
+ });
+ });
+
+ describe('implicit attributes', () => {
+ it('should extract implicit attributes', () => {
+ expect(extract('bolditalic', [], {'b': ['title']}))
+ .toEqual([
+ [['title="bb"'], '', ''],
+ ]);
+ });
+ });
+
+ describe('errors', () => {
+ describe('elements', () => {
+ it('should report nested translatable elements', () => {
+ expect(extractErrors(`
`)).toEqual([
+ [
+ 'Could not mark an element as translatable inside a translatable section',
+ ''
+ ],
+ ]);
+ });
+
+ it('should report translatable elements in implicit elements', () => {
+ expect(extractErrors(`
`, ['p'])).toEqual([
+ [
+ 'Could not mark an element as translatable inside a translatable section',
+ ''
+ ],
+ ]);
+ });
+
+ it('should report translatable elements in translatable blocks', () => {
+ expect(extractErrors(``)).toEqual([
+ [
+ 'Could not mark an element as translatable inside a translatable section',
+ ''
+ ],
+ ]);
+ });
+ });
+
+ describe('blocks', () => {
+ it('should report nested blocks', () => {
+ expect(extractErrors(``))
+ .toEqual([
+ ['Could not start a block inside a translatable section', '`)).toEqual([
+ ['Unclosed block', '
`)).toEqual([
+ ['Could not start a block inside a translatable section', '`, ['p'])).toEqual([
+ ['Could not start a block inside a translatable section', '`)).toEqual([
+ ['I18N blocks should not cross element boundaries', '`)).toEqual([
+ ['I18N blocks should not cross element boundaries', '`, ['b'])).toEqual([
+ ['Could not mark an element as translatable inside a translatable section', ''],
+ ]);
+ });
+ });
+ });
+ });
+}