Skip to content

Commit 4f7c369

Browse files
vicbalxhub
authored andcommitted
fix(compiler): fix support for html-like text in translatable attributes (#23053)
PR Close #23053
1 parent e7b2e97 commit 4f7c369

7 files changed

+55
-8
lines changed

packages/compiler/src/i18n/serializers/xml_helper.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class Declaration implements Node {
5454

5555
constructor(unescapedAttrs: {[k: string]: string}) {
5656
Object.keys(unescapedAttrs).forEach((k: string) => {
57-
this.attrs[k] = _escapeXml(unescapedAttrs[k]);
57+
this.attrs[k] = escapeXml(unescapedAttrs[k]);
5858
});
5959
}
6060

@@ -74,7 +74,7 @@ export class Tag implements Node {
7474
public name: string, unescapedAttrs: {[k: string]: string} = {},
7575
public children: Node[] = []) {
7676
Object.keys(unescapedAttrs).forEach((k: string) => {
77-
this.attrs[k] = _escapeXml(unescapedAttrs[k]);
77+
this.attrs[k] = escapeXml(unescapedAttrs[k]);
7878
});
7979
}
8080

@@ -83,7 +83,7 @@ export class Tag implements Node {
8383

8484
export class Text implements Node {
8585
value: string;
86-
constructor(unescapedValue: string) { this.value = _escapeXml(unescapedValue); }
86+
constructor(unescapedValue: string) { this.value = escapeXml(unescapedValue); }
8787

8888
visit(visitor: IVisitor): any { return visitor.visitText(this); }
8989
}
@@ -100,7 +100,8 @@ const _ESCAPED_CHARS: [RegExp, string][] = [
100100
[/>/g, '>'],
101101
];
102102

103-
function _escapeXml(text: string): string {
103+
// Escape `_ESCAPED_CHARS` characters in the given text with encoded entities
104+
export function escapeXml(text: string): string {
104105
return _ESCAPED_CHARS.reduce(
105106
(text: string, entry: [RegExp, string]) => text.replace(entry[0], entry[1]), text);
106107
}

packages/compiler/src/i18n/translation_bundle.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {Console} from '../util';
1414
import * as i18n from './i18n_ast';
1515
import {I18nError} from './parse_util';
1616
import {PlaceholderMapper, Serializer} from './serializers/serializer';
17+
import {escapeXml} from './serializers/xml_helper';
1718

1819

1920
/**
@@ -88,7 +89,11 @@ class I18nToHtmlVisitor implements i18n.Visitor {
8889
};
8990
}
9091

91-
visitText(text: i18n.Text, context?: any): string { return text.value; }
92+
visitText(text: i18n.Text, context?: any): string {
93+
// `convert()` uses an `HtmlParser` to return `html.Node`s
94+
// we should then make sure that any special characters are escaped
95+
return escapeXml(text.value);
96+
}
9297

9398
visitContainer(container: i18n.Container, context?: any): any {
9499
return container.children.map(n => n.visit(this)).join('');

packages/compiler/test/i18n/integration_common.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ export function validateHtml(
4747
expectHtml(el, '#i18n-3b')
4848
.toBe(
4949
'<div id="i18n-3b"><p><i class="preserved-on-placeholders">avec des espaces réservés</i></p></div>');
50-
expectHtml(el, '#i18n-4').toBe('<p id="i18n-4" title="sur des balises non traductibles"></p>');
50+
expectHtml(el, '#i18n-4')
51+
.toBe('<p data-html="<b>gras</b>" id="i18n-4" title="sur des balises non traductibles"></p>');
5152
expectHtml(el, '#i18n-5').toBe('<p id="i18n-5" title="sur des balises traductibles"></p>');
5253
expectHtml(el, '#i18n-6').toBe('<p id="i18n-6" title=""></p>');
5354

@@ -117,7 +118,7 @@ export const HTML = `
117118
<div id="i18n-3c"><div i18n><div>with <div>nested</div> placeholders</div></div></div>
118119
119120
<div>
120-
<p id="i18n-4" i18n-title title="on not translatable node"></p>
121+
<p id="i18n-4" i18n-title title="on not translatable node" i18n-data-html data-html="<b>bold</b>"></p>
121122
<p id="i18n-5" i18n i18n-title title="on translatable node"></p>
122123
<p id="i18n-6" i18n-title title></p>
123124
</div>

packages/compiler/test/i18n/integration_xliff2_spec.ts

+14
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ const XLIFF2_TOMERGE = `
9595
<target>sur des balises non traductibles</target>
9696
</segment>
9797
</unit>
98+
<unit id="2174788525135228764">
99+
<segment>
100+
<source>&lt;b&gt;bold&lt;/b&gt;</source>
101+
<target>&lt;b&gt;gras&lt;/b&gt;</target>
102+
</segment>
103+
</unit>
98104
<unit id="8670732454866344690">
99105
<segment>
100106
<source>on translatable node</source>
@@ -267,6 +273,14 @@ const XLIFF2_EXTRACTED = `
267273
<source>on not translatable node</source>
268274
</segment>
269275
</unit>
276+
<unit id="2174788525135228764">
277+
<notes>
278+
<note category="location">file.ts:14</note>
279+
</notes>
280+
<segment>
281+
<source>&lt;b&gt;bold&lt;/b&gt;</source>
282+
</segment>
283+
</unit>
270284
<unit id="8670732454866344690">
271285
<notes>
272286
<note category="location">file.ts:15</note>

packages/compiler/test/i18n/integration_xliff_spec.ts

+11
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ const XLIFF_TOMERGE = `
8585
<source>on not translatable node</source>
8686
<target>sur des balises non traductibles</target>
8787
</trans-unit>
88+
<trans-unit id="480aaeeea1570bc1dde6b8404e380dee11ed0759" datatype="html">
89+
<source>&lt;b&gt;bold&lt;/b&gt;</source>
90+
<target>&lt;b&gt;gras&lt;/b&gt;</target>
91+
</trans-unit>
8892
<trans-unit id="67162b5af5f15fd0eb6480c88688dafdf952b93a" datatype="html">
8993
<source>on translatable node</source>
9094
<target>sur des balises traductibles</target>
@@ -215,6 +219,13 @@ const XLIFF_EXTRACTED = `
215219
<context context-type="linenumber">14</context>
216220
</context-group>
217221
</trans-unit>
222+
<trans-unit id="480aaeeea1570bc1dde6b8404e380dee11ed0759" datatype="html">
223+
<source>&lt;b&gt;bold&lt;/b&gt;</source>
224+
<context-group purpose="location">
225+
<context context-type="sourcefile">file.ts</context>
226+
<context context-type="linenumber">14</context>
227+
</context-group>
228+
</trans-unit>
218229
<trans-unit id="67162b5af5f15fd0eb6480c88688dafdf952b93a" datatype="html">
219230
<source>on translatable node</source>
220231
<context-group purpose="location">

packages/compiler/test/i18n/integration_xmb_xtb_spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const XTB = `
6363
<translation id="3780349238193953556"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
6464
<translation id="5415448997399451992"><ph name="START_TAG_DIV"><ex>&lt;div&gt;</ex></ph>avec <ph name="START_TAG_DIV"><ex>&lt;div&gt;</ex></ph>des espaces réservés<ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph> imbriqués<ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph></translation>
6565
<translation id="5525133077318024839">sur des balises non traductibles</translation>
66+
<translation id="2174788525135228764">&lt;b&gt;gras&lt;/b&gt;</translation>
6667
<translation id="8670732454866344690">sur des balises traductibles</translation>
6768
<translation id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation>
6869
<translation id="703464324060964421"><ph name="ICU"/></translation>
@@ -93,6 +94,7 @@ const XMB = `<msg id="615790887472569365"><source>file.ts:3</source>i18n attribu
9394
<msg id="3780349238193953556"><source>file.ts:9</source><source>file.ts:10</source><ph name="START_ITALIC_TEXT"><ex>&lt;i&gt;</ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex>&lt;/i&gt;</ex></ph></msg>
9495
<msg id="5415448997399451992"><source>file.ts:11</source><ph name="START_TAG_DIV"><ex>&lt;div&gt;</ex></ph>with <ph name="START_TAG_DIV"><ex>&lt;div&gt;</ex></ph>nested<ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph> placeholders<ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph></msg>
9596
<msg id="5525133077318024839"><source>file.ts:14</source>on not translatable node</msg>
97+
<msg id="2174788525135228764"><source>file.ts:14</source>&lt;b&gt;bold&lt;/b&gt;</msg>
9698
<msg id="8670732454866344690"><source>file.ts:15</source>on translatable node</msg>
9799
<msg id="4593805537723189714"><source>file.ts:20</source><source>file.ts:37</source>{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
98100
<msg id="703464324060964421"><source>file.ts:22,24</source>

packages/compiler/test/i18n/translation_bundle_spec.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {MissingTranslationStrategy} from '@angular/core';
1010

1111
import * as i18n from '../../src/i18n/i18n_ast';
1212
import {TranslationBundle} from '../../src/i18n/translation_bundle';
13+
import * as html from '../../src/ml_parser/ast';
1314
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
1415
import {serializeNodes} from '../ml_parser/ast_serializer_spec';
16+
1517
import {_extractMessages} from './i18n_parser_spec';
1618

1719
{
@@ -22,13 +24,24 @@ import {_extractMessages} from './i18n_parser_spec';
2224
const span = new ParseSourceSpan(startLocation, endLocation);
2325
const srcNode = new i18n.Text('src', span);
2426

25-
it('should translate a plain message', () => {
27+
it('should translate a plain text', () => {
2628
const msgMap = {foo: [new i18n.Text('bar', null !)]};
2729
const tb = new TranslationBundle(msgMap, null, (_) => 'foo');
2830
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
2931
expect(serializeNodes(tb.get(msg))).toEqual(['bar']);
3032
});
3133

34+
it('should translate html-like plain text', () => {
35+
const msgMap = {foo: [new i18n.Text('<p>bar</p>', null !)]};
36+
const tb = new TranslationBundle(msgMap, null, (_) => 'foo');
37+
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
38+
const nodes = tb.get(msg);
39+
expect(nodes.length).toEqual(1);
40+
const textNode: html.Text = nodes[0] as any;
41+
expect(textNode instanceof html.Text).toEqual(true);
42+
expect(textNode.value).toBe('<p>bar</p>');
43+
});
44+
3245
it('should translate a message with placeholder', () => {
3346
const msgMap = {
3447
foo: [

0 commit comments

Comments
 (0)