/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; import {ExtractionResult, Message as HtmlMessage, extractAstMessages, mergeTranslations} from '../../src/i18n/extractor_merger'; import {getHtmlToI18nConverter} from '../../src/i18n/i18n_parser'; import {digestMessage} from '../../src/i18n/message_bundle'; import {TranslationBundle} from '../../src/i18n/translation_bundle'; import * as html from '../../src/ml_parser/ast'; import {HtmlParser} from '../../src/ml_parser/html_parser'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config'; import {serializeNodes} from '../ml_parser/ast_serializer_spec'; export function main() { describe('Extractor', () => { describe('elements', () => { it('should extract from elements', () => { expect(extract('
textnested
')).toEqual([ [['text', 'nested'], 'm', 'd|e'], ]); }); it('should extract from elements', () => { expect( extract( '
nested
')) .toEqual([ [ ['nested'], 'm', 'd', ], [ ['title="single child"'], 'm', 'd', ], ]); }); it('should extract from elements', () => { expect( extract( '
{count, plural, =0 {

}}
')) .toEqual([ [ [ '{count, plural, =0 {

}}' ], 'm', 'd' ], [['title="title"'], '', ''], [['desc="desc"'], '', ''], ]); }); it('should not create a message for empty elements', () => { expect(extract('
')).toEqual([]); }); }); describe('blocks', () => { it('should extract from blocks', () => { expect(extract(`message1 message2 message3`)) .toEqual([ [['message1'], 'meaning1', 'desc1'], [['message2'], '', 'desc2'], [['message3'], '', ''], ]); }); it('should extract siblings', () => { expect( extract( `text

htmlnested

{count, plural, =0 {html}}{{interp}}`)) .toEqual([ [['{count, plural, =0 {html}}'], '', ''], [ [ 'text', '

htmlnested

', '{count, plural, =0 {html}}', '{{interp}}' ], '', '' ], ]); }); it('should ignore other comments', () => { expect(extract(`message1`)) .toEqual([ [['message1'], 'meaning1', 'desc1'], ]); }); it('should not create a message for empty blocks', () => { expect(extract(``)).toEqual([]); }); }); describe('ICU messages', () => { it('should extract ICU messages from translatable elements', () => { // single message when ICU is the only children expect(extract('
{count, plural, =0 {text}}
')).toEqual([ [['{count, plural, =0 {text}}'], 'm', 'd'], ]); // single message when ICU is the only (implicit) children expect(extract('
{count, plural, =0 {text}}
', ['div'])).toEqual([ [['{count, plural, =0 {text}}'], '', ''], ]); // one message for the element content and one message for the ICU expect(extract('
before{count, plural, =0 {text}}after
')).toEqual([ [['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'], [['{count, plural, =0 {text}}'], '', ''], ]); }); it('should extract ICU messages from translatable block', () => { // single message when ICU is the only children expect(extract('{count, plural, =0 {text}}')).toEqual([ [['{count, plural, =0 {text}}'], 'm', 'd'], ]); // one message for the block content and one message for the ICU expect(extract('before{count, plural, =0 {text}}after')) .toEqual([ [['{count, plural, =0 {text}}'], '', ''], [['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'], ]); }); it('should not extract ICU messages outside of i18n sections', () => { expect(extract('{count, plural, =0 {text}}')).toEqual([]); }); it('should not extract nested ICU messages', () => { expect(extract('
{count, plural, =0 { {sex, gender, =m {m}} }}
')) .toEqual([ [['{count, plural, =0 {{sex, gender, =m {m}} }}'], 'm', 'd'], ]); }); }); describe('attributes', () => { it('should extract from attributes outside of translatable sections', () => { expect(extract('
')).toEqual([ [['title="msg"'], 'm', 'd'], ]); }); it('should extract from attributes in translatable elements', () => { expect(extract('

')).toEqual([ [['

'], '', ''], [['title="msg"'], 'm', 'd'], ]); }); it('should extract from attributes in translatable blocks', () => { expect(extract('

')) .toEqual([ [['title="msg"'], 'm', 'd'], [['

'], '', ''], ]); }); it('should extract from attributes in translatable ICUs', () => { expect( extract( '{count, plural, =0 {

}}')) .toEqual([ [['title="msg"'], 'm', 'd'], [['{count, plural, =0 {

}}'], '', ''], ]); }); it('should extract from attributes in non translatable ICUs', () => { expect(extract('{count, plural, =0 {

}}')) .toEqual([ [['title="msg"'], 'm', 'd'], ]); }); it('should not create a message for empty attributes', () => { expect(extract('
')).toEqual([]); }); }); describe('implicit elements', () => { it('should extract from implicit elements', () => { expect(extract('bolditalic', ['b'])).toEqual([ [['bold'], '', ''], ]); }); }); describe('implicit attributes', () => { it('should extract implicit attributes', () => { expect(extract('bolditalic', [], {'b': ['title']})) .toEqual([ [['title="bb"'], '', ''], ]); }); }); describe('errors', () => { describe('elements', () => { it('should report nested translatable elements', () => { expect(extractErrors(`

