Sitelet https://github.com/angular/angular/commit/ea1baf9
Skip to content

Commit ea1baf9

Browse files
petebacondarwinalxhub
authored andcommitted
fix(localize): render placeholder types in extracted XLIFF files (#39459)
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 #39459
1 parent ec30175 commit ea1baf9

5 files changed

Lines changed: 236 additions & 15 deletions

File tree

‎packages/localize/src/tools/src/extract/translation_files/xliff1_translation_serializer.ts‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ export class Xliff1TranslationSerializer implements TranslationSerializer {
100100

101101
private serializePlaceholder(xml: XmlFile, id: string, text: string|undefined): void {
102102
const attrs: Record<string, string> = {id};
103+
const ctype = getCtypeForPlaceholder(id);
104+
if (ctype !== null) {
105+
attrs.ctype = ctype;
106+
}
103107
if (text !== undefined) {
104108
attrs['equiv-text'] = text;
105109
}
@@ -148,3 +152,68 @@ export class Xliff1TranslationSerializer implements TranslationSerializer {
148152
message.id;
149153
}
150154
}
155+
156+
/**
157+
* Compute the value of the `ctype` attribute from the `placeholder` name.
158+
*
159+
* The placeholder can take the following forms:
160+
*
161+
* - `START_BOLD_TEXT`/`END_BOLD_TEXT`
162+
* - `TAG_<ELEMENT_NAME>`
163+
* - `START_TAG_<ELEMENT_NAME>`
164+
* - `CLOSE_TAG_<ELEMENT_NAME>`
165+
*
166+
* In these cases the element name of the tag is extracted from the placeholder name and returned as
167+
* `x-<element_name>`.
168+
*
169+
* Line breaks and images are special cases.
170+
*/
171+
function getCtypeForPlaceholder(placeholder: string): string|null {
172+
const tag = placeholder.replace(/^(START_|CLOSE_)/, '');
173+
switch (tag) {
174+
case 'LINE_BREAK':
175+
return 'lb';
176+
case 'TAG_IMG':
177+
return 'image';
178+
default:
179+
const element = tag.startsWith('TAG_') ?
180+
tag.replace(/^TAG_(.+)/, (_, tagName: string) => tagName.toLowerCase()) :
181+
TAG_MAP[tag];
182+
if (element === undefined) {
183+
return null;
184+
}
185+
return `x-${element}`;
186+
}
187+
}
188+
189+
const TAG_MAP: Record<string, string> = {
190+
'LINK': 'a',
191+
'BOLD_TEXT': 'b',
192+
'EMPHASISED_TEXT': 'em',
193+
'HEADING_LEVEL1': 'h1',
194+
'HEADING_LEVEL2': 'h2',
195+
'HEADING_LEVEL3': 'h3',
196+
'HEADING_LEVEL4': 'h4',
197+
'HEADING_LEVEL5': 'h5',
198+
'HEADING_LEVEL6': 'h6',
199+
'HORIZONTAL_RULE': 'hr',
200+
'ITALIC_TEXT': 'i',
201+
'LIST_ITEM': 'li',
202+
'MEDIA_LINK': 'link',
203+
'ORDERED_LIST': 'ol',
204+
'PARAGRAPH': 'p',
205+
'QUOTATION': 'q',
206+
'STRIKETHROUGH_TEXT': 's',
207+
'SMALL_TEXT': 'small',
208+
'SUBSTRIPT': 'sub',
209+
'SUPERSCRIPT': 'sup',
210+
'TABLE_BODY': 'tbody',
211+
'TABLE_CELL': 'td',
212+
'TABLE_FOOTER': 'tfoot',
213+
'TABLE_HEADER_CELL': 'th',
214+
'TABLE_HEADER': 'thead',
215+
'TABLE_ROW': 'tr',
216+
'MONOSPACED_TEXT': 'tt',
217+
'UNDERLINED_TEXT': 'u',
218+
'UNORDERED_LIST': 'ul',
219+
};

‎packages/localize/src/tools/src/extract/translation_files/xliff2_translation_serializer.ts‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ export class Xliff2TranslationSerializer implements TranslationSerializer {
119119
equivStart: placeholderName,
120120
equivEnd: closingPlaceholderName,
121121
};
122+
const type = getTypeForPlaceholder(placeholderName);
123+
if (type !== null) {
124+
attrs.type = type;
125+
}
122126
if (text !== undefined) {
123127
attrs.dispStart = text;
124128
}
@@ -129,8 +133,14 @@ export class Xliff2TranslationSerializer implements TranslationSerializer {
129133
} else if (placeholderName.startsWith('CLOSE_')) {
130134
xml.endTag('pc');
131135
} else {
132-
const attrs:
133-
Record<string, string> = {id: `${this.currentPlaceholderId++}`, equiv: placeholderName};
136+
const attrs: Record<string, string> = {
137+
id: `${this.currentPlaceholderId++}`,
138+
equiv: placeholderName,
139+
};
140+
const type = getTypeForPlaceholder(placeholderName);
141+
if (type !== null) {
142+
attrs.type = type;
143+
}
134144
if (text !== undefined) {
135145
attrs.disp = text;
136146
}
@@ -166,3 +176,29 @@ export class Xliff2TranslationSerializer implements TranslationSerializer {
166176
message.id;
167177
}
168178
}
179+
180+
/**
181+
* Compute the value of the `type` attribute from the `placeholder` name.
182+
*
183+
* If the tag is not known but starts with `TAG_`, `START_TAG_` or `CLOSE_TAG_` then the type is
184+
* `other`. Certain formatting tags (e.g. bold, italic, etc) have type `fmt`. Line-breaks, images
185+
* and links are special cases.
186+
*/
187+
function getTypeForPlaceholder(placeholder: string): string|null {
188+
const tag = placeholder.replace(/^(START_|CLOSE_)/, '');
189+
switch (tag) {
190+
case 'BOLD_TEXT':
191+
case 'EMPHASISED_TEXT':
192+
case 'ITALIC_TEXT':
193+
case 'LINE_BREAK':
194+
case 'STRIKETHROUGH_TEXT':
195+
case 'UNDERLINED_TEXT':
196+
return 'fmt';
197+
case 'TAG_IMG':
198+
return 'image';
199+
case 'LINK':
200+
return 'link';
201+
default:
202+
return /^(START_|CLOSE_)/.test(placeholder) ? 'other' : null;
203+
}
204+
}

‎packages/localize/src/tools/test/extract/integration/main_spec.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,9 @@ runInEachFileSystem(() => {
202202
` </context-group>`,
203203
` </trans-unit>`,
204204
` <trans-unit id="2932901491976224757" datatype="html">`,
205-
` <source>pre<x id="START_TAG_SPAN" equiv-text="&apos;&lt;span&gt;&apos;"/>` +
206-
`inner-pre<x id="START_BOLD_TEXT" equiv-text="&apos;&lt;b&gt;&apos;"/>bold<x id="CLOSE_BOLD_TEXT" equiv-text="&apos;&lt;/b&gt;&apos;"/>` +
207-
`inner-post<x id="CLOSE_TAG_SPAN" equiv-text="&apos;&lt;/span&gt;&apos;"/>post</source>`,
205+
` <source>pre<x id="START_TAG_SPAN" ctype="x-span" equiv-text="&apos;&lt;span&gt;&apos;"/>` +
206+
`inner-pre<x id="START_BOLD_TEXT" ctype="x-b" equiv-text="&apos;&lt;b&gt;&apos;"/>bold<x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="&apos;&lt;/b&gt;&apos;"/>` +
207+
`inner-post<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&apos;&lt;/span&gt;&apos;"/>post</source>`,
208208
` <context-group purpose="location">`,
209209
` <context context-type="sourcefile">test_files/test.js</context>`,
210210
` <context context-type="linenumber">9,10</context>`,
@@ -279,8 +279,8 @@ runInEachFileSystem(() => {
279279
` <note category="location">test_files/test.js:9,10</note>`,
280280
` </notes>`,
281281
` <segment>`,
282-
` <source>pre<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" dispStart="&apos;&lt;span&gt;&apos;" dispEnd="&apos;&lt;/span&gt;&apos;">` +
283-
`inner-pre<pc id="1" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" dispStart="&apos;&lt;b&gt;&apos;" dispEnd="&apos;&lt;/b&gt;&apos;">bold</pc>` +
282+
` <source>pre<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other" dispStart="&apos;&lt;span&gt;&apos;" dispEnd="&apos;&lt;/span&gt;&apos;">` +
283+
`inner-pre<pc id="1" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart="&apos;&lt;b&gt;&apos;" dispEnd="&apos;&lt;/b&gt;&apos;">bold</pc>` +
284284
`inner-post</pc>post</source>`,
285285
` </segment>`,
286286
` </unit>`,

‎packages/localize/src/tools/test/extract/translation_files/xliff1_translation_serializer_spec.ts‎

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,18 @@ runInEachFileSystem(() => {
9999
` <source>a<x id="PH" equiv-text="placeholder + 1"/>b<x id="PH_1"/>c</source>`,
100100
` </trans-unit>`,
101101
` <trans-unit id="67890" datatype="html">`,
102-
` <source>a<x id="START_TAG_SPAN"/><x id="CLOSE_TAG_SPAN"/>c</source>`,
102+
` <source>a<x id="START_TAG_SPAN" ctype="x-span"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/>c</source>`,
103103
` <note priority="1" from="description">some description</note>`,
104104
` </trans-unit>`,
105105
` <trans-unit id="38705" datatype="html">`,
106-
` <source>a<x id="START_TAG_SPAN"/><x id="CLOSE_TAG_SPAN"/>c</source>`,
106+
` <source>a<x id="START_TAG_SPAN" ctype="x-span"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/>c</source>`,
107107
` <context-group purpose="location">`,
108108
` <context context-type="sourcefile">file.ts</context>`,
109109
` <context context-type="linenumber">3,4</context>`,
110110
` </context-group>`,
111111
` </trans-unit>`,
112112
` <trans-unit id="13579" datatype="html">`,
113-
` <source><x id="START_BOLD_TEXT"/>b<x id="CLOSE_BOLD_TEXT"/></source>`,
113+
` <source><x id="START_BOLD_TEXT" ctype="x-b"/>b<x id="CLOSE_BOLD_TEXT" ctype="x-b"/></source>`,
114114
` </trans-unit>`,
115115
` <trans-unit id="24680" datatype="html">`,
116116
` <source>a</source>`,
@@ -128,7 +128,7 @@ runInEachFileSystem(() => {
128128
` <source>pre-ICU {VAR_SELECT, select, a {a} b {<x id="INTERPOLATION"/>} c {pre <x id="INTERPOLATION_1"/> post}} post-ICU</source>`,
129129
` </trans-unit>`,
130130
` <trans-unit id="100001" datatype="html">`,
131-
` <source>{VAR_PLURAL, plural, one {<x id="START_BOLD_TEXT"/>something bold<x id="CLOSE_BOLD_TEXT"/>} other {pre <x id="START_TAG_SPAN"/>middle<x id="CLOSE_TAG_SPAN"/> post}}</source>`,
131+
` <source>{VAR_PLURAL, plural, one {<x id="START_BOLD_TEXT" ctype="x-b"/>something bold<x id="CLOSE_BOLD_TEXT" ctype="x-b"/>} other {pre <x id="START_TAG_SPAN" ctype="x-span"/>middle<x id="CLOSE_TAG_SPAN" ctype="x-span"/> post}}</source>`,
132132
` </trans-unit>`,
133133
` </body>`,
134134
` </file>`,
@@ -230,6 +230,64 @@ runInEachFileSystem(() => {
230230
`</xliff>\n`,
231231
].join('\n'));
232232
});
233+
234+
it('should render the "ctype" for line breaks', () => {
235+
const serializer = new Xliff1TranslationSerializer(
236+
'xx', absoluteFrom('/project'), useLegacyIds, options);
237+
const output = serializer.serialize([mockMessage('1', ['a', 'b'], ['LINE_BREAK'], {})]);
238+
expect(output).toContain(
239+
'<source>a<x id="LINE_BREAK" ctype="lb"/>b</source>',
240+
);
241+
});
242+
243+
it('should render the "ctype" for images', () => {
244+
const serializer = new Xliff1TranslationSerializer(
245+
'xx', absoluteFrom('/project'), useLegacyIds, options);
246+
const output = serializer.serialize([mockMessage('2', ['a', 'b'], ['TAG_IMG'], {})]);
247+
expect(output).toContain(
248+
'<source>a<x id="TAG_IMG" ctype="image"/>b</source>',
249+
);
250+
});
251+
252+
it('should render the "ctype" for bold elements', () => {
253+
const serializer = new Xliff1TranslationSerializer(
254+
'xx', absoluteFrom('/project'), useLegacyIds, options);
255+
const output = serializer.serialize(
256+
[mockMessage('3', ['a', 'b', 'c'], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {})]);
257+
expect(output).toContain(
258+
'<source>a<x id="START_BOLD_TEXT" ctype="x-b"/>b<x id="CLOSE_BOLD_TEXT" ctype="x-b"/>c</source>',
259+
);
260+
});
261+
262+
it('should render the "ctype" for headings', () => {
263+
const serializer = new Xliff1TranslationSerializer(
264+
'xx', absoluteFrom('/project'), useLegacyIds, options);
265+
const output = serializer.serialize([mockMessage(
266+
'4', ['a', 'b', 'c'], ['START_HEADING_LEVEL1', 'CLOSE_HEADING_LEVEL1'], {})]);
267+
expect(output).toContain(
268+
'<source>a<x id="START_HEADING_LEVEL1" ctype="x-h1"/>b<x id="CLOSE_HEADING_LEVEL1" ctype="x-h1"/>c</source>',
269+
);
270+
});
271+
272+
it('should render the "ctype" for span elements', () => {
273+
const serializer = new Xliff1TranslationSerializer(
274+
'xx', absoluteFrom('/project'), useLegacyIds, options);
275+
const output = serializer.serialize(
276+
[mockMessage('5', ['a', 'b', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {})]);
277+
expect(output).toContain(
278+
'<source>a<x id="START_TAG_SPAN" ctype="x-span"/>b<x id="CLOSE_TAG_SPAN" ctype="x-span"/>c</source>',
279+
);
280+
});
281+
282+
it('should render the "ctype" for div elements', () => {
283+
const serializer = new Xliff1TranslationSerializer(
284+
'xx', absoluteFrom('/project'), useLegacyIds, options);
285+
const output = serializer.serialize(
286+
[mockMessage('6', ['a', 'b', 'c'], ['START_TAG_DIV', 'CLOSE_TAG_DIV'], {})]);
287+
expect(output).toContain(
288+
'<source>a<x id="START_TAG_DIV" ctype="x-div"/>b<x id="CLOSE_TAG_DIV" ctype="x-div"/>c</source>',
289+
);
290+
});
233291
});
234292
});
235293
});

‎packages/localize/src/tools/test/extract/translation_files/xliff2_translation_serializer_spec.ts‎

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,20 @@ runInEachFileSystem(() => {
108108
` <note category="description">some description</note>`,
109109
` </notes>`,
110110
` <segment>`,
111-
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN"></pc>c</source>`,
111+
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other"></pc>c</source>`,
112112
` </segment>`,
113113
` </unit>`,
114114
` <unit id="location-only">`,
115115
` <notes>`,
116116
` <note category="location">file.ts:3,4</note>`,
117117
` </notes>`,
118118
` <segment>`,
119-
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN"></pc>c</source>`,
119+
` <source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other"></pc>c</source>`,
120120
` </segment>`,
121121
` </unit>`,
122122
` <unit id="13579">`,
123123
` <segment>`,
124-
` <source><pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT">b</pc></source>`,
124+
` <source><pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt">b</pc></source>`,
125125
` </segment>`,
126126
` </unit>`,
127127
` <unit id="24680">`,
@@ -151,7 +151,7 @@ runInEachFileSystem(() => {
151151
` </unit>`,
152152
` <unit id="100001">`,
153153
` <segment>`,
154-
` <source>{VAR_PLURAL, plural, one {<pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT">something bold</pc>} other {pre <pc id="1" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN">middle</pc> post}}</source>`,
154+
` <source>{VAR_PLURAL, plural, one {<pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt">something bold</pc>} other {pre <pc id="1" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other">middle</pc> post}}</source>`,
155155
` </segment>`,
156156
` </unit>`,
157157
` </file>`,
@@ -243,6 +243,64 @@ runInEachFileSystem(() => {
243243
`</xliff>\n`,
244244
].join('\n'));
245245
});
246+
247+
it('should render the "type" for line breaks', () => {
248+
const serializer = new Xliff2TranslationSerializer(
249+
'xx', absoluteFrom('/project'), useLegacyIds, options);
250+
const output = serializer.serialize([mockMessage('1', ['a', 'b'], ['LINE_BREAK'], {})]);
251+
expect(output).toContain(
252+
'<source>a<ph id="0" equiv="LINE_BREAK" type="fmt"/>b</source>',
253+
);
254+
});
255+
256+
it('should render the "type" for images', () => {
257+
const serializer = new Xliff2TranslationSerializer(
258+
'xx', absoluteFrom('/project'), useLegacyIds, options);
259+
const output = serializer.serialize([mockMessage('2', ['a', 'b'], ['TAG_IMG'], {})]);
260+
expect(output).toContain(
261+
'<source>a<ph id="0" equiv="TAG_IMG" type="image"/>b</source>',
262+
);
263+
});
264+
265+
it('should render the "type" for bold elements', () => {
266+
const serializer = new Xliff2TranslationSerializer(
267+
'xx', absoluteFrom('/project'), useLegacyIds, options);
268+
const output = serializer.serialize(
269+
[mockMessage('3', ['a', 'b', 'c'], ['START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'], {})]);
270+
expect(output).toContain(
271+
'<source>a<pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt">b</pc>c</source>',
272+
);
273+
});
274+
275+
it('should render the "type" for heading elements', () => {
276+
const serializer = new Xliff2TranslationSerializer(
277+
'xx', absoluteFrom('/project'), useLegacyIds, options);
278+
const output = serializer.serialize([mockMessage(
279+
'4', ['a', 'b', 'c'], ['START_HEADING_LEVEL1', 'CLOSE_HEADING_LEVEL1'], {})]);
280+
expect(output).toContain(
281+
'<source>a<pc id="0" equivStart="START_HEADING_LEVEL1" equivEnd="CLOSE_HEADING_LEVEL1" type="other">b</pc>c</source>',
282+
);
283+
});
284+
285+
it('should render the "type" for span elements', () => {
286+
const serializer = new Xliff2TranslationSerializer(
287+
'xx', absoluteFrom('/project'), useLegacyIds, options);
288+
const output = serializer.serialize(
289+
[mockMessage('5', ['a', 'b', 'c'], ['START_TAG_SPAN', 'CLOSE_TAG_SPAN'], {})]);
290+
expect(output).toContain(
291+
'<source>a<pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other">b</pc>c</source>',
292+
);
293+
});
294+
295+
it('should render the "type" for div elements', () => {
296+
const serializer = new Xliff2TranslationSerializer(
297+
'xx', absoluteFrom('/project'), useLegacyIds, options);
298+
const output = serializer.serialize(
299+
[mockMessage('6', ['a', 'b', 'c'], ['START_TAG_DIV', 'CLOSE_TAG_DIV'], {})]);
300+
expect(output).toContain(
301+
'<source>a<pc id="0" equivStart="START_TAG_DIV" equivEnd="CLOSE_TAG_DIV" type="other">b</pc>c</source>',
302+
);
303+
});
246304
});
247305
});
248306
});

0 commit comments

Comments
 (0)