diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index f718565ce32f4..c0e06637f0533 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -9,7 +9,6 @@ import {ParseSourceSpan} from '../parse_util'; import {I18nMeta} from '../render3/view/i18n/meta'; -import {error} from '../util'; //// Types export enum TypeModifier { @@ -516,11 +515,16 @@ export class LiteralExpr extends Expression { } } +export abstract class MessagePiece { + constructor(public text: string, public sourceSpan: ParseSourceSpan) {} +} +export class LiteralPiece extends MessagePiece {} +export class PlaceholderPiece extends MessagePiece {} export class LocalizedString extends Expression { constructor( - readonly metaBlock: I18nMeta, readonly messageParts: string[], - readonly placeHolderNames: string[], readonly expressions: Expression[], + readonly metaBlock: I18nMeta, readonly messageParts: LiteralPiece[], + readonly placeHolderNames: PlaceholderPiece[], readonly expressions: Expression[], sourceSpan?: ParseSourceSpan|null) { super(STRING_TYPE, sourceSpan); } @@ -563,7 +567,16 @@ export class LocalizedString extends Expression { metaBlock = `${metaBlock}${LEGACY_ID_INDICATOR}${legacyId}`; }); } - return createCookedRawString(metaBlock, this.messageParts[0]); + return createCookedRawString(metaBlock, this.messageParts[0].text); + } + + getMessagePartSourceSpan(i: number): ParseSourceSpan|null { + return this.messageParts[i]?.sourceSpan ?? this.sourceSpan; + } + + getPlaceholderSourceSpan(i: number): ParseSourceSpan { + return this.placeHolderNames[i]?.sourceSpan ?? this.expressions[i]?.sourceSpan ?? + this.sourceSpan; } /** @@ -574,9 +587,9 @@ export class LocalizedString extends Expression { * @param messagePart The following message string after this placeholder */ serializeI18nTemplatePart(partIndex: number): {cooked: string, raw: string} { - const placeholderName = this.placeHolderNames[partIndex - 1]; + const placeholderName = this.placeHolderNames[partIndex - 1].text; const messagePart = this.messageParts[partIndex]; - return createCookedRawString(placeholderName, messagePart); + return createCookedRawString(placeholderName, messagePart.text); } } @@ -1799,7 +1812,7 @@ export function literal( } export function localizedString( - metaBlock: I18nMeta, messageParts: string[], placeholderNames: string[], + metaBlock: I18nMeta, messageParts: LiteralPiece[], placeholderNames: PlaceholderPiece[], expressions: Expression[], sourceSpan?: ParseSourceSpan|null): LocalizedString { return new LocalizedString(metaBlock, messageParts, placeholderNames, expressions, sourceSpan); } diff --git a/packages/compiler/src/render3/view/i18n/localize_utils.ts b/packages/compiler/src/render3/view/i18n/localize_utils.ts index 127a3f6d94699..4c4f29eff27db 100644 --- a/packages/compiler/src/render3/view/i18n/localize_utils.ts +++ b/packages/compiler/src/render3/view/i18n/localize_utils.ts @@ -7,7 +7,7 @@ */ import * as i18n from '../../../i18n/i18n_ast'; import * as o from '../../../output/output_ast'; -import {ParseSourceSpan} from '../../../parse_util'; +import {ParseLocation, ParseSourceSpan} from '../../../parse_util'; import {serializeIcuNode} from './icu_serializer'; import {formatI18nPlaceholderName} from './util'; @@ -17,60 +17,55 @@ export function createLocalizeStatements( params: {[name: string]: o.Expression}): o.Statement[] { const {messageParts, placeHolders} = serializeI18nMessageForLocalize(message); const sourceSpan = getSourceSpan(message); - const expressions = placeHolders.map(ph => params[ph]); + const expressions = placeHolders.map(ph => params[ph.text]); const localizedString = o.localizedString(message, messageParts, placeHolders, expressions, sourceSpan); const variableInitialization = variable.set(localizedString); return [new o.ExpressionStatement(variableInitialization)]; } -class MessagePiece { - constructor(public text: string) {} -} -class LiteralPiece extends MessagePiece {} -class PlaceholderPiece extends MessagePiece { - constructor(name: string) { - super(formatI18nPlaceholderName(name, /* useCamelCase */ false)); - } -} - /** * This visitor walks over an i18n tree, capturing literal strings and placeholders. * * The result can be used for generating the `$localize` tagged template literals. */ class LocalizeSerializerVisitor implements i18n.Visitor { - visitText(text: i18n.Text, context: MessagePiece[]): any { - if (context[context.length - 1] instanceof LiteralPiece) { + visitText(text: i18n.Text, context: o.MessagePiece[]): any { + if (context[context.length - 1] instanceof o.LiteralPiece) { // Two literal pieces in a row means that there was some comment node in-between. context[context.length - 1].text += text.value; } else { - context.push(new LiteralPiece(text.value)); + context.push(new o.LiteralPiece(text.value, text.sourceSpan)); } } - visitContainer(container: i18n.Container, context: MessagePiece[]): any { + visitContainer(container: i18n.Container, context: o.MessagePiece[]): any { container.children.forEach(child => child.visit(this, context)); } - visitIcu(icu: i18n.Icu, context: MessagePiece[]): any { - context.push(new LiteralPiece(serializeIcuNode(icu))); + visitIcu(icu: i18n.Icu, context: o.MessagePiece[]): any { + context.push(new o.LiteralPiece(serializeIcuNode(icu), icu.sourceSpan)); } - visitTagPlaceholder(ph: i18n.TagPlaceholder, context: MessagePiece[]): any { - context.push(new PlaceholderPiece(ph.startName)); + visitTagPlaceholder(ph: i18n.TagPlaceholder, context: o.MessagePiece[]): any { + context.push(this.createPlaceholderPiece(ph.startName, ph.sourceSpan)); if (!ph.isVoid) { ph.children.forEach(child => child.visit(this, context)); - context.push(new PlaceholderPiece(ph.closeName)); + context.push(this.createPlaceholderPiece(ph.closeName, ph.closeSourceSpan ?? ph.sourceSpan)); } } - visitPlaceholder(ph: i18n.Placeholder, context: MessagePiece[]): any { - context.push(new PlaceholderPiece(ph.name)); + visitPlaceholder(ph: i18n.Placeholder, context: o.MessagePiece[]): any { + context.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan)); } visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { - context.push(new PlaceholderPiece(ph.name)); + context.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan)); + } + + private createPlaceholderPiece(name: string, sourceSpan: ParseSourceSpan): o.PlaceholderPiece { + return new o.PlaceholderPiece( + formatI18nPlaceholderName(name, /* useCamelCase */ false), sourceSpan); } } @@ -85,8 +80,8 @@ const serializerVisitor = new LocalizeSerializerVisitor(); * @returns an object containing the messageParts and placeholders. */ export function serializeI18nMessageForLocalize(message: i18n.Message): - {messageParts: string[], placeHolders: string[]} { - const pieces: MessagePiece[] = []; + {messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} { + const pieces: o.MessagePiece[] = []; message.nodes.forEach(node => node.visit(serializerVisitor, pieces)); return processMessagePieces(pieces); } @@ -107,31 +102,35 @@ function getSourceSpan(message: i18n.Message): ParseSourceSpan { * @param pieces The pieces to process. * @returns an object containing the messageParts and placeholders. */ -function processMessagePieces(pieces: MessagePiece[]): - {messageParts: string[], placeHolders: string[]} { - const messageParts: string[] = []; - const placeHolders: string[] = []; +function processMessagePieces(pieces: o.MessagePiece[]): + {messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} { + const messageParts: o.LiteralPiece[] = []; + const placeHolders: o.PlaceholderPiece[] = []; - if (pieces[0] instanceof PlaceholderPiece) { + if (pieces[0] instanceof o.PlaceholderPiece) { // The first piece was a placeholder so we need to add an initial empty message part. - messageParts.push(''); + messageParts.push(createEmptyMessagePart(pieces[0].sourceSpan.start)); } for (let i = 0; i < pieces.length; i++) { const part = pieces[i]; - if (part instanceof LiteralPiece) { - messageParts.push(part.text); + if (part instanceof o.LiteralPiece) { + messageParts.push(part); } else { - placeHolders.push(part.text); - if (pieces[i - 1] instanceof PlaceholderPiece) { + placeHolders.push(part); + if (pieces[i - 1] instanceof o.PlaceholderPiece) { // There were two placeholders in a row, so we need to add an empty message part. - messageParts.push(''); + messageParts.push(createEmptyMessagePart(part.sourceSpan.end)); } } } - if (pieces[pieces.length - 1] instanceof PlaceholderPiece) { + if (pieces[pieces.length - 1] instanceof o.PlaceholderPiece) { // The last piece was a placeholder so we need to add a final empty message part. - messageParts.push(''); + messageParts.push(createEmptyMessagePart(pieces[pieces.length - 1].sourceSpan.end)); } return {messageParts, placeHolders}; } + +function createEmptyMessagePart(location: ParseLocation): o.LiteralPiece { + return new o.LiteralPiece('', new ParseSourceSpan(location, location)); +} diff --git a/packages/compiler/test/output/js_emitter_spec.ts b/packages/compiler/test/output/js_emitter_spec.ts index c21297c4ee823..b6ff7deab5a81 100644 --- a/packages/compiler/test/output/js_emitter_spec.ts +++ b/packages/compiler/test/output/js_emitter_spec.ts @@ -205,9 +205,12 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some }); it('should support ES5 localized strings', () => { - expect(emitStmt(new o.ExpressionStatement(o.localizedString( - {}, ['ab\\:c', 'd"e\'f'], ['ph1'], - [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))])))) + const messageParts = + [new o.LiteralPiece('ab\\:c', {} as any), new o.LiteralPiece('d"e\'f', {} as any)]; + const placeholders = [new o.PlaceholderPiece('ph1', {} as any)]; + const expressions = [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))]; + const localizedString = o.localizedString({}, messageParts, placeholders, expressions); + expect(emitStmt(new o.ExpressionStatement(localizedString))) .toEqual( String.raw `$localize((this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})(['ab\\:c', ':ph1:d"e\'f'], ['ab\\\\:c', ':ph1:d"e\'f']), (7 + 8));`); diff --git a/packages/compiler/test/output/ts_emitter_spec.ts b/packages/compiler/test/output/ts_emitter_spec.ts index 78da4b697fb9c..e20b136f38b90 100644 --- a/packages/compiler/test/output/ts_emitter_spec.ts +++ b/packages/compiler/test/output/ts_emitter_spec.ts @@ -254,9 +254,12 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some }); it('should support localized strings', () => { - expect(emitStmt(new o.ExpressionStatement(o.localizedString( - {}, ['ab\\:c', 'd"e\'f'], ['ph1'], - [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))])))) + const messageParts = + [new o.LiteralPiece('ab\\:c', {} as any), new o.LiteralPiece('d"e\'f', {} as any)]; + const placeholders = [new o.PlaceholderPiece('ph1', {} as any)]; + const expressions = [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))]; + const localizedString = o.localizedString({}, messageParts, placeholders, expressions); + expect(emitStmt(new o.ExpressionStatement(localizedString))) .toEqual('$localize `ab\\\\:c${(7 + 8)}:ph1:d"e\'f`;'); }); diff --git a/packages/compiler/test/render3/view/i18n_spec.ts b/packages/compiler/test/render3/view/i18n_spec.ts index 328cb7b7f3d32..50d534c64ea83 100644 --- a/packages/compiler/test/render3/view/i18n_spec.ts +++ b/packages/compiler/test/render3/view/i18n_spec.ts @@ -10,6 +10,7 @@ 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 {ParseSourceSpan} from '../../../src/parse_util'; 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'; @@ -221,64 +222,75 @@ describe('Utils', () => { }); it('serializeI18nHead()', () => { - expect(o.localizedString(meta(), [''], [], []).serializeI18nHead()) + expect(o.localizedString(meta(), [literal('')], [], []).serializeI18nHead()) .toEqual({cooked: '', raw: ''}); - expect(o.localizedString(meta('', '', 'desc'), [''], [], []).serializeI18nHead()) + expect(o.localizedString(meta('', '', 'desc'), [literal('')], [], []).serializeI18nHead()) .toEqual({cooked: ':desc:', raw: ':desc:'}); - expect(o.localizedString(meta('id', '', 'desc'), [''], [], []).serializeI18nHead()) + expect(o.localizedString(meta('id', '', 'desc'), [literal('')], [], []).serializeI18nHead()) .toEqual({cooked: ':desc@@id:', raw: ':desc@@id:'}); - expect(o.localizedString(meta('', 'meaning', 'desc'), [''], [], []).serializeI18nHead()) + expect( + o.localizedString(meta('', 'meaning', 'desc'), [literal('')], [], []).serializeI18nHead()) .toEqual({cooked: ':meaning|desc:', raw: ':meaning|desc:'}); - expect(o.localizedString(meta('id', 'meaning', 'desc'), [''], [], []).serializeI18nHead()) + expect(o.localizedString(meta('id', 'meaning', 'desc'), [literal('')], [], []) + .serializeI18nHead()) .toEqual({cooked: ':meaning|desc@@id:', raw: ':meaning|desc@@id:'}); - expect(o.localizedString(meta('id', '', ''), [''], [], []).serializeI18nHead()) + expect(o.localizedString(meta('id', '', ''), [literal('')], [], []).serializeI18nHead()) .toEqual({cooked: ':@@id:', raw: ':@@id:'}); // Escaping colons (block markers) - expect( - o.localizedString(meta('id:sub_id', 'meaning', 'desc'), [''], [], []).serializeI18nHead()) + expect(o.localizedString(meta('id:sub_id', 'meaning', 'desc'), [literal('')], [], []) + .serializeI18nHead()) .toEqual({cooked: ':meaning|desc@@id:sub_id:', raw: ':meaning|desc@@id\\:sub_id:'}); - expect(o.localizedString(meta('id', 'meaning:sub_meaning', 'desc'), [''], [], []) + expect(o.localizedString(meta('id', 'meaning:sub_meaning', 'desc'), [literal('')], [], []) .serializeI18nHead()) .toEqual( {cooked: ':meaning:sub_meaning|desc@@id:', raw: ':meaning\\:sub_meaning|desc@@id:'}); - expect(o.localizedString(meta('id', 'meaning', 'desc:sub_desc'), [''], [], []) + expect(o.localizedString(meta('id', 'meaning', 'desc:sub_desc'), [literal('')], [], []) .serializeI18nHead()) .toEqual({cooked: ':meaning|desc:sub_desc@@id:', raw: ':meaning|desc\\:sub_desc@@id:'}); - expect(o.localizedString(meta('id', 'meaning', 'desc'), ['message source'], [], []) + expect(o.localizedString(meta('id', 'meaning', 'desc'), [literal('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'], [], []) + expect(o.localizedString(meta('id', 'meaning', 'desc'), [literal(':message source')], [], []) .serializeI18nHead()) .toEqual({ cooked: ':meaning|desc@@id::message source', raw: ':meaning|desc@@id::message source' }); - expect(o.localizedString(meta('', '', ''), ['message source'], [], []).serializeI18nHead()) + expect(o.localizedString(meta('', '', ''), [literal('message source')], [], []) + .serializeI18nHead()) .toEqual({cooked: 'message source', raw: 'message source'}); - expect(o.localizedString(meta('', '', ''), [':message source'], [], []).serializeI18nHead()) + expect(o.localizedString(meta('', '', ''), [literal(':message source')], [], []) + .serializeI18nHead()) .toEqual({cooked: ':message source', raw: '\\:message source'}); }); it('serializeI18nPlaceholderBlock()', () => { - expect(o.localizedString(meta('', '', ''), ['', ''], [''], []).serializeI18nTemplatePart(1)) + expect(o.localizedString(meta('', '', ''), [literal(''), literal('')], [literal('')], []) + .serializeI18nTemplatePart(1)) .toEqual({cooked: '', raw: ''}); - expect( - o.localizedString(meta('', '', ''), ['', ''], ['abc'], []).serializeI18nTemplatePart(1)) - .toEqual({cooked: ':abc:', raw: ':abc:'}); - expect(o.localizedString(meta('', '', ''), ['', 'message'], [''], []) + expect(o.localizedString( + meta('', '', ''), [literal(''), literal('')], + [new o.LiteralPiece('abc', {} as any)], []) .serializeI18nTemplatePart(1)) + .toEqual({cooked: ':abc:', raw: ':abc:'}); + expect( + o.localizedString(meta('', '', ''), [literal(''), literal('message')], [literal('')], []) + .serializeI18nTemplatePart(1)) .toEqual({cooked: 'message', raw: 'message'}); - expect(o.localizedString(meta('', '', ''), ['', 'message'], ['abc'], []) + expect(o.localizedString( + meta('', '', ''), [literal(''), literal('message')], [literal('abc')], []) .serializeI18nTemplatePart(1)) .toEqual({cooked: ':abc:message', raw: ':abc:message'}); - expect(o.localizedString(meta('', '', ''), ['', ':message'], [''], []) - .serializeI18nTemplatePart(1)) + expect( + o.localizedString(meta('', '', ''), [literal(''), literal(':message')], [literal('')], []) + .serializeI18nTemplatePart(1)) .toEqual({cooked: ':message', raw: '\\:message'}); - expect(o.localizedString(meta('', '', ''), ['', ':message'], ['abc'], []) + expect(o.localizedString( + meta('', '', ''), [literal(''), literal(':message')], [literal('abc')], []) .serializeI18nTemplatePart(1)) .toEqual({cooked: ':abc::message', raw: ':abc::message'}); }); @@ -349,55 +361,106 @@ describe('serializeI18nMessageForLocalize', () => { }; it('should serialize plain text for `$localize()`', () => { - expect(serialize('Some text')).toEqual({messageParts: ['Some text'], placeHolders: []}); + expect(serialize('Some text')) + .toEqual({messageParts: [literal('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'] + messageParts: [literal('Some text '), literal(' and '), literal(' done')], + placeHolders: [placeholder('INTERPOLATION'), placeholder('INTERPOLATION_1')], }); }); + it('should compute source-spans when serializing text with interpolation for `$localize()`', + () => { + const {messageParts, placeHolders} = + serialize('Some text {{ valueA }} and {{ valueB + valueC }} done'); + + expect(messageParts[0].text).toEqual('Some text '); + expect(messageParts[0].sourceSpan.toString()).toEqual('Some text '); + expect(messageParts[1].text).toEqual(' and '); + expect(messageParts[1].sourceSpan.toString()).toEqual(' and '); + expect(messageParts[2].text).toEqual(' done'); + expect(messageParts[2].sourceSpan.toString()).toEqual(' done'); + + expect(placeHolders[0].text).toEqual('INTERPOLATION'); + expect(placeHolders[0].sourceSpan.toString()).toEqual('{{ valueA }}'); + expect(placeHolders[1].text).toEqual('INTERPOLATION_1'); + expect(placeHolders[1].sourceSpan.toString()).toEqual('{{ valueB + valueC }}'); + }); + 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'] + messageParts: [literal(''), literal(' and '), literal(' done')], + placeHolders: [placeholder('INTERPOLATION'), placeholder('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'] + messageParts: [literal('Some text '), literal(' and '), literal('')], + placeHolders: [placeholder('INTERPOLATION'), placeholder('INTERPOLATION_1')], }); }); it('should serialize only interpolation for `$localize()`', () => { - expect(serialize('{{ valueB + valueC }}')) - .toEqual({messageParts: ['', ''], placeHolders: ['INTERPOLATION']}); + expect(serialize('{{ valueB + valueC }}')).toEqual({ + messageParts: [literal(''), literal('')], + placeHolders: [placeholder('INTERPOLATION')] + }); }); it('should serialize interpolation with named placeholder for `$localize()`', () => { - expect(serialize('{{ valueB + valueC // i18n(ph="PLACEHOLDER NAME") }}')) - .toEqual({messageParts: ['', ''], placeHolders: ['PLACEHOLDER_NAME']}); + expect(serialize('{{ valueB + valueC // i18n(ph="PLACEHOLDER NAME") }}')).toEqual({ + messageParts: [literal(''), literal('')], + placeHolders: [placeholder('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'] + messageParts: [literal('A '), literal('B'), literal('C'), literal(''), literal(' D')], + placeHolders: [ + placeholder('START_TAG_SPAN'), placeholder('START_TAG_DIV'), placeholder('CLOSE_TAG_DIV'), + placeholder('CLOSE_TAG_SPAN') + ] }); }); + it('should compute source-spans when serializing content with HTML tags for `$localize()`', + () => { + const {messageParts, placeHolders} = serialize('A B
C
D'); + + expect(messageParts[0].text).toEqual('A '); + expect(messageParts[0].sourceSpan.toString()).toEqual('A '); + expect(messageParts[1].text).toEqual('B'); + expect(messageParts[1].sourceSpan.toString()).toEqual('B'); + expect(messageParts[2].text).toEqual('C'); + expect(messageParts[2].sourceSpan.toString()).toEqual('C'); + expect(messageParts[3].text).toEqual(''); + expect(messageParts[3].sourceSpan.toString()).toEqual(''); + expect(messageParts[4].text).toEqual(' D'); + expect(messageParts[4].sourceSpan.toString()).toEqual(' D'); + + expect(placeHolders[0].text).toEqual('START_TAG_SPAN'); + expect(placeHolders[0].sourceSpan.toString()).toEqual(''); + expect(placeHolders[1].text).toEqual('START_TAG_DIV'); + expect(placeHolders[1].sourceSpan.toString()).toEqual('
'); + expect(placeHolders[2].text).toEqual('CLOSE_TAG_DIV'); + expect(placeHolders[2].sourceSpan.toString()).toEqual('
'); + expect(placeHolders[3].text).toEqual('CLOSE_TAG_SPAN'); + expect(placeHolders[3].sourceSpan.toString()).toEqual('
'); + }); + it('should serialize simple ICU for `$localize()`', () => { expect(serialize('{age, plural, 10 {ten} other {other}}')).toEqual({ - messageParts: ['{VAR_PLURAL, plural, 10 {ten} other {other}}'], + messageParts: [literal('{VAR_PLURAL, plural, 10 {ten} other {other}}')], placeHolders: [] }); }); @@ -408,7 +471,8 @@ describe('serializeI18nMessageForLocalize', () => { '{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}}' + literal( + '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}') ], placeHolders: [] }); @@ -418,7 +482,8 @@ describe('serializeI18nMessageForLocalize', () => { it('should serialize ICU with embedded 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}}}' + literal( + '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}') ], placeHolders: [] }); @@ -427,7 +492,8 @@ describe('serializeI18nMessageForLocalize', () => { it('should serialize ICU with embedded interpolation for `$localize()`', () => { expect(serialize('{age, plural, 10 {ten} other {{{age}} years old}}')).toEqual({ messageParts: [ - '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{INTERPOLATION} years old}}' + literal( + '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{INTERPOLATION} years old}}') ], placeHolders: [] }); @@ -438,8 +504,11 @@ describe('serializeI18nMessageForLocalize', () => { 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'] + messageParts: [literal(''), literal(''), literal(''), literal(''), literal('')], + placeHolders: [ + placeholder('ICU'), placeholder('START_TAG_DIV'), placeholder('ICU'), + placeholder('CLOSE_TAG_DIV') + ] }); }); @@ -449,7 +518,8 @@ describe('serializeI18nMessageForLocalize', () => { '{age, plural, 10 {ten {size, select, 1 {{{ varOne }}} 2 {{{ varTwo }}} other {2+}}} other {other}}')) .toEqual({ messageParts: [ - '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {{INTERPOLATION}} 2 {{INTERPOLATION_1}} other {2+}}} other {other}}' + literal( + '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {{INTERPOLATION}} 2 {{INTERPOLATION_1}} other {2+}}} other {other}}') ], placeHolders: [] }); @@ -486,3 +556,11 @@ describe('serializeIcuNode', () => { .toEqual('{VAR_SELECT, select, 10 {ten} other {{INTERPOLATION} years old}}'); }); }); + +function literal(text: string, span: any = jasmine.any(ParseSourceSpan)): o.LiteralPiece { + return new o.LiteralPiece(text, span); +} + +function placeholder(name: string, span: any = jasmine.any(ParseSourceSpan)): o.PlaceholderPiece { + return new o.PlaceholderPiece(name, span); +}