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', + ); + }); }); }); });