From 32163ef355af4f446fa90b087f66bac5bf6429ac 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 78006a29806..f6e799c41b3 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 fb926abfdbd..4fee339e592 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 797b4558181..650c91e9539 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 9697a8cfdaa..5ca658aa6fa 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 832d517ff7f..b8f812d40d4 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', + ); + }); }); }); });