From 28058b784b6d74fff62e244cd0eefd491c0d8205 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Wed, 28 Mar 2018 22:10:08 -0700 Subject: [PATCH] fix(compiler): fix support for html-like text in translatable attributes (#23053) PR Close #23053 --- .../compiler/src/i18n/serializers/xml_helper.ts | 9 +++++---- packages/compiler/src/i18n/translation_bundle.ts | 7 ++++++- packages/compiler/test/i18n/integration_common.ts | 5 +++-- .../compiler/test/i18n/integration_xliff2_spec.ts | 14 ++++++++++++++ .../compiler/test/i18n/integration_xliff_spec.ts | 11 +++++++++++ .../test/i18n/integration_xmb_xtb_spec.ts | 2 ++ .../compiler/test/i18n/translation_bundle_spec.ts | 15 ++++++++++++++- 7 files changed, 55 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/i18n/serializers/xml_helper.ts b/packages/compiler/src/i18n/serializers/xml_helper.ts index 805592f7f5..27a8d10c5a 100644 --- a/packages/compiler/src/i18n/serializers/xml_helper.ts +++ b/packages/compiler/src/i18n/serializers/xml_helper.ts @@ -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]); }); } @@ -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]); }); } @@ -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); } } @@ -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); } diff --git a/packages/compiler/src/i18n/translation_bundle.ts b/packages/compiler/src/i18n/translation_bundle.ts index c1c5daf68c..242ff966d8 100644 --- a/packages/compiler/src/i18n/translation_bundle.ts +++ b/packages/compiler/src/i18n/translation_bundle.ts @@ -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'; /** @@ -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(''); diff --git a/packages/compiler/test/i18n/integration_common.ts b/packages/compiler/test/i18n/integration_common.ts index 400365f9ad..11027cbf75 100644 --- a/packages/compiler/test/i18n/integration_common.ts +++ b/packages/compiler/test/i18n/integration_common.ts @@ -47,7 +47,8 @@ export function validateHtml( expectHtml(el, '#i18n-3b') .toBe( '

avec des espaces réservés

'); - expectHtml(el, '#i18n-4').toBe('

'); + expectHtml(el, '#i18n-4') + .toBe('

'); expectHtml(el, '#i18n-5').toBe('

'); expectHtml(el, '#i18n-6').toBe('

'); @@ -117,7 +118,7 @@ export const HTML = `
with
nested
placeholders
-

+

diff --git a/packages/compiler/test/i18n/integration_xliff2_spec.ts b/packages/compiler/test/i18n/integration_xliff2_spec.ts index 657f8141f5..e7f82d09cc 100644 --- a/packages/compiler/test/i18n/integration_xliff2_spec.ts +++ b/packages/compiler/test/i18n/integration_xliff2_spec.ts @@ -95,6 +95,12 @@ const XLIFF2_TOMERGE = ` sur des balises non traductibles + + + <b>bold</b> + <b>gras</b> + + on translatable node @@ -267,6 +273,14 @@ const XLIFF2_EXTRACTED = ` on not translatable node + + + file.ts:14 + + + <b>bold</b> + + file.ts:15 diff --git a/packages/compiler/test/i18n/integration_xliff_spec.ts b/packages/compiler/test/i18n/integration_xliff_spec.ts index ba347cc1bb..5c8daf4984 100644 --- a/packages/compiler/test/i18n/integration_xliff_spec.ts +++ b/packages/compiler/test/i18n/integration_xliff_spec.ts @@ -85,6 +85,10 @@ const XLIFF_TOMERGE = ` on not translatable node sur des balises non traductibles + + <b>bold</b> + <b>gras</b> + on translatable node sur des balises traductibles @@ -215,6 +219,13 @@ const XLIFF_EXTRACTED = ` 14 + + <b>bold</b> + + file.ts + 14 + + on translatable node diff --git a/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts b/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts index 33f9543da4..51b074700f 100644 --- a/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts +++ b/packages/compiler/test/i18n/integration_xmb_xtb_spec.ts @@ -63,6 +63,7 @@ const XTB = ` avec des espaces réservés <div>avec <div>des espaces réservés</div> imbriqués</div> sur des balises non traductibles + <b>gras</b> sur des balises traductibles {VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {beaucoup}} @@ -93,6 +94,7 @@ const XMB = `file.ts:3i18n attribu file.ts:9file.ts:10<i>with placeholders</i> file.ts:11<div>with <div>nested</div> placeholders</div> file.ts:14on not translatable node + file.ts:14<b>bold</b> file.ts:15on translatable node file.ts:20file.ts:37{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>} } file.ts:22,24 diff --git a/packages/compiler/test/i18n/translation_bundle_spec.ts b/packages/compiler/test/i18n/translation_bundle_spec.ts index 6da0e99456..cac903ecca 100644 --- a/packages/compiler/test/i18n/translation_bundle_spec.ts +++ b/packages/compiler/test/i18n/translation_bundle_spec.ts @@ -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'; { @@ -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('

bar

', 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('

bar

'); + }); + it('should translate a message with placeholder', () => { const msgMap = { foo: [