`)).toEqual([ ['Could not mark an element as translatable inside a translatable section', ''], ]); }); it('should report translatable elements in implicit elements', () => { expect(extractErrors(`

`, ['p'])).toEqual([ ['Could not mark an element as translatable inside a translatable section', ''], ]); }); it('should report translatable elements in translatable blocks', () => { expect(extractErrors(``)).toEqual([ ['Could not mark an element as translatable inside a translatable section', ''], ]); }); }); describe('blocks', () => { it('should report nested blocks', () => { expect(extractErrors(``)).toEqual([ ['Could not start a block inside a translatable section', '`)).toEqual([ ['Unclosed block', '

`)).toEqual([ ['Could not start a block inside a translatable section', '

`, ['p'])).toEqual([ ['Could not start a block inside a translatable section', '

`)).toEqual([ ['I18N blocks should not cross element boundaries', '

`)).toEqual([ ['I18N blocks should not cross element boundaries', '`, ['b'])).toEqual([ ['Could not mark an element as translatable inside a translatable section', ''], ]); }); }); }); }); describe('Merger', () => { describe('elements', () => { it('should merge elements', () => { const HTML = `

foo

`; expect(fakeTranslate(HTML)).toEqual('

-*foo*-

'); }); it('should merge nested elements', () => { const HTML = `
before

foo

`; expect(fakeTranslate(HTML)).toEqual('
before

-*foo*-

'); }); }); describe('blocks', () => { it('should merge blocks', () => { const HTML = `before

foo

barafter`; expect(fakeTranslate(HTML)).toEqual('before-*

foo

bar*-after'); }); it('should merge nested blocks', () => { const HTML = `
before

foo

barafter
`; expect(fakeTranslate(HTML)) .toEqual('
before-*

foo

bar*-after
'); }); }); describe('attributes', () => { it('should merge attributes', () => { const HTML = `

`; expect(fakeTranslate(HTML)).toEqual('

'); }); it('should merge attributes', () => { const HTML = `
{count, plural, =0 {

}}
`; expect(fakeTranslate(HTML)).toEqual('
{count, plural, =0 {

}}
'); }); }); }); } function parseHtml(html: string): html.Node[] { const htmlParser = new HtmlParser(); const parseResult = htmlParser.parse(html, 'extractor spec', true); if (parseResult.errors.length > 1) { throw Error(`unexpected parse errors: ${parseResult.errors.join('\n')}`); } return parseResult.rootNodes; } function fakeTranslate( content: string, implicitTags: string[] = [], implicitAttrs: {[k: string]: string[]} = {}): string { const htmlNodes: html.Node[] = parseHtml(content); const htmlMsgs: HtmlMessage[] = extractAstMessages(htmlNodes, implicitTags, implicitAttrs).messages; const i18nMsgMap: {[id: string]: html.Node[]} = {}; const converter = getHtmlToI18nConverter(DEFAULT_INTERPOLATION_CONFIG); htmlMsgs.forEach(msg => { const i18nMsg = converter(msg); i18nMsgMap[digestMessage(i18nMsg.nodes, i18nMsg.meaning)] = [ new html.Text('-*', null), ...msg.nodes, new html.Text('*-', null), ]; }); const translations = new TranslationBundle(i18nMsgMap); const translateNodes = mergeTranslations( htmlNodes, translations, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs); return serializeNodes(translateNodes).join(''); } function extract( html: string, implicitTags: string[] = [], implicitAttrs: {[k: string]: string[]} = {}): [string[], string, string][] { const messages = extractAstMessages(parseHtml(html), implicitTags, implicitAttrs).messages; // clang-format off // https://github.com/angular/clang-format/issues/35 return messages.map( message => [serializeNodes(message.nodes), message.meaning, message.description, ]) as [string[], string, string][]; // clang-format on } function extractErrors( html: string, implicitTags: string[] = [], implicitAttrs: {[k: string]: string[]} = {}): any[] { const errors = extractAstMessages(parseHtml(html), implicitTags, implicitAttrs).errors; return errors.map((e): [string, string] => [e.msg, e.span.toString()]); }