Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/compiler/src/i18n/serializers/xml_helper.ts
Expand Up @@ -54,7 +54,7 @@ export class Declaration implements Node {

constructor(unescapedAttrs: {[k: string]: string}) {
Object.keys(unescapedAttrs).forEach((k: string) => {
this.attrs[k] = _escapeXml(unescapedAttrs[k]);
this.attrs[k] = escapeXml(unescapedAttrs[k]);
});
}

Expand All @@ -74,7 +74,7 @@ export class Tag implements Node {
public name: string, unescapedAttrs: {[k: string]: string} = {},
public children: Node[] = []) {
Object.keys(unescapedAttrs).forEach((k: string) => {
this.attrs[k] = _escapeXml(unescapedAttrs[k]);
this.attrs[k] = escapeXml(unescapedAttrs[k]);
});
}

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

export class Text implements Node {
value: string;
constructor(unescapedValue: string) { this.value = _escapeXml(unescapedValue); }
constructor(unescapedValue: string) { this.value = escapeXml(unescapedValue); }

visit(visitor: IVisitor): any { return visitor.visitText(this); }
}
Expand All @@ -100,7 +100,8 @@ const _ESCAPED_CHARS: [RegExp, string][] = [
[/>/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(
(text: string, entry: [RegExp, string]) => text.replace(entry[0], entry[1]), text);
}
7 changes: 6 additions & 1 deletion packages/compiler/src/i18n/translation_bundle.ts
Expand Up @@ -14,6 +14,7 @@ import {Console} from '../util';
import * as i18n from './i18n_ast';
import {I18nError} from './parse_util';
import {PlaceholderMapper, Serializer} from './serializers/serializer';
import {escapeXml} from './serializers/xml_helper';


/**
Expand Down Expand Up @@ -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 {
return container.children.map(n => n.visit(this)).join('');
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler/test/i18n/integration_common.ts
Expand Up @@ -47,7 +47,8 @@ export function validateHtml(
expectHtml(el, '#i18n-3b')
.toBe(
'<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-6').toBe('<p id="i18n-6" title=""></p>');

Expand Down Expand Up @@ -117,7 +118,7 @@ export const HTML = `
<div id="i18n-3c"><div i18n><div>with <div>nested</div> placeholders</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-6" i18n-title title></p>
</div>
Expand Down
14 changes: 14 additions & 0 deletions packages/compiler/test/i18n/integration_xliff2_spec.ts
Expand Up @@ -95,6 +95,12 @@ const XLIFF2_TOMERGE = `
<target>sur des balises non traductibles</target>
</segment>
</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">
<segment>
<source>on translatable node</source>
Expand Down Expand Up @@ -267,6 +273,14 @@ const XLIFF2_EXTRACTED = `
<source>on not translatable node</source>
</segment>
</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">
<notes>
<note category="location">file.ts:15</note>
Expand Down
11 changes: 11 additions & 0 deletions packages/compiler/test/i18n/integration_xliff_spec.ts
Expand Up @@ -85,6 +85,10 @@ const XLIFF_TOMERGE = `
<source>on not translatable node</source>
<target>sur des balises non traductibles</target>
</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">
<source>on translatable node</source>
<target>sur des balises traductibles</target>
Expand Down Expand Up @@ -215,6 +219,13 @@ const XLIFF_EXTRACTED = `
<context context-type="linenumber">14</context>
</context-group>
</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">
<source>on translatable node</source>
<context-group purpose="location">
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/test/i18n/integration_xmb_xtb_spec.ts
Expand Up @@ -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="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="2174788525135228764">&lt;b&gt;gras&lt;/b&gt;</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="703464324060964421"><ph name="ICU"/></translation>
Expand Down Expand Up @@ -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="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="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="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>
Expand Down
15 changes: 14 additions & 1 deletion packages/compiler/test/i18n/translation_bundle_spec.ts
Expand Up @@ -10,8 +10,10 @@ import {MissingTranslationStrategy} from '@angular/core';

import * as i18n from '../../src/i18n/i18n_ast';
import {TranslationBundle} from '../../src/i18n/translation_bundle';
import * as html from '../../src/ml_parser/ast';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
import {serializeNodes} from '../ml_parser/ast_serializer_spec';

import {_extractMessages} from './i18n_parser_spec';

{
Expand All @@ -22,13 +24,24 @@ import {_extractMessages} from './i18n_parser_spec';
const span = new ParseSourceSpan(startLocation, endLocation);
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 tb = new TranslationBundle(msgMap, null, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
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', () => {
const msgMap = {
foo: [
Expand Down