fix(compiler): fix support for html-like text in translatable attributes (#23053)

PR Close #23053
This commit is contained in:
Victor Berchet 2018-03-28 22:10:08 -07:00 committed by Alex Rickabaugh
parent 7de13b60d6
commit 28058b784b
7 changed files with 55 additions and 8 deletions

View File

@ -54,7 +54,7 @@ export class Declaration implements Node {
constructor(unescapedAttrs: {[k: string]: string}) { constructor(unescapedAttrs: {[k: string]: string}) {
Object.keys(unescapedAttrs).forEach((k: string) => { Object.keys(unescapedAttrs).forEach((k: string) => {
this.attrs[k] = _escapeXml(unescapedAttrs[k]); this.attrs[k] = escapeXml(unescapedAttrs[k]);
}); });
} }
@ -74,7 +74,7 @@ export class Tag implements Node {
public name: string, unescapedAttrs: {[k: string]: string} = {}, public name: string, unescapedAttrs: {[k: string]: string} = {},
public children: Node[] = []) { public children: Node[] = []) {
Object.keys(unescapedAttrs).forEach((k: string) => { Object.keys(unescapedAttrs).forEach((k: string) => {
this.attrs[k] = _escapeXml(unescapedAttrs[k]); this.attrs[k] = escapeXml(unescapedAttrs[k]);
}); });
} }
@ -83,7 +83,7 @@ export class Tag implements Node {
export class Text implements Node { export class Text implements Node {
value: string; value: string;
constructor(unescapedValue: string) { this.value = _escapeXml(unescapedValue); } constructor(unescapedValue: string) { this.value = escapeXml(unescapedValue); }
visit(visitor: IVisitor): any { return visitor.visitText(this); } visit(visitor: IVisitor): any { return visitor.visitText(this); }
} }
@ -100,7 +100,8 @@ const _ESCAPED_CHARS: [RegExp, string][] = [
[/>/g, '>'], [/>/g, '>'],
]; ];
function _escapeXml(text: string): string { // Escape `_ESCAPED_CHARS` characters in the given text with encoded entities
export function escapeXml(text: string): string {
return _ESCAPED_CHARS.reduce( return _ESCAPED_CHARS.reduce(
(text: string, entry: [RegExp, string]) => text.replace(entry[0], entry[1]), text); (text: string, entry: [RegExp, string]) => text.replace(entry[0], entry[1]), text);
} }

View File

@ -14,6 +14,7 @@ import {Console} from '../util';
import * as i18n from './i18n_ast'; import * as i18n from './i18n_ast';
import {I18nError} from './parse_util'; import {I18nError} from './parse_util';
import {PlaceholderMapper, Serializer} from './serializers/serializer'; import {PlaceholderMapper, Serializer} from './serializers/serializer';
import {escapeXml} from './serializers/xml_helper';
/** /**
@ -88,7 +89,11 @@ class I18nToHtmlVisitor implements i18n.Visitor {
}; };
} }
visitText(text: i18n.Text, context?: any): string { return text.value; } visitText(text: i18n.Text, context?: any): string {
// `convert()` uses an `HtmlParser` to return `html.Node`s
// we should then make sure that any special characters are escaped
return escapeXml(text.value);
}
visitContainer(container: i18n.Container, context?: any): any { visitContainer(container: i18n.Container, context?: any): any {
return container.children.map(n => n.visit(this)).join(''); return container.children.map(n => n.visit(this)).join('');

View File

@ -47,7 +47,8 @@ export function validateHtml(
expectHtml(el, '#i18n-3b') expectHtml(el, '#i18n-3b')
.toBe( .toBe(
'<div id="i18n-3b"><p><i class="preserved-on-placeholders">avec des espaces réservés</i></p></div>'); '<div id="i18n-3b"><p><i class="preserved-on-placeholders">avec des espaces réservés</i></p></div>');
expectHtml(el, '#i18n-4').toBe('<p id="i18n-4" title="sur des balises non traductibles"></p>'); expectHtml(el, '#i18n-4')
.toBe('<p data-html="<b>gras</b>" id="i18n-4" title="sur des balises non traductibles"></p>');
expectHtml(el, '#i18n-5').toBe('<p id="i18n-5" title="sur des balises traductibles"></p>'); expectHtml(el, '#i18n-5').toBe('<p id="i18n-5" title="sur des balises traductibles"></p>');
expectHtml(el, '#i18n-6').toBe('<p id="i18n-6" title=""></p>'); expectHtml(el, '#i18n-6').toBe('<p id="i18n-6" title=""></p>');
@ -117,7 +118,7 @@ export const HTML = `
<div id="i18n-3c"><div i18n><div>with <div>nested</div> placeholders</div></div></div> <div id="i18n-3c"><div i18n><div>with <div>nested</div> placeholders</div></div></div>
<div> <div>
<p id="i18n-4" i18n-title title="on not translatable node"></p> <p id="i18n-4" i18n-title title="on not translatable node" i18n-data-html data-html="<b>bold</b>"></p>
<p id="i18n-5" i18n i18n-title title="on translatable node"></p> <p id="i18n-5" i18n i18n-title title="on translatable node"></p>
<p id="i18n-6" i18n-title title></p> <p id="i18n-6" i18n-title title></p>
</div> </div>

View File

@ -95,6 +95,12 @@ const XLIFF2_TOMERGE = `
<target>sur des balises non traductibles</target> <target>sur des balises non traductibles</target>
</segment> </segment>
</unit> </unit>
<unit id="2174788525135228764">
<segment>
<source>&lt;b&gt;bold&lt;/b&gt;</source>
<target>&lt;b&gt;gras&lt;/b&gt;</target>
</segment>
</unit>
<unit id="8670732454866344690"> <unit id="8670732454866344690">
<segment> <segment>
<source>on translatable node</source> <source>on translatable node</source>
@ -267,6 +273,14 @@ const XLIFF2_EXTRACTED = `
<source>on not translatable node</source> <source>on not translatable node</source>
</segment> </segment>
</unit> </unit>
<unit id="2174788525135228764">
<notes>
<note category="location">file.ts:14</note>
</notes>
<segment>
<source>&lt;b&gt;bold&lt;/b&gt;</source>
</segment>
</unit>
<unit id="8670732454866344690"> <unit id="8670732454866344690">
<notes> <notes>
<note category="location">file.ts:15</note> <note category="location">file.ts:15</note>

View File

@ -85,6 +85,10 @@ const XLIFF_TOMERGE = `
<source>on not translatable node</source> <source>on not translatable node</source>
<target>sur des balises non traductibles</target> <target>sur des balises non traductibles</target>
</trans-unit> </trans-unit>
<trans-unit id="480aaeeea1570bc1dde6b8404e380dee11ed0759" datatype="html">
<source>&lt;b&gt;bold&lt;/b&gt;</source>
<target>&lt;b&gt;gras&lt;/b&gt;</target>
</trans-unit>
<trans-unit id="67162b5af5f15fd0eb6480c88688dafdf952b93a" datatype="html"> <trans-unit id="67162b5af5f15fd0eb6480c88688dafdf952b93a" datatype="html">
<source>on translatable node</source> <source>on translatable node</source>
<target>sur des balises traductibles</target> <target>sur des balises traductibles</target>
@ -215,6 +219,13 @@ const XLIFF_EXTRACTED = `
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="480aaeeea1570bc1dde6b8404e380dee11ed0759" datatype="html">
<source>&lt;b&gt;bold&lt;/b&gt;</source>
<context-group purpose="location">
<context context-type="sourcefile">file.ts</context>
<context context-type="linenumber">14</context>
</context-group>
</trans-unit>
<trans-unit id="67162b5af5f15fd0eb6480c88688dafdf952b93a" datatype="html"> <trans-unit id="67162b5af5f15fd0eb6480c88688dafdf952b93a" datatype="html">
<source>on translatable node</source> <source>on translatable node</source>
<context-group purpose="location"> <context-group purpose="location">

View File

@ -63,6 +63,7 @@ const XTB = `
<translation id="3780349238193953556"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation> <translation id="3780349238193953556"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
<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> <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>
<translation id="5525133077318024839">sur des balises non traductibles</translation> <translation id="5525133077318024839">sur des balises non traductibles</translation>
<translation id="2174788525135228764">&lt;b&gt;gras&lt;/b&gt;</translation>
<translation id="8670732454866344690">sur des balises traductibles</translation> <translation id="8670732454866344690">sur des balises traductibles</translation>
<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> <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>
<translation id="703464324060964421"><ph name="ICU"/></translation> <translation id="703464324060964421"><ph name="ICU"/></translation>
@ -93,6 +94,7 @@ const XMB = `<msg id="615790887472569365"><source>file.ts:3</source>i18n attribu
<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> <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>
<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> <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>
<msg id="5525133077318024839"><source>file.ts:14</source>on not translatable node</msg> <msg id="5525133077318024839"><source>file.ts:14</source>on not translatable node</msg>
<msg id="2174788525135228764"><source>file.ts:14</source>&lt;b&gt;bold&lt;/b&gt;</msg>
<msg id="8670732454866344690"><source>file.ts:15</source>on translatable node</msg> <msg id="8670732454866344690"><source>file.ts:15</source>on translatable node</msg>
<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> <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>
<msg id="703464324060964421"><source>file.ts:22,24</source> <msg id="703464324060964421"><source>file.ts:22,24</source>

View File

@ -10,8 +10,10 @@ import {MissingTranslationStrategy} from '@angular/core';
import * as i18n from '../../src/i18n/i18n_ast'; import * as i18n from '../../src/i18n/i18n_ast';
import {TranslationBundle} from '../../src/i18n/translation_bundle'; import {TranslationBundle} from '../../src/i18n/translation_bundle';
import * as html from '../../src/ml_parser/ast';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util'; import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
import {serializeNodes} from '../ml_parser/ast_serializer_spec'; import {serializeNodes} from '../ml_parser/ast_serializer_spec';
import {_extractMessages} from './i18n_parser_spec'; import {_extractMessages} from './i18n_parser_spec';
{ {
@ -22,13 +24,24 @@ import {_extractMessages} from './i18n_parser_spec';
const span = new ParseSourceSpan(startLocation, endLocation); const span = new ParseSourceSpan(startLocation, endLocation);
const srcNode = new i18n.Text('src', span); const srcNode = new i18n.Text('src', span);
it('should translate a plain message', () => { it('should translate a plain text', () => {
const msgMap = {foo: [new i18n.Text('bar', null !)]}; const msgMap = {foo: [new i18n.Text('bar', null !)]};
const tb = new TranslationBundle(msgMap, null, (_) => 'foo'); const tb = new TranslationBundle(msgMap, null, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i'); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(serializeNodes(tb.get(msg))).toEqual(['bar']); expect(serializeNodes(tb.get(msg))).toEqual(['bar']);
}); });
it('should translate html-like plain text', () => {
const msgMap = {foo: [new i18n.Text('<p>bar</p>', null !)]};
const tb = new TranslationBundle(msgMap, null, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
const nodes = tb.get(msg);
expect(nodes.length).toEqual(1);
const textNode: html.Text = nodes[0] as any;
expect(textNode instanceof html.Text).toEqual(true);
expect(textNode.value).toBe('<p>bar</p>');
});
it('should translate a message with placeholder', () => { it('should translate a message with placeholder', () => {
const msgMap = { const msgMap = {
foo: [ foo: [