diff --git a/packages/localize/src/tools/src/extract/extraction.ts b/packages/localize/src/tools/src/extract/extraction.ts index 61331064ee..b86875e569 100644 --- a/packages/localize/src/tools/src/extract/extraction.ts +++ b/packages/localize/src/tools/src/extract/extraction.ts @@ -77,6 +77,20 @@ export class MessageExtractor { for (const message of messages) { if (message.location !== undefined) { message.location = this.getOriginalLocation(sourceFile, message.location); + + if (message.messagePartLocations) { + message.messagePartLocations = message.messagePartLocations.map( + location => location && this.getOriginalLocation(sourceFile, location)); + } + + if (message.substitutionLocations) { + const placeholderNames = Object.keys(message.substitutionLocations); + for (const placeholderName of placeholderNames) { + const location = message.substitutionLocations[placeholderName]; + message.substitutionLocations[placeholderName] = + location && this.getOriginalLocation(sourceFile, location); + } + } } } } diff --git a/packages/localize/src/tools/src/extract/translation_files/xliff1_translation_serializer.ts b/packages/localize/src/tools/src/extract/translation_files/xliff1_translation_serializer.ts index 334a508b0f..8dd6f2085b 100644 --- a/packages/localize/src/tools/src/extract/translation_files/xliff1_translation_serializer.ts +++ b/packages/localize/src/tools/src/extract/translation_files/xliff1_translation_serializer.ts @@ -67,7 +67,8 @@ export class Xliff1TranslationSerializer implements TranslationSerializer { const length = message.messageParts.length - 1; for (let i = 0; i < length; i++) { this.serializeTextPart(xml, message.messageParts[i]); - xml.startTag('x', {id: message.placeholderNames[i]}, {selfClosing: true}); + const location = message.substitutionLocations?.[message.placeholderNames[i]]; + this.serializePlaceholder(xml, message.placeholderNames[i], location?.text); } this.serializeTextPart(xml, message.messageParts[length]); } @@ -77,11 +78,19 @@ export class Xliff1TranslationSerializer implements TranslationSerializer { const length = pieces.length - 1; for (let i = 0; i < length; i += 2) { xml.text(pieces[i]); - xml.startTag('x', {id: pieces[i + 1]}, {selfClosing: true}); + this.serializePlaceholder(xml, pieces[i + 1], undefined); } xml.text(pieces[length]); } + private serializePlaceholder(xml: XmlFile, id: string, text: string|undefined): void { + const attrs: Record = {id}; + if (text !== undefined) { + attrs['equiv-text'] = text; + } + xml.startTag('x', attrs, {selfClosing: true}); + } + private serializeNote(xml: XmlFile, name: string, value: string): void { xml.startTag('note', {priority: '1', from: name}, {preserveWhitespace: true}); xml.text(value); diff --git a/packages/localize/src/tools/src/extract/translation_files/xliff2_translation_serializer.ts b/packages/localize/src/tools/src/extract/translation_files/xliff2_translation_serializer.ts index 66ed022474..234ceaf45e 100644 --- a/packages/localize/src/tools/src/extract/translation_files/xliff2_translation_serializer.ts +++ b/packages/localize/src/tools/src/extract/translation_files/xliff2_translation_serializer.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system'; -import {ɵParsedMessage} from '@angular/localize'; +import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; import {extractIcuPlaceholders} from './icu_parsing'; import {TranslationSerializer} from './translation_serializer'; @@ -80,7 +80,7 @@ export class Xliff2TranslationSerializer implements TranslationSerializer { const length = message.messageParts.length - 1; for (let i = 0; i < length; i++) { this.serializeTextPart(xml, message.messageParts[i]); - this.serializePlaceholder(xml, message.placeholderNames[i]); + this.serializePlaceholder(xml, message.placeholderNames[i], message.substitutionLocations); } this.serializeTextPart(xml, message.messageParts[length]); } @@ -90,24 +90,40 @@ export class Xliff2TranslationSerializer implements TranslationSerializer { const length = pieces.length - 1; for (let i = 0; i < length; i += 2) { xml.text(pieces[i]); - this.serializePlaceholder(xml, pieces[i + 1]); + this.serializePlaceholder(xml, pieces[i + 1], undefined); } xml.text(pieces[length]); } - private serializePlaceholder(xml: XmlFile, placeholderName: string): void { + private serializePlaceholder( + xml: XmlFile, placeholderName: string, + substitutionLocations: Record|undefined): void { + const text = substitutionLocations?.[placeholderName]?.text; + if (placeholderName.startsWith('START_')) { - xml.startTag('pc', { + const closingPlaceholderName = placeholderName.replace(/^START/, 'CLOSE'); + const closingText = substitutionLocations?.[closingPlaceholderName]?.text; + const attrs: Record = { id: `${this.currentPlaceholderId++}`, equivStart: placeholderName, - equivEnd: placeholderName.replace(/^START/, 'CLOSE') - }); + equivEnd: closingPlaceholderName, + }; + if (text !== undefined) { + attrs.dispStart = text; + } + if (closingText !== undefined) { + attrs.dispEnd = closingText; + } + xml.startTag('pc', attrs); } else if (placeholderName.startsWith('CLOSE_')) { xml.endTag('pc'); } else { - xml.startTag( - 'ph', {id: `${this.currentPlaceholderId++}`, equiv: placeholderName}, - {selfClosing: true}); + const attrs: + Record = {id: `${this.currentPlaceholderId++}`, equiv: placeholderName}; + if (text !== undefined) { + attrs.disp = text; + } + xml.startTag('ph', attrs, {selfClosing: true}); } } diff --git a/packages/localize/src/tools/test/extract/integration/main_spec.ts b/packages/localize/src/tools/test/extract/integration/main_spec.ts index 1fa46237bc..72b9d1aa5f 100644 --- a/packages/localize/src/tools/test/extract/integration/main_spec.ts +++ b/packages/localize/src/tools/test/extract/integration/main_spec.ts @@ -77,7 +77,8 @@ runInEachFileSystem(() => { ` "8669027859022295761": "try{$PH}me",`, ` "custom-id": "Custom id message",`, ` "273296103957933077": "Legacy id message",`, - ` "custom-id-2": "Custom and legacy message"`, + ` "custom-id-2": "Custom and legacy message",`, + ` "2932901491976224757": "pre{$START_TAG_SPAN}inner-pre{$START_BOLD_TEXT}bold{$CLOSE_BOLD_TEXT}inner-post{$CLOSE_TAG_SPAN}post"`, ` }`, `}`, ].join('\n')); @@ -127,6 +128,8 @@ runInEachFileSystem(() => { '12345678901234567890' : '273296103957933077'}">test_files/test.js:5Legacy id message`, ` test_files/test.js:7Custom and legacy message`, + ` test_files/test.js:8,10pre` + + `inner-preboldinner-postpost`, `\n`, ].join('\n')); }); @@ -149,14 +152,14 @@ runInEachFileSystem(() => { ` `, ` `, ` `, - ` Hello, !`, + ` Hello, !`, ` `, ` test_files/test.js`, ` 2`, ` `, ` `, ` `, - ` tryme`, + ` tryme`, ` `, ` test_files/test.js`, ` 3`, @@ -185,6 +188,15 @@ runInEachFileSystem(() => { ` 8`, ` `, ` `, + ` `, + ` pre` + + `inner-prebold` + + `inner-postpost`, + ` `, + ` test_files/test.js`, + ` 9,10`, + ` `, + ` `, ` `, ` `, `\n`, @@ -209,12 +221,12 @@ runInEachFileSystem(() => { ` `, ` `, ` `, - ` Hello, !`, + ` Hello, !`, ` `, ` `, ` `, ` `, - ` tryme`, + ` tryme`, ` `, ` `, ` `, @@ -232,6 +244,13 @@ runInEachFileSystem(() => { ` Custom and legacy message`, ` `, ` `, + ` `, + ` `, + ` pre` + + `inner-prebold` + + `inner-postpost`, + ` `, + ` `, ` `, `\n`, ].join('\n')); @@ -260,7 +279,7 @@ runInEachFileSystem(() => { ` `, ` `, ` `, - ` Message in !`, + ` Message in !`, ` `, // These source file paths are due to how Bazel TypeScript compilation source-maps work ` ../packages/localize/src/tools/test/extract/integration/test_files/src/a.ts`, @@ -268,7 +287,7 @@ runInEachFileSystem(() => { ` `, ` `, ` `, - ` Message in !`, + ` Message in !`, ` `, ` ../packages/localize/src/tools/test/extract/integration/test_files/src/b.ts`, ` 3`, diff --git a/packages/localize/src/tools/test/extract/integration/test_files/test.js b/packages/localize/src/tools/test/extract/integration/test_files/test.js index e098bc127d..66d58b0ee2 100644 --- a/packages/localize/src/tools/test/extract/integration/test_files/test.js +++ b/packages/localize/src/tools/test/extract/integration/test_files/test.js @@ -6,3 +6,5 @@ var legacyMessage = $localize`:␟1234567890123456789012345678901234567890␟12345678901234567890:Legacy id message`; var customAndLegacyMessage = $localize`:@@custom-id-2␟1234567890123456789012345678901234567890␟12345678901234567890:Custom and legacy message`; +var containers = $localize`pre${''}:START_TAG_SPAN:inner-pre${''}:START_BOLD_TEXT:bold${ + ''}:CLOSE_BOLD_TEXT:inner-post${''}:CLOSE_TAG_SPAN:post`; diff --git a/packages/localize/src/tools/test/extract/translation_files/mock_message.ts b/packages/localize/src/tools/test/extract/translation_files/mock_message.ts index 19cfe6d96c..b05b16e510 100644 --- a/packages/localize/src/tools/test/extract/translation_files/mock_message.ts +++ b/packages/localize/src/tools/test/extract/translation_files/mock_message.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵParsedMessage} from '@angular/localize'; -import {SourceLocation} from '@angular/localize/src/utils'; +import {MessageId, SourceLocation} from '@angular/localize/src/utils'; export interface MockMessageOptions { customId?: string; @@ -14,6 +14,8 @@ export interface MockMessageOptions { description?: string; location?: SourceLocation; legacyIds?: string[]; + messagePartLocations?: (SourceLocation|undefined)[]; + substitutionLocations?: Record; } /** @@ -21,23 +23,18 @@ export interface MockMessageOptions { * `TranslationSerializer` tests. */ export function mockMessage( - id: string, messageParts: string[], placeholderNames: string[], - {customId, meaning = '', description = '', location, legacyIds = []}: MockMessageOptions): - ɵParsedMessage { + id: MessageId, messageParts: string[], placeholderNames: string[], + options: MockMessageOptions): ɵParsedMessage { let text = messageParts[0]; for (let i = 1; i < messageParts.length; i++) { text += `{$${placeholderNames[i - 1]}}${messageParts[i]}`; } return { - id: customId || id, // customId trumps id + substitutions: [], + ...options, + id: options.customId || id, // customId trumps id text, messageParts, placeholderNames, - customId, - description, - meaning, - substitutions: [], - legacyIds, - location, }; } diff --git a/packages/localize/src/tools/test/extract/translation_files/xliff1_translation_serializer_spec.ts b/packages/localize/src/tools/test/extract/translation_files/xliff1_translation_serializer_spec.ts index 9b66fb96fe..31ff2d99c2 100644 --- a/packages/localize/src/tools/test/extract/translation_files/xliff1_translation_serializer_spec.ts +++ b/packages/localize/src/tools/test/extract/translation_files/xliff1_translation_serializer_spec.ts @@ -7,7 +7,7 @@ */ import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; -import {ɵParsedMessage} from '@angular/localize'; +import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; import {Xliff1TranslationSerializer} from '../../../src/extract/translation_files/xliff1_translation_serializer'; @@ -18,6 +18,18 @@ runInEachFileSystem(() => { [false, true].forEach(useLegacyIds => { describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => { it('should convert a set of parsed messages into an XML string', () => { + const phLocation: ɵSourceLocation = { + start: {line: 0, column: 10}, + end: {line: 1, column: 15}, + file: absoluteFrom('/project/file.ts'), + text: 'placeholder + 1' + }; + const messagePartLocation: ɵSourceLocation = { + start: {line: 0, column: 5}, + end: {line: 0, column: 10}, + file: absoluteFrom('/project/file.ts'), + text: 'message part' + }; const messages: ɵParsedMessage[] = [ mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], { meaning: 'some meaning', @@ -31,6 +43,8 @@ runInEachFileSystem(() => { mockMessage('54321', ['a', 'b', 'c'], ['PH', 'PH_1'], { customId: 'someId', legacyIds: ['87654321FEDCBA0987654321FEDCBA0987654321', '563965274788097516'], + messagePartLocations: [undefined, messagePartLocation, undefined], + substitutionLocations: {'PH': phLocation, 'PH_1': undefined}, }), mockMessage( '67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], @@ -71,7 +85,7 @@ runInEachFileSystem(() => { ` some meaning`, ` `, ` `, - ` abc`, + ` abc`, ` `, ` `, ` ac`, diff --git a/packages/localize/src/tools/test/extract/translation_files/xliff2_translation_serializer_spec.ts b/packages/localize/src/tools/test/extract/translation_files/xliff2_translation_serializer_spec.ts index 8e98690fcd..cf11c8d199 100644 --- a/packages/localize/src/tools/test/extract/translation_files/xliff2_translation_serializer_spec.ts +++ b/packages/localize/src/tools/test/extract/translation_files/xliff2_translation_serializer_spec.ts @@ -5,10 +5,9 @@ * 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 {computeMsgId} from '@angular/compiler'; import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; -import {ɵParsedMessage} from '@angular/localize'; +import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; import {Xliff2TranslationSerializer} from '../../../src/extract/translation_files/xliff2_translation_serializer'; @@ -19,6 +18,18 @@ runInEachFileSystem(() => { [false, true].forEach(useLegacyIds => { describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => { it('should convert a set of parsed messages into an XML string', () => { + const phLocation: ɵSourceLocation = { + start: {line: 0, column: 10}, + end: {line: 1, column: 15}, + file: absoluteFrom('/project/file.ts'), + text: 'placeholder + 1' + }; + const messagePartLocation: ɵSourceLocation = { + start: {line: 0, column: 5}, + end: {line: 0, column: 10}, + file: absoluteFrom('/project/file.ts'), + text: 'message part' + }; const messages: ɵParsedMessage[] = [ mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], { meaning: 'some meaning', @@ -32,6 +43,8 @@ runInEachFileSystem(() => { mockMessage('54321', ['a', 'b', 'c'], ['PH', 'PH_1'], { customId: 'someId', legacyIds: ['87654321FEDCBA0987654321FEDCBA0987654321', '563965274788097516'], + messagePartLocations: [undefined, messagePartLocation, undefined], + substitutionLocations: {'PH': phLocation, 'PH_1': undefined}, }), mockMessage('67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], { description: 'some description', @@ -76,7 +89,7 @@ runInEachFileSystem(() => { ` `, ` `, ` `, - ` abc`, + ` abc`, ` `, ` `, ` `, diff --git a/packages/localize/src/tools/test/extract/translation_files/xmb_translation_serializer_spec.ts b/packages/localize/src/tools/test/extract/translation_files/xmb_translation_serializer_spec.ts index 1c73501872..f7cf83e9cc 100644 --- a/packages/localize/src/tools/test/extract/translation_files/xmb_translation_serializer_spec.ts +++ b/packages/localize/src/tools/test/extract/translation_files/xmb_translation_serializer_spec.ts @@ -7,7 +7,7 @@ */ import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; -import {ɵParsedMessage} from '@angular/localize'; +import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; import {XmbTranslationSerializer} from '../../../src/extract/translation_files/xmb_translation_serializer'; @@ -18,6 +18,18 @@ runInEachFileSystem(() => { [false, true].forEach(useLegacyIds => { describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => { it('should convert a set of parsed messages into an XML string', () => { + const phLocation: ɵSourceLocation = { + start: {line: 0, column: 10}, + end: {line: 1, column: 15}, + file: absoluteFrom('/project/file.ts'), + text: 'placeholder + 1' + }; + const messagePartLocation: ɵSourceLocation = { + start: {line: 0, column: 5}, + end: {line: 0, column: 10}, + file: absoluteFrom('/project/file.ts'), + text: 'message part' + }; const messages: ɵParsedMessage[] = [ mockMessage('12345', ['a', 'b', 'c'], ['PH', 'PH_1'], { meaning: 'some meaning', @@ -26,6 +38,8 @@ runInEachFileSystem(() => { mockMessage('54321', ['a', 'b', 'c'], ['PH', 'PH_1'], { customId: 'someId', legacyIds: ['87654321FEDCBA0987654321FEDCBA0987654321', '563965274788097516'], + messagePartLocations: [undefined, messagePartLocation, undefined], + substitutionLocations: {'PH': phLocation, 'PH_1': undefined}, }), mockMessage( '67890', ['a', '', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'],