/** * @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 {AST} from '../../../src/expression_parser/ast'; import {Lexer} from '../../../src/expression_parser/lexer'; import {Parser} from '../../../src/expression_parser/parser'; import * as i18n from '../../../src/i18n/i18n_ast'; import * as o from '../../../src/output/output_ast'; import * as t from '../../../src/render3/r3_ast'; import {I18nContext} from '../../../src/render3/view/i18n/context'; import {serializeI18nMessageForGetMsg} from '../../../src/render3/view/i18n/get_msg_utils'; import {serializeIcuNode} from '../../../src/render3/view/i18n/icu_serializer'; import {serializeI18nMessageForLocalize} from '../../../src/render3/view/i18n/localize_utils'; import {I18nMeta, parseI18nMeta} from '../../../src/render3/view/i18n/meta'; import {formatI18nPlaceholderName} from '../../../src/render3/view/i18n/util'; import {parseR3 as parse} from './util'; const expressionParser = new Parser(new Lexer()); const i18nOf = (element: t.Node & {i18n?: i18n.I18nMeta}) => element.i18n !; describe('I18nContext', () => { it('should support i18n content collection', () => { const ref = o.variable('ref'); const ast = new i18n.Message([], {}, {}, '', '', ''); const ctx = new I18nContext(5, ref, 0, null, ast); // basic checks expect(ctx.isRoot).toBe(true); expect(ctx.isResolved).toBe(true); expect(ctx.id).toBe(0); expect(ctx.ref).toBe(ref); expect(ctx.index).toBe(5); expect(ctx.templateIndex).toBe(null); const tree = parse('
A {{ valueA }}
B

C

'); const [boundText, element, template] = (tree.nodes[0] as t.Element).children; // data collection checks expect(ctx.placeholders.size).toBe(0); ctx.appendBoundText(i18nOf(boundText)); // interpolation ctx.appendElement(i18nOf(element), 1); // open tag ctx.appendElement(i18nOf(element), 1, true); // close tag ctx.appendTemplate(i18nOf(template), 2); // open + close tags expect(ctx.placeholders.size).toBe(5); // binding collection checks expect(ctx.bindings.size).toBe(0); ctx.appendBinding(expressionParser.parseInterpolation('{{ valueA }}', '', 0) as AST); ctx.appendBinding(expressionParser.parseInterpolation('{{ valueB }}', '', 0) as AST); expect(ctx.bindings.size).toBe(2); }); it('should support nested contexts', () => { const template = `
A {{ valueA }}
A
B {{ valueB }}
B
C {{ valueC }}
`; const tree = parse(template); const root = tree.nodes[0] as t.Element; const [boundTextA, elementA, templateA] = root.children; const elementB = (templateA as t.Template).children[0] as t.Element; const [boundTextB, elementC, boundTextC] = (elementB as t.Element).children; // simulate I18nContext for a given template const ctx = new I18nContext(1, o.variable('ctx'), 0, null, root.i18n !); // set data for root ctx ctx.appendBoundText(i18nOf(boundTextA)); ctx.appendBinding(expressionParser.parseInterpolation('{{ valueA }}', '', 0) as AST); ctx.appendElement(i18nOf(elementA), 0); ctx.appendTemplate(i18nOf(templateA), 1); ctx.appendElement(i18nOf(elementA), 0, true); expect(ctx.bindings.size).toBe(1); expect(ctx.placeholders.size).toBe(5); expect(ctx.isResolved).toBe(false); // create child context const childCtx = ctx.forkChildContext(2, 1, (templateA as t.Template).i18n !); expect(childCtx.bindings.size).toBe(0); expect(childCtx.isRoot).toBe(false); // set data for child context childCtx.appendElement(i18nOf(elementB), 0); childCtx.appendBoundText(i18nOf(boundTextB)); childCtx.appendBinding(expressionParser.parseInterpolation('{{ valueB }}', '', 0) as AST); childCtx.appendElement(i18nOf(elementC), 1); childCtx.appendElement(i18nOf(elementC), 1, true); childCtx.appendBoundText(i18nOf(boundTextC)); childCtx.appendBinding(expressionParser.parseInterpolation('{{ valueC }}', '', 0) as AST); childCtx.appendElement(i18nOf(elementB), 0, true); expect(childCtx.bindings.size).toBe(2); expect(childCtx.placeholders.size).toBe(6); // ctx bindings and placeholders are not shared, // so root bindings and placeholders do not change expect(ctx.bindings.size).toBe(1); expect(ctx.placeholders.size).toBe(5); // reconcile ctx.reconcileChildContext(childCtx); // verify placeholders const expected = new Map([ ['INTERPOLATION', '�0�'], ['START_TAG_DIV', '�#0�|�#1:1�'], ['START_BOLD_TEXT', '�*1:1��#0:1�'], ['CLOSE_BOLD_TEXT', '�/#0:1��/*1:1�'], ['CLOSE_TAG_DIV', '�/#0�|�/#1:1�'], ['INTERPOLATION_1', '�0:1�'], ['INTERPOLATION_2', '�1:1�'] ]); const phs = ctx.getSerializedPlaceholders(); expected.forEach((value, key) => { expect(phs.get(key) !.join('|')).toEqual(value); }); // placeholders are added into the root ctx expect(phs.size).toBe(expected.size); // root context is considered resolved now expect(ctx.isResolved).toBe(true); // bindings are not merged into root ctx expect(ctx.bindings.size).toBe(1); }); it('should support templates based on ', () => { const template = ` Level A Level B Level C `; const tree = parse(template); const root = tree.nodes[0] as t.Template; const [textA, templateA] = root.children; const [textB, templateB] = (templateA as t.Template).children; const [textC] = (templateB as t.Template).children; // simulate I18nContext for a given template const ctxLevelA = new I18nContext(0, o.variable('ctx'), 0, null, root.i18n !); // create Level A context ctxLevelA.appendTemplate(i18nOf(templateA), 1); expect(ctxLevelA.placeholders.size).toBe(2); expect(ctxLevelA.isResolved).toBe(false); // create Level B context const ctxLevelB = ctxLevelA.forkChildContext(0, 1, (templateA as t.Template).i18n !); ctxLevelB.appendTemplate(i18nOf(templateB), 1); expect(ctxLevelB.isRoot).toBe(false); // create Level 2 context const ctxLevelC = ctxLevelB.forkChildContext(0, 1, (templateB as t.Template).i18n !); expect(ctxLevelC.isRoot).toBe(false); // reconcile ctxLevelB.reconcileChildContext(ctxLevelC); ctxLevelA.reconcileChildContext(ctxLevelB); // verify placeholders const expected = new Map( [['START_TAG_NG-TEMPLATE', '�*1:1�|�*1:2�'], ['CLOSE_TAG_NG-TEMPLATE', '�/*1:2�|�/*1:1�']]); const phs = ctxLevelA.getSerializedPlaceholders(); expected.forEach((value, key) => { expect(phs.get(key) !.join('|')).toEqual(value); }); // placeholders are added into the root ctx expect(phs.size).toBe(expected.size); // root context is considered resolved now expect(ctxLevelA.isResolved).toBe(true); }); }); describe('Utils', () => { it('formatI18nPlaceholderName', () => { const cases = [ // input, output ['', ''], ['ICU', 'icu'], ['ICU_1', 'icu_1'], ['ICU_1000', 'icu_1000'], ['START_TAG_NG-CONTAINER', 'startTagNgContainer'], ['START_TAG_NG-CONTAINER_1', 'startTagNgContainer_1'], ['CLOSE_TAG_ITALIC', 'closeTagItalic'], ['CLOSE_TAG_BOLD_1', 'closeTagBold_1'] ]; cases.forEach( ([input, output]) => { expect(formatI18nPlaceholderName(input)).toEqual(output); }); }); describe('metadata serialization', () => { it('parseI18nMeta()', () => { expect(parseI18nMeta('')).toEqual(meta()); expect(parseI18nMeta('desc')).toEqual(meta('', '', 'desc')); expect(parseI18nMeta('desc@@id')).toEqual(meta('id', '', 'desc')); expect(parseI18nMeta('meaning|desc')).toEqual(meta('', 'meaning', 'desc')); expect(parseI18nMeta('meaning|desc@@id')).toEqual(meta('id', 'meaning', 'desc')); expect(parseI18nMeta('@@id')).toEqual(meta('id', '', '')); }); it('serializeI18nHead()', () => { expect(o.localizedString(meta(), [''], [], []).serializeI18nHead()) .toEqual({cooked: '', raw: ''}); expect(o.localizedString(meta('', '', 'desc'), [''], [], []).serializeI18nHead()) .toEqual({cooked: ':desc:', raw: ':desc:'}); expect(o.localizedString(meta('id', '', 'desc'), [''], [], []).serializeI18nHead()) .toEqual({cooked: ':desc@@id:', raw: ':desc@@id:'}); expect(o.localizedString(meta('', 'meaning', 'desc'), [''], [], []).serializeI18nHead()) .toEqual({cooked: ':meaning|desc:', raw: ':meaning|desc:'}); expect(o.localizedString(meta('id', 'meaning', 'desc'), [''], [], []).serializeI18nHead()) .toEqual({cooked: ':meaning|desc@@id:', raw: ':meaning|desc@@id:'}); expect(o.localizedString(meta('id', '', ''), [''], [], []).serializeI18nHead()) .toEqual({cooked: ':@@id:', raw: ':@@id:'}); // Escaping colons (block markers) expect( o.localizedString(meta('id:sub_id', 'meaning', 'desc'), [''], [], []).serializeI18nHead()) .toEqual({cooked: ':meaning|desc@@id:sub_id:', raw: ':meaning|desc@@id\\:sub_id:'}); expect(o.localizedString(meta('id', 'meaning:sub_meaning', 'desc'), [''], [], []) .serializeI18nHead()) .toEqual( {cooked: ':meaning:sub_meaning|desc@@id:', raw: ':meaning\\:sub_meaning|desc@@id:'}); expect(o.localizedString(meta('id', 'meaning', 'desc:sub_desc'), [''], [], []) .serializeI18nHead()) .toEqual({cooked: ':meaning|desc:sub_desc@@id:', raw: ':meaning|desc\\:sub_desc@@id:'}); expect(o.localizedString(meta('id', 'meaning', 'desc'), ['message source'], [], []) .serializeI18nHead()) .toEqual({ cooked: ':meaning|desc@@id:message source', raw: ':meaning|desc@@id:message source' }); expect(o.localizedString(meta('id', 'meaning', 'desc'), [':message source'], [], []) .serializeI18nHead()) .toEqual({ cooked: ':meaning|desc@@id::message source', raw: ':meaning|desc@@id::message source' }); expect(o.localizedString(meta('', '', ''), ['message source'], [], []).serializeI18nHead()) .toEqual({cooked: 'message source', raw: 'message source'}); expect(o.localizedString(meta('', '', ''), [':message source'], [], []).serializeI18nHead()) .toEqual({cooked: ':message source', raw: '\\:message source'}); }); it('serializeI18nPlaceholderBlock()', () => { expect(o.localizedString(meta('', '', ''), ['', ''], [''], []).serializeI18nTemplatePart(1)) .toEqual({cooked: '', raw: ''}); expect( o.localizedString(meta('', '', ''), ['', ''], ['abc'], []).serializeI18nTemplatePart(1)) .toEqual({cooked: ':abc:', raw: ':abc:'}); expect(o.localizedString(meta('', '', ''), ['', 'message'], [''], []) .serializeI18nTemplatePart(1)) .toEqual({cooked: 'message', raw: 'message'}); expect(o.localizedString(meta('', '', ''), ['', 'message'], ['abc'], []) .serializeI18nTemplatePart(1)) .toEqual({cooked: ':abc:message', raw: ':abc:message'}); expect(o.localizedString(meta('', '', ''), ['', ':message'], [''], []) .serializeI18nTemplatePart(1)) .toEqual({cooked: ':message', raw: '\\:message'}); expect(o.localizedString(meta('', '', ''), ['', ':message'], ['abc'], []) .serializeI18nTemplatePart(1)) .toEqual({cooked: ':abc::message', raw: ':abc::message'}); }); function meta(customId?: string, meaning?: string, description?: string): I18nMeta { return {customId, meaning, description}; } }); }); describe('serializeI18nMessageForGetMsg', () => { const serialize = (input: string): string => { const tree = parse(`
${input}
`); const root = tree.nodes[0] as t.Element; return serializeI18nMessageForGetMsg(root.i18n as i18n.Message); }; it('should serialize plain text for `GetMsg()`', () => { expect(serialize('Some text')).toEqual('Some text'); }); it('should serialize text with interpolation for `GetMsg()`', () => { expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }}')) .toEqual('Some text {$interpolation} and {$interpolation_1}'); }); it('should serialize interpolation with named placeholder for `GetMsg()`', () => { expect(serialize('{{ valueB + valueC // i18n(ph="PLACEHOLDER NAME") }}')) .toEqual('{$placeholderName}'); }); it('should serialize content with HTML tags for `GetMsg()`', () => { expect(serialize('A B
C
D')) .toEqual('A {$startTagSpan}B{$startTagDiv}C{$closeTagDiv}{$closeTagSpan} D'); }); it('should serialize simple ICU for `GetMsg()`', () => { expect(serialize('{age, plural, 10 {ten} other {other}}')) .toEqual('{VAR_PLURAL, plural, 10 {ten} other {other}}'); }); it('should serialize nested ICUs for `GetMsg()`', () => { expect(serialize( '{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}')) .toEqual( '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}'); }); it('should serialize ICU with nested HTML for `GetMsg()`', () => { expect(serialize('{age, plural, 10 {ten} other {
other
}}')) .toEqual( '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}'); }); it('should serialize ICU with nested HTML containing further ICUs for `GetMsg()`', () => { expect( serialize( '{gender, select, male {male} female {female} other {other}}
{gender, select, male {male} female {female} other {other}}
')) .toEqual('{$icu}{$startTagDiv}{$icu}{$closeTagDiv}'); }); }); describe('serializeI18nMessageForLocalize', () => { const serialize = (input: string) => { const tree = parse(`
${input}
`); const root = tree.nodes[0] as t.Element; return serializeI18nMessageForLocalize(root.i18n as i18n.Message); }; it('should serialize plain text for `$localize()`', () => { expect(serialize('Some text')).toEqual({messageParts: ['Some text'], placeHolders: []}); }); it('should serialize text with interpolation for `$localize()`', () => { expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }} done')).toEqual({ messageParts: ['Some text ', ' and ', ' done'], placeHolders: ['INTERPOLATION', 'INTERPOLATION_1'] }); }); it('should serialize text with interpolation at start for `$localize()`', () => { expect(serialize('{{ valueA }} and {{ valueB + valueC }} done')).toEqual({ messageParts: ['', ' and ', ' done'], placeHolders: ['INTERPOLATION', 'INTERPOLATION_1'] }); }); it('should serialize text with interpolation at end for `$localize()`', () => { expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }}')).toEqual({ messageParts: ['Some text ', ' and ', ''], placeHolders: ['INTERPOLATION', 'INTERPOLATION_1'] }); }); it('should serialize only interpolation for `$localize()`', () => { expect(serialize('{{ valueB + valueC }}')) .toEqual({messageParts: ['', ''], placeHolders: ['INTERPOLATION']}); }); it('should serialize interpolation with named placeholder for `$localize()`', () => { expect(serialize('{{ valueB + valueC // i18n(ph="PLACEHOLDER NAME") }}')) .toEqual({messageParts: ['', ''], placeHolders: ['PLACEHOLDER_NAME']}); }); it('should serialize content with HTML tags for `$localize()`', () => { expect(serialize('A B
C
D')).toEqual({ messageParts: ['A ', 'B', 'C', '', ' D'], placeHolders: ['START_TAG_SPAN', 'START_TAG_DIV', 'CLOSE_TAG_DIV', 'CLOSE_TAG_SPAN'] }); }); it('should serialize simple ICU for `$localize()`', () => { expect(serialize('{age, plural, 10 {ten} other {other}}')).toEqual({ messageParts: ['{VAR_PLURAL, plural, 10 {ten} other {other}}'], placeHolders: [] }); }); it('should serialize nested ICUs for `$localize()`', () => { expect(serialize( '{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}')) .toEqual({ messageParts: [ '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}' ], placeHolders: [] }); }); it('should serialize ICU with nested HTML for `$localize()`', () => { expect(serialize('{age, plural, 10 {ten} other {
other
}}')).toEqual({ messageParts: [ '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}' ], placeHolders: [] }); }); it('should serialize ICU with nested HTML containing further ICUs for `$localize()`', () => { expect( serialize( '{gender, select, male {male} female {female} other {other}}
{gender, select, male {male} female {female} other {other}}
')) .toEqual({ messageParts: ['', '', '', '', ''], placeHolders: ['ICU', 'START_TAG_DIV', 'ICU', 'CLOSE_TAG_DIV'] }); }); }); describe('serializeIcuNode', () => { const serialize = (input: string) => { const tree = parse(`
${input}
`); const rooti18n = (tree.nodes[0] as t.Element).i18n as i18n.Message; return serializeIcuNode(rooti18n.nodes[0] as i18n.Icu); }; it('should serialize a simple ICU', () => { expect(serialize('{age, plural, 10 {ten} other {other}}')) .toEqual('{VAR_PLURAL, plural, 10 {ten} other {other}}'); }); it('should serialize a next ICU', () => { expect(serialize( '{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}')) .toEqual( '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}'); }); it('should serialize ICU with nested HTML', () => { expect(serialize('{age, plural, 10 {ten} other {
other
}}')) .toEqual( '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}'); }); });