From 0ed2c4fef9cbbe67827436ec7afffdcf290623e0 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Fri, 23 Oct 2020 13:32:36 +0100 Subject: [PATCH] fix(localize): render placeholder types in extracted XLIFF files (#39398) The previous ViewEngine extraction tooling added `ctype` and `type` attributes to XLIFF 1.2 and 2.0 translation files, respectively. This commit adds this to the new $localize based extraction tooling. Since the new extraction tooling works from the compiled output rather than having direct access to the template content, the placeholder types must be inferred from the name of the placeholder. This is considered reasonable, since it already does this to compute opening and closing tag placeholders. Fixes #38791 PR Close #39398 --- .../xliff1_translation_serializer.ts | 69 +++++++++++++++++++ .../xliff2_translation_serializer.ts | 40 ++++++++++- .../test/extract/integration/main_spec.ts | 10 +-- .../xliff1_translation_serializer_spec.ts | 66 ++++++++++++++++-- .../xliff2_translation_serializer_spec.ts | 66 ++++++++++++++++-- 5 files changed, 236 insertions(+), 15 deletions(-) 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 78006a2980..f6e799c41b 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 @@ -100,6 +100,10 @@ export class Xliff1TranslationSerializer implements TranslationSerializer { private serializePlaceholder(xml: XmlFile, id: string, text: string|undefined): void { const attrs: Record = {id}; + const ctype = getCtypeForPlaceholder(id); + if (ctype !== null) { + attrs.ctype = ctype; + } if (text !== undefined) { attrs['equiv-text'] = text; } @@ -148,3 +152,68 @@ export class Xliff1TranslationSerializer implements TranslationSerializer { message.id; } } + +/** + * Compute the value of the `ctype` attribute from the `placeholder` name. + * + * The placeholder can take the following forms: + * + * - `START_BOLD_TEXT`/`END_BOLD_TEXT` + * - `TAG_` + * - `START_TAG_` + * - `CLOSE_TAG_` + * + * In these cases the element name of the tag is extracted from the placeholder name and returned as + * `x-`. + * + * Line breaks and images are special cases. + */ +function getCtypeForPlaceholder(placeholder: string): string|null { + const tag = placeholder.replace(/^(START_|CLOSE_)/, ''); + switch (tag) { + case 'LINE_BREAK': + return 'lb'; + case 'TAG_IMG': + return 'image'; + default: + const element = tag.startsWith('TAG_') ? + tag.replace(/^TAG_(.+)/, (_, tagName: string) => tagName.toLowerCase()) : + TAG_MAP[tag]; + if (element === undefined) { + return null; + } + return `x-${element}`; + } +} + +const TAG_MAP: Record = { + 'LINK': 'a', + 'BOLD_TEXT': 'b', + 'EMPHASISED_TEXT': 'em', + 'HEADING_LEVEL1': 'h1', + 'HEADING_LEVEL2': 'h2', + 'HEADING_LEVEL3': 'h3', + 'HEADING_LEVEL4': 'h4', + 'HEADING_LEVEL5': 'h5', + 'HEADING_LEVEL6': 'h6', + 'HORIZONTAL_RULE': 'hr', + 'ITALIC_TEXT': 'i', + 'LIST_ITEM': 'li', + 'MEDIA_LINK': 'link', + 'ORDERED_LIST': 'ol', + 'PARAGRAPH': 'p', + 'QUOTATION': 'q', + 'STRIKETHROUGH_TEXT': 's', + 'SMALL_TEXT': 'small', + 'SUBSTRIPT': 'sub', + 'SUPERSCRIPT': 'sup', + 'TABLE_BODY': 'tbody', + 'TABLE_CELL': 'td', + 'TABLE_FOOTER': 'tfoot', + 'TABLE_HEADER_CELL': 'th', + 'TABLE_HEADER': 'thead', + 'TABLE_ROW': 'tr', + 'MONOSPACED_TEXT': 'tt', + 'UNDERLINED_TEXT': 'u', + 'UNORDERED_LIST': 'ul', +}; 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 fb926abfdb..4fee339e59 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 @@ -119,6 +119,10 @@ export class Xliff2TranslationSerializer implements TranslationSerializer { equivStart: placeholderName, equivEnd: closingPlaceholderName, }; + const type = getTypeForPlaceholder(placeholderName); + if (type !== null) { + attrs.type = type; + } if (text !== undefined) { attrs.dispStart = text; } @@ -129,8 +133,14 @@ export class Xliff2TranslationSerializer implements TranslationSerializer { } else if (placeholderName.startsWith('CLOSE_')) { xml.endTag('pc'); } else { - const attrs: - Record = {id: `${this.currentPlaceholderId++}`, equiv: placeholderName}; + const attrs: Record = { + id: `${this.currentPlaceholderId++}`, + equiv: placeholderName, + }; + const type = getTypeForPlaceholder(placeholderName); + if (type !== null) { + attrs.type = type; + } if (text !== undefined) { attrs.disp = text; } @@ -166,3 +176,29 @@ export class Xliff2TranslationSerializer implements TranslationSerializer { message.id; } } + +/** + * Compute the value of the `type` attribute from the `placeholder` name. + * + * If the tag is not known but starts with `TAG_`, `START_TAG_` or `CLOSE_TAG_` then the type is + * `other`. Certain formatting tags (e.g. bold, italic, etc) have type `fmt`. Line-breaks, images + * and links are special cases. + */ +function getTypeForPlaceholder(placeholder: string): string|null { + const tag = placeholder.replace(/^(START_|CLOSE_)/, ''); + switch (tag) { + case 'BOLD_TEXT': + case 'EMPHASISED_TEXT': + case 'ITALIC_TEXT': + case 'LINE_BREAK': + case 'STRIKETHROUGH_TEXT': + case 'UNDERLINED_TEXT': + return 'fmt'; + case 'TAG_IMG': + return 'image'; + case 'LINK': + return 'link'; + default: + return /^(START_|CLOSE_)/.test(placeholder) ? 'other' : null; + } +} 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 797b455818..650c91e953 100644 --- a/packages/localize/src/tools/test/extract/integration/main_spec.ts +++ b/packages/localize/src/tools/test/extract/integration/main_spec.ts @@ -202,9 +202,9 @@ runInEachFileSystem(() => { ` `, ` `, ` `, - ` pre` + - `inner-prebold` + - `inner-postpost`, + ` pre` + + `inner-prebold` + + `inner-postpost`, ` `, ` test_files/test.js`, ` 9,10`, @@ -279,8 +279,8 @@ runInEachFileSystem(() => { ` test_files/test.js:9,10`, ` `, ` `, - ` pre` + - `inner-prebold` + + ` pre` + + `inner-prebold` + `inner-postpost`, ` `, ` `, 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 9697a8cfda..5ca658aa6f 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 @@ -99,18 +99,18 @@ runInEachFileSystem(() => { ` abc`, ` `, ` `, - ` ac`, + ` ac`, ` some description`, ` `, ` `, - ` ac`, + ` ac`, ` `, ` file.ts`, ` 3,4`, ` `, ` `, ` `, - ` b`, + ` b`, ` `, ` `, ` a`, @@ -128,7 +128,7 @@ runInEachFileSystem(() => { ` pre-ICU {VAR_SELECT, select, a {a} b {} c {pre post}} post-ICU`, ` `, ` `, - ` {VAR_PLURAL, plural, one {something bold} other {pre middle post}}`, + ` {VAR_PLURAL, plural, one {something bold} other {pre middle post}}`, ` `, ` `, ` `, @@ -230,6 +230,64 @@ runInEachFileSystem(() => { `\n`, ].join('\n')); }); + + it('should render the "ctype" for line breaks', () => { + const serializer = new Xliff1TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize([mockMessage('1', ['a', 'b'], ['LINE_BREAK'], {})]); + expect(output).toContain( + 'ab', + ); + }); + + it('should render the "ctype" for images', () => { + const serializer = new Xliff1TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize([mockMessage('2', ['a', 'b'], ['TAG_IMG'], {})]); + expect(output).toContain( + 'ab', + ); + }); + + it('should render the "ctype" for bold elements', () => { + const serializer = new Xliff1TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize( + [mockMessage('3', ['a', 'b', 'c'], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {})]); + expect(output).toContain( + 'abc', + ); + }); + + it('should render the "ctype" for headings', () => { + const serializer = new Xliff1TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize([mockMessage( + '4', ['a', 'b', 'c'], ['START_HEADING_LEVEL1', 'CLOSE_HEADING_LEVEL1'], {})]); + expect(output).toContain( + 'abc', + ); + }); + + it('should render the "ctype" for span elements', () => { + const serializer = new Xliff1TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize( + [mockMessage('5', ['a', 'b', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {})]); + expect(output).toContain( + 'abc', + ); + }); + + it('should render the "ctype" for div elements', () => { + const serializer = new Xliff1TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize( + [mockMessage('6', ['a', 'b', 'c'], ['START_TAG_DIV', 'CLOSE_TAG_DIV'], {})]); + expect(output).toContain( + 'abc', + ); + }); }); }); }); 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 832d517ff7..b8f812d40d 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 @@ -108,7 +108,7 @@ runInEachFileSystem(() => { ` some description`, ` `, ` `, - ` ac`, + ` ac`, ` `, ` `, ` `, @@ -116,12 +116,12 @@ runInEachFileSystem(() => { ` file.ts:3,4`, ` `, ` `, - ` ac`, + ` ac`, ` `, ` `, ` `, ` `, - ` b`, + ` b`, ` `, ` `, ` `, @@ -151,7 +151,7 @@ runInEachFileSystem(() => { ` `, ` `, ` `, - ` {VAR_PLURAL, plural, one {something bold} other {pre middle post}}`, + ` {VAR_PLURAL, plural, one {something bold} other {pre middle post}}`, ` `, ` `, ` `, @@ -243,6 +243,64 @@ runInEachFileSystem(() => { `\n`, ].join('\n')); }); + + it('should render the "type" for line breaks', () => { + const serializer = new Xliff2TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize([mockMessage('1', ['a', 'b'], ['LINE_BREAK'], {})]); + expect(output).toContain( + 'ab', + ); + }); + + it('should render the "type" for images', () => { + const serializer = new Xliff2TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize([mockMessage('2', ['a', 'b'], ['TAG_IMG'], {})]); + expect(output).toContain( + 'ab', + ); + }); + + it('should render the "type" for bold elements', () => { + const serializer = new Xliff2TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize( + [mockMessage('3', ['a', 'b', 'c'], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {})]); + expect(output).toContain( + 'abc', + ); + }); + + it('should render the "type" for heading elements', () => { + const serializer = new Xliff2TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize([mockMessage( + '4', ['a', 'b', 'c'], ['START_HEADING_LEVEL1', 'CLOSE_HEADING_LEVEL1'], {})]); + expect(output).toContain( + 'abc', + ); + }); + + it('should render the "type" for span elements', () => { + const serializer = new Xliff2TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize( + [mockMessage('5', ['a', 'b', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {})]); + expect(output).toContain( + 'abc', + ); + }); + + it('should render the "type" for div elements', () => { + const serializer = new Xliff2TranslationSerializer( + 'xx', absoluteFrom('/project'), useLegacyIds, options); + const output = serializer.serialize( + [mockMessage('6', ['a', 'b', 'c'], ['START_TAG_DIV', 'CLOSE_TAG_DIV'], {})]); + expect(output).toContain( + 'abc', + ); + }); }); }); });