fix(compiler): fix support for html-like text in translatable attributes (#23053)
PR Close #23053
This commit is contained in:
parent
7de13b60d6
commit
28058b784b
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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('');
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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><b>bold</b></source>
|
||||||
|
<target><b>gras</b></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><b>bold</b></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>
|
||||||
|
|
|
@ -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><b>bold</b></source>
|
||||||
|
<target><b>gras</b></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><b>bold</b></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">
|
||||||
|
|
|
@ -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><div></ex></ph>avec <ph name="START_TAG_DIV"><ex><div></ex></ph>des espaces réservés<ph name="CLOSE_TAG_DIV"><ex></div></ex></ph> imbriqués<ph name="CLOSE_TAG_DIV"><ex></div></ex></ph></translation>
|
<translation id="5415448997399451992"><ph name="START_TAG_DIV"><ex><div></ex></ph>avec <ph name="START_TAG_DIV"><ex><div></ex></ph>des espaces réservés<ph name="CLOSE_TAG_DIV"><ex></div></ex></ph> imbriqués<ph name="CLOSE_TAG_DIV"><ex></div></ex></ph></translation>
|
||||||
<translation id="5525133077318024839">sur des balises non traductibles</translation>
|
<translation id="5525133077318024839">sur des balises non traductibles</translation>
|
||||||
|
<translation id="2174788525135228764"><b>gras</b></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><i></ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex></i></ex></ph></msg>
|
<msg id="3780349238193953556"><source>file.ts:9</source><source>file.ts:10</source><ph name="START_ITALIC_TEXT"><ex><i></ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex></i></ex></ph></msg>
|
||||||
<msg id="5415448997399451992"><source>file.ts:11</source><ph name="START_TAG_DIV"><ex><div></ex></ph>with <ph name="START_TAG_DIV"><ex><div></ex></ph>nested<ph name="CLOSE_TAG_DIV"><ex></div></ex></ph> placeholders<ph name="CLOSE_TAG_DIV"><ex></div></ex></ph></msg>
|
<msg id="5415448997399451992"><source>file.ts:11</source><ph name="START_TAG_DIV"><ex><div></ex></ph>with <ph name="START_TAG_DIV"><ex><div></ex></ph>nested<ph name="CLOSE_TAG_DIV"><ex></div></ex></ph> placeholders<ph name="CLOSE_TAG_DIV"><ex></div></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><b>bold</b></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><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></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><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph>} }</msg>
|
||||||
<msg id="703464324060964421"><source>file.ts:22,24</source>
|
<msg id="703464324060964421"><source>file.ts:22,24</source>
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
Loading…
Reference in New Issue