From 992c70df590151522bfeee7567a89ac9c4a108f1 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 4 May 2021 15:28:41 +0100 Subject: [PATCH] fix(localize): relax error to warning for missing target (#41944) Some localization workflows want to use the extracted source translation files directy back in the project as a target translation file. The extraction process generates files that only contain "source" messages and not "target" messages. This is actually valid for most translation formats but currently the Angular localization process expects target translation files to always contain target messages and will stop with an error in this case. Now, instead of an error, the translation file loader will log a warning, and then try to falback to a source message, only erroring if this is also missing. Fixes #21690 PR Close #41944 --- .../xliff1_translation_parser.ts | 20 +- .../xliff2_translation_parser.ts | 19 +- .../xliff1_translation_parser_spec.ts | 1522 +++++++++-------- .../xliff2_translation_parser_spec.ts | 1367 +++++++-------- 4 files changed, 1505 insertions(+), 1423 deletions(-) diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts index dacc24e935572..2dd951ffe4c09 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts @@ -140,13 +140,23 @@ class XliffTranslationVisitor extends BaseVisitor { return; } - // Error if there is no `` child element - const targetMessage = element.children.find(isNamedElement('target')); + let targetMessage = element.children.find(isNamedElement('target')); if (targetMessage === undefined) { + // Warn if there is no `` child element addParseDiagnostic( - bundle.diagnostics, element.sourceSpan, 'Missing required element', - ParseErrorLevel.ERROR); - return; + bundle.diagnostics, element.sourceSpan, 'Missing element', + ParseErrorLevel.WARNING); + + // Fallback to the `` element if available. + targetMessage = element.children.find(isNamedElement('source')); + if (targetMessage === undefined) { + // Error if there is neither `` nor ``. + addParseDiagnostic( + bundle.diagnostics, element.sourceSpan, + 'Missing required element: one of or is required', + ParseErrorLevel.ERROR); + return; + } } const {translation, parseErrors, serializeErrors} = serializeTranslationMessage(targetMessage, { diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts index 2f5edf03efa7e..b47d9c1ff8f44 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts @@ -132,12 +132,23 @@ class Xliff2TranslationVisitor extends BaseVisitor { return; } - const targetMessage = element.children.find(isNamedElement('target')); + let targetMessage = element.children.find(isNamedElement('target')); if (targetMessage === undefined) { + // Warn if there is no `` child element addParseDiagnostic( - bundle.diagnostics, element.sourceSpan, 'Missing required element', - ParseErrorLevel.ERROR); - return; + bundle.diagnostics, element.sourceSpan, 'Missing element', + ParseErrorLevel.WARNING); + + // Fallback to the `` element if available. + targetMessage = element.children.find(isNamedElement('source')); + if (targetMessage === undefined) { + // Error if there is neither `` nor ``. + addParseDiagnostic( + bundle.diagnostics, element.sourceSpan, + 'Missing required element: one of or is required', + ParseErrorLevel.ERROR); + return; + } } const {translation, parseErrors, serializeErrors} = serializeTranslationMessage(targetMessage, { diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts index 9f475e54e8f69..4eea42a389cb5 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts @@ -9,754 +9,790 @@ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; import {ParseAnalysis, ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; import {Xliff1TranslationParser} from '../../../../src/translate/translation_files/translation_parsers/xliff1_translation_parser'; -describe('Xliff1TranslationParser', () => { - describe('canParse()', () => { - it('should return true only if the file contains an element with version="1.2" attribute', - () => { - const parser = new Xliff1TranslationParser(); - expect(parser.canParse( - '/some/file.xlf', - '')) - .toBeTruthy(); - expect(parser.canParse( - '/some/file.json', - '')) - .toBeTruthy(); - expect(parser.canParse('/some/file.xliff', '')).toBeTruthy(); - expect(parser.canParse('/some/file.json', '')).toBeTruthy(); - expect(parser.canParse('/some/file.xlf', '')).toBe(false); - expect(parser.canParse('/some/file.xlf', '')).toBe(false); - expect(parser.canParse('/some/file.xlf', '')).toBe(false); - expect(parser.canParse('/some/file.json', '')).toBe(false); - }); - }); - - describe('analyze()', () => { - it('should return a success object if the file contains an element with version="1.2" attribute', - () => { - const parser = new Xliff1TranslationParser(); - expect(parser.analyze('/some/file.xlf', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - expect(parser.analyze('/some/file.json', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - expect(parser.analyze('/some/file.xliff', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - expect(parser.analyze('/some/file.json', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - }); - - it('should return a failure object if the file cannot be parsed as XLIFF 1.2', () => { - const parser = new Xliff1TranslationParser(); - expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ - canParse: false - })); - expect(parser.analyze('/some/file.xlf', '')) - .toEqual(jasmine.objectContaining({canParse: false})); - expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ - canParse: false - })); - expect(parser.analyze('/some/file.json', '')).toEqual(jasmine.objectContaining({ - canParse: false - })); - }); - - it('should return a diagnostics object when the file is not a valid format', () => { - let result: ParseAnalysis; - const parser = new Xliff1TranslationParser(); - - result = parser.analyze('/some/file.xlf', ''); - expect(result.diagnostics.messages).toEqual([ - {type: 'warning', message: 'The XML file does not contain a root node.'} - ]); - - result = parser.analyze('/some/file.xlf', ''); - expect(result.diagnostics.messages).toEqual([{ - type: 'warning', - message: - 'The node does not have the required attribute: version="1.2". ("[WARNING ->]"): /some/file.xlf@0:0' - }]); - - result = parser.analyze('/some/file.xlf', ''); - expect(result.diagnostics.messages).toEqual([{ - type: 'error', - message: - 'Unexpected closing tag "file". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]"): /some/file.xlf@0:21' - }]); - }); - }); - - for (const withHint of [true, false]) { - describe(`parse() [${withHint ? 'with' : 'without'} hint]`, () => { - const doParse: (fileName: string, XLIFF: string) => ParsedTranslationBundle = - withHint ? (fileName, XLIFF) => { - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse(fileName, XLIFF); - if (!hint) { - throw new Error('expected XLIFF to be valid'); - } - return parser.parse(fileName, XLIFF, hint); - } : (fileName, XLIFF) => { - const parser = new Xliff1TranslationParser(); - return parser.parse(fileName, XLIFF); - }; - - const expectToFail: - (fileName: string, XLIFF: string, errorMatcher: RegExp, diagnosticMessage: string) => - void = withHint ? (fileName, XLIFF, _errorMatcher, diagnosticMessage) => { - const result = doParse(fileName, XLIFF); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message).toEqual(diagnosticMessage); - } : (fileName, XLIFF, errorMatcher, _diagnosticMessage) => { - expect(() => doParse(fileName, XLIFF)).toThrowError(errorMatcher); - }; - - it('should extract the locale from the last `` element to contain a `target-language` attribute', - () => { - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.locale).toEqual('de'); - }); - - it('should return an undefined locale if there is no locale in the file', () => { - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.locale).toBeUndefined(); - }); - - it('should extract basic messages', () => { - /** - * Source HTML: - * - * ``` - *
translatable attribute
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` translatable attribute`, - ` etubirtta elbatalsnart`, - ` `, - ` file.ts`, - ` 1`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect(result.translations[ɵcomputeMsgId('translatable attribute')]) - .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); - }); - - it('should extract translations with simple placeholders', () => { - /** - * Source HTML: - * - * ``` - *
translatable element with placeholders {{ interpolation}}
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` translatable element with placeholders `, - ` tnemele elbatalsnart sredlohecalp htiw`, - ` `, - ` file.ts`, - ` 2`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect( - result.translations[ɵcomputeMsgId( - 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) - .toEqual(ɵmakeParsedTranslation( - ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], - ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); - }); - - it('should extract nested placeholder containers (i.e. nested HTML elements)', () => { - /** - * Source HTML: - * - * ``` - *
- * translatable element with placeholders {{ interpolation}} - *
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` translatable element with placeholders `, - ` tnemele elbatalsnart sredlohecalp htiw`, - ` `, - ` file.ts`, - ` 3`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations[ɵcomputeMsgId( - 'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' + - '{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')]) - .toEqual(ɵmakeParsedTranslation( - ['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [ - 'START_TAG_SPAN', - 'INTERPOLATION', - 'CLOSE_TAG_SPAN', - 'START_BOLD_TEXT', - 'CLOSE_BOLD_TEXT', - ])); - }); - - it('should extract translations with placeholders containing hyphens', () => { - /** - * Source HTML: - * - * ``` - *
Welcome
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` Welcome`, - ` `, - ` src/app/app.component.html`, - ` 1`, - ` `, - ` Translate`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - const id = - ɵcomputeMsgId('{$START_TAG_APP_MY_COMPONENT}{$CLOSE_TAG_APP_MY_COMPONENT} Welcome'); - expect(result.translations[id]).toEqual(ɵmakeParsedTranslation(['', '', ' Translate'], [ - 'START_TAG_APP_MY_COMPONENT', 'CLOSE_TAG_APP_MY_COMPONENT' - ])); - }); - - it('should extract translations with simple ICU expressions', () => { - /** - * Source HTML: - * - * ``` - *
{VAR_PLURAL, plural, =0 {

test

} }
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` {VAR_PLURAL, plural, =0 {test} }`, - ` {VAR_PLURAL, plural, =0 {TEST} }`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect(result.translations[ɵcomputeMsgId( - '{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}test{CLOSE_PARAGRAPH}}}')]) - .toEqual(ɵmakeParsedTranslation( - ['{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}TEST{CLOSE_PARAGRAPH}}}'], [])); - }); - - it('should extract translations with duplicate source messages', () => { - /** - * Source HTML: - * - * ``` - *
foo
- *
foo
- *
foo
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` foo`, - ` oof`, - ` `, - ` file.ts`, - ` 3`, - ` `, - ` d`, - ` m`, - ` `, - ` `, - ` foo`, - ` toto`, - ` `, - ` file.ts`, - ` 4`, - ` `, - ` d`, - ` m`, - ` `, - ` `, - ` foo`, - ` tata`, - ` `, - ` file.ts`, - ` 5`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect(result.translations[ɵcomputeMsgId('foo')]).toEqual(ɵmakeParsedTranslation(['oof'])); - expect(result.translations['i']).toEqual(ɵmakeParsedTranslation(['toto'])); - expect(result.translations['bar']).toEqual(ɵmakeParsedTranslation(['tata'])); - }); - - it('should extract translations with only placeholders, which are re-ordered', () => { - /** - * Source HTML: - * - * ``` - *

- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` file.ts`, - ` 6`, - ` `, - ` ph names`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')]) - .toEqual( - ɵmakeParsedTranslation(['', '', '', ''], ['TAG_IMG_1', 'TAG_IMG', 'LINE_BREAK'])); - }); - - it('should extract translations with empty target', () => { - /** - * Source HTML: - * - * ``` - *
hello
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` hello `, - ` `, - ` `, - ` file.ts`, - ` 6`, - ` `, - ` ph names`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect(result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')]) - .toEqual(ɵmakeParsedTranslation([''])); - }); - - it('should extract translations with deeply nested ICUs', () => { - /** - * Source HTML: - * - * ``` - * Test: { count, plural, =0 { { sex, select, other {

deeply nested

}} } =other {a - * lot}} - * ``` - * - * Note that the message gets split into two translation units: - * * The first one contains the outer message with an `ICU` placeholder - * * The second one is the ICU expansion itself - * - * Note that special markers `VAR_PLURAL` and `VAR_SELECT` are added, which are then - * replaced by IVY at runtime with the actual values being rendered by the ICU expansion. - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` Test: `, - ` Le test: `, - ` `, - ` file.ts`, - ` 11`, - ` `, - ` `, - ` `, - ` {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {deeply nested}}} =other {a lot}}`, - ` {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {profondément imbriqué}}} =other {beaucoup}}`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect(result.translations[ɵcomputeMsgId('Test: {$ICU}')]) - .toEqual(ɵmakeParsedTranslation(['Le test: ', ''], ['ICU'])); - - expect( - result.translations[ɵcomputeMsgId( - '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}deeply nested{CLOSE_PARAGRAPH}}}} =other {beaucoup}}')]) - .toEqual(ɵmakeParsedTranslation([ - '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}profondément imbriqué{CLOSE_PARAGRAPH}}}} =other {beaucoup}}' - ])); - }); - - it('should extract translations containing multiple lines', () => { - /** - * Source HTML: - * - * ``` - *
multi - * lines
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` multi\nlines`, - ` multi\nlignes`, - ` `, - ` file.ts`, - ` 12`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect(result.translations[ɵcomputeMsgId('multi\nlines')]) - .toEqual(ɵmakeParsedTranslation(['multi\nlignes'])); - }); - - it('should extract translations with elements', () => { - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` First sentence.`, - ` `, - ` Should not be parsed`, - ` `, - ` Translated first sentence.`, - ` `, - ` `, - ` First sentence. Second sentence.`, - ` `, - ` Should not be parsed`, - ` `, - ` Translated first sentence.`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect(result.translations['mrk-test']) - .toEqual(ɵmakeParsedTranslation(['Translated first sentence.'])); - - expect(result.translations['mrk-test2']) - .toEqual(ɵmakeParsedTranslation(['Translated first sentence.'])); - }); - - it('should ignore alt-trans targets', () => { - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` Continue`, - ` Weiter`, - ` `, - ` src/app/auth/registration-form/registration-form.component.html`, - ` 69`, - ` `, - ` `, - ` `, - ` Content`, - ` Content`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations['registration.submit']) - .toEqual(ɵmakeParsedTranslation(['Weiter'])); +describe( + 'Xliff1TranslationParser', () => { + describe('canParse()', () => { + it('should return true only if the file contains an element with version="1.2" attribute', + () => { + const parser = new Xliff1TranslationParser(); + expect(parser.canParse( + '/some/file.xlf', + '')) + .toBeTruthy(); + expect(parser.canParse( + '/some/file.json', + '')) + .toBeTruthy(); + expect(parser.canParse('/some/file.xliff', '')).toBeTruthy(); + expect(parser.canParse('/some/file.json', '')).toBeTruthy(); + expect(parser.canParse('/some/file.xlf', '')).toBe(false); + expect(parser.canParse('/some/file.xlf', '')).toBe(false); + expect(parser.canParse('/some/file.xlf', '')).toBe(false); + expect(parser.canParse('/some/file.json', '')).toBe(false); + }); }); - it('should merge messages from each `` element', () => { - /** - * Source HTML: - * - * ``` - *
translatable attribute
- * ``` - - * ``` - *
translatable element with placeholders {{ interpolation}}
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` translatable attribute`, - ` etubirtta elbatalsnart`, - ` `, - ` file.ts`, - ` 1`, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` translatable element with placeholders `, - ` tnemele elbatalsnart sredlohecalp htiw`, - ` `, - ` file.ts`, - ` 2`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations[ɵcomputeMsgId('translatable attribute')]) - .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); - expect( - result.translations[ɵcomputeMsgId( - 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) - .toEqual(ɵmakeParsedTranslation( - ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], - ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); - }); - - describe('[structure errors]', () => { - it('should fail when a trans-unit has no translation', () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - expectToFail('/some/file.xlf', XLIFF, /Missing required element/, [ - `Missing required element ("e-language="en" target-language="fr" datatype="plaintext" original="ng2.template">`, - ` `, - ` [ERROR ->]`, - ` `, - ` `, - `"): /some/file.xlf@4:6`, - ].join('\n')); - }); - - - it('should fail when a trans-unit has no id attribute', () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - expectToFail('/some/file.xlf', XLIFF, /Missing required "id" attribute/, [ - `Missing required "id" attribute on element. ("e-language="en" target-language="fr" datatype="plaintext" original="ng2.template">`, - ` `, - ` [ERROR ->]`, - ` `, - ` `, - `"): /some/file.xlf@4:6`, - ].join('\n')); + describe('analyze()', () => { + it('should return a success object if the file contains an element with version="1.2" attribute', + () => { + const parser = new Xliff1TranslationParser(); + expect(parser.analyze('/some/file.xlf', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.json', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.xliff', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.json', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + }); + + it('should return a failure object if the file cannot be parsed as XLIFF 1.2', () => { + const parser = new Xliff1TranslationParser(); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.xlf', '')) + .toEqual(jasmine.objectContaining({canParse: false})); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.json', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); }); - it('should fail on duplicate trans-unit id', () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - expectToFail('/some/file.xlf', XLIFF, /Duplicated translations for message "deadbeef"/, [ - `Duplicated translations for message "deadbeef" ("`, - ` `, - ` `, - ` [ERROR ->]`, - ` `, - ` `, - `"): /some/file.xlf@8:6`, - ].join('\n')); + it('should return a diagnostics object when the file is not a valid format', () => { + let result: ParseAnalysis; + const parser = new Xliff1TranslationParser(); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([ + {type: 'warning', message: 'The XML file does not contain a root node.'} + ]); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([{ + type: 'warning', + message: + 'The node does not have the required attribute: version="1.2". ("[WARNING ->]"): /some/file.xlf@0:0' + }]); + + result = parser.analyze('/some/file.xlf', '
'); + expect(result.diagnostics.messages).toEqual([{ + type: 'error', + message: + 'Unexpected closing tag "file". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]
"): /some/file.xlf@0:21' + }]); }); }); - describe('[message errors]', () => { - it('should fail on unknown message tags', () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` msg should contain only ph tags`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - expectToFail('/some/file.xlf', XLIFF, /Invalid element found in message/, [ - `Error: Invalid element found in message.`, - `At /some/file.xlf@6:16:`, - `...`, - ` `, - ` [ERROR ->]msg should contain only ph tags`, - ` `, - `...`, - ``, - ].join('\n')); - }); - - it('should fail when a placeholder misses an id attribute', () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - expectToFail('/some/file.xlf', XLIFF, /required "id" attribute/gi, [ - `Error: Missing required "id" attribute:`, - `At /some/file.xlf@6:16:`, - `...`, - ` `, - ` [ERROR ->]`, - ` `, - `...`, - ``, - ].join('\n')); - }); - }); + for (const withHint of [true, false]) { + describe( + `parse() [${withHint ? 'with' : 'without'} hint]`, () => { + const doParse: (fileName: string, XLIFF: string) => ParsedTranslationBundle = + withHint ? (fileName, XLIFF) => { + const parser = new Xliff1TranslationParser(); + const hint = parser.canParse(fileName, XLIFF); + if (!hint) { + throw new Error('expected XLIFF to be valid'); + } + return parser.parse(fileName, XLIFF, hint); + } : (fileName, XLIFF) => { + const parser = new Xliff1TranslationParser(); + return parser.parse(fileName, XLIFF); + }; + + const expectToFail: ( + fileName: string, XLIFF: string, errorMatcher: RegExp, + diagnosticMessage: string) => void = + withHint ? (fileName, XLIFF, _errorMatcher, diagnosticMessage) => { + const result = doParse(fileName, XLIFF); + expect(result.diagnostics.messages.length).toBeGreaterThan(0); + expect(result.diagnostics.messages.pop()!.message).toEqual(diagnosticMessage); + } : (fileName, XLIFF, errorMatcher, _diagnosticMessage) => { + expect(() => doParse(fileName, XLIFF)).toThrowError(errorMatcher); + }; + + it('should extract the locale from the last `` element to contain a `target-language` attribute', + () => { + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.locale).toEqual('de'); + }); + + it('should return an undefined locale if there is no locale in the file', () => { + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.locale).toBeUndefined(); + }); + + it('should extract basic messages', () => { + /** + * Source HTML: + * + * ``` + *
translatable attribute
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` translatable attribute`, + ` etubirtta elbatalsnart`, + ` `, + ` file.ts`, + ` 1`, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + + expect(result.translations[ɵcomputeMsgId('translatable attribute')]) + .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); + }); + + it('should extract translations with simple placeholders', () => { + /** + * Source HTML: + * + * ``` + *
translatable element with placeholders {{ interpolation}}
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` translatable element with placeholders `, + ` tnemele elbatalsnart sredlohecalp htiw`, + ` `, + ` file.ts`, + ` 2`, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + + expect( + result.translations[ɵcomputeMsgId( + 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], + ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); + }); + + it('should extract nested placeholder containers (i.e. nested HTML elements)', () => { + /** + * Source HTML: + * + * ``` + *
+ * translatable element with placeholders {{ interpolation}} + *
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` translatable element with placeholders `, + ` tnemele elbatalsnart sredlohecalp htiw`, + ` `, + ` file.ts`, + ` 3`, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect( + result.translations[ɵcomputeMsgId( + 'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' + + '{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [ + 'START_TAG_SPAN', + 'INTERPOLATION', + 'CLOSE_TAG_SPAN', + 'START_BOLD_TEXT', + 'CLOSE_BOLD_TEXT', + ])); + }); + + it('should extract translations with placeholders containing hyphens', () => { + /** + * Source HTML: + * + * ``` + *
Welcome
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` Welcome`, + ` `, + ` src/app/app.component.html`, + ` 1`, + ` `, + ` Translate`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + const id = ɵcomputeMsgId( + '{$START_TAG_APP_MY_COMPONENT}{$CLOSE_TAG_APP_MY_COMPONENT} Welcome'); + expect(result.translations[id]) + .toEqual(ɵmakeParsedTranslation( + ['', '', ' Translate'], + ['START_TAG_APP_MY_COMPONENT', 'CLOSE_TAG_APP_MY_COMPONENT'])); + }); + + it('should extract translations with simple ICU expressions', () => { + /** + * Source HTML: + * + * ``` + *
{VAR_PLURAL, plural, =0 {

test

} }
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` {VAR_PLURAL, plural, =0 {test} }`, + ` {VAR_PLURAL, plural, =0 {TEST} }`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + + expect(result.translations[ɵcomputeMsgId( + '{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}test{CLOSE_PARAGRAPH}}}')]) + .toEqual(ɵmakeParsedTranslation( + ['{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}TEST{CLOSE_PARAGRAPH}}}'], [])); + }); + + it('should extract translations with duplicate source messages', () => { + /** + * Source HTML: + * + * ``` + *
foo
+ *
foo
+ *
foo
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` foo`, + ` oof`, + ` `, + ` file.ts`, + ` 3`, + ` `, + ` d`, + ` m`, + ` `, + ` `, + ` foo`, + ` toto`, + ` `, + ` file.ts`, + ` 4`, + ` `, + ` d`, + ` m`, + ` `, + ` `, + ` foo`, + ` tata`, + ` `, + ` file.ts`, + ` 5`, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + + expect(result.translations[ɵcomputeMsgId('foo')]) + .toEqual(ɵmakeParsedTranslation(['oof'])); + expect(result.translations['i']).toEqual(ɵmakeParsedTranslation(['toto'])); + expect(result.translations['bar']).toEqual(ɵmakeParsedTranslation(['tata'])); + }); + + it('should extract translations with only placeholders, which are re-ordered', () => { + /** + * Source HTML: + * + * ``` + *

+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` file.ts`, + ` 6`, + ` `, + ` ph names`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + + expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')]) + .toEqual(ɵmakeParsedTranslation( + ['', '', '', ''], ['TAG_IMG_1', 'TAG_IMG', 'LINE_BREAK'])); + }); + + it('should extract translations with empty target', () => { + /** + * Source HTML: + * + * ``` + *
hello
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` hello `, + ` `, + ` `, + ` file.ts`, + ` 6`, + ` `, + ` ph names`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + + expect( + result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')]) + .toEqual(ɵmakeParsedTranslation([''])); + }); + + it('should extract translations with deeply nested ICUs', () => { + /** + * Source HTML: + * + * ``` + * Test: { count, plural, =0 { { sex, select, other {

deeply nested

}} } + * =other {a lot}} + * ``` + * + * Note that the message gets split into two translation units: + * * The first one contains the outer message with an `ICU` placeholder + * * The second one is the ICU expansion itself + * + * Note that special markers `VAR_PLURAL` and `VAR_SELECT` are added, which are then + * replaced by IVY at runtime with the actual values being rendered by the ICU + * expansion. + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` Test: `, + ` Le test: `, + ` `, + ` file.ts`, + ` 11`, + ` `, + ` `, + ` `, + ` {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {deeply nested}}} =other {a lot}}`, + ` {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {profondément imbriqué}}} =other {beaucoup}}`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + + expect(result.translations[ɵcomputeMsgId('Test: {$ICU}')]) + .toEqual(ɵmakeParsedTranslation(['Le test: ', ''], ['ICU'])); + + expect( + result.translations[ɵcomputeMsgId( + '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}deeply nested{CLOSE_PARAGRAPH}}}} =other {beaucoup}}')]) + .toEqual(ɵmakeParsedTranslation([ + '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}profondément imbriqué{CLOSE_PARAGRAPH}}}} =other {beaucoup}}' + ])); + }); + + it('should extract translations containing multiple lines', () => { + /** + * Source HTML: + * + * ``` + *
multi + * lines
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` multi\nlines`, + ` multi\nlignes`, + ` `, + ` file.ts`, + ` 12`, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + + expect(result.translations[ɵcomputeMsgId('multi\nlines')]) + .toEqual(ɵmakeParsedTranslation(['multi\nlignes'])); + }); + + it('should extract translations with elements', () => { + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` First sentence.`, + ` `, + ` Should not be parsed`, + ` `, + ` Translated first sentence.`, + ` `, + ` `, + ` First sentence. Second sentence.`, + ` `, + ` Should not be parsed`, + ` `, + ` Translated first sentence.`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + + expect(result.translations['mrk-test']) + .toEqual(ɵmakeParsedTranslation(['Translated first sentence.'])); + + expect(result.translations['mrk-test2']) + .toEqual(ɵmakeParsedTranslation(['Translated first sentence.'])); + }); + + it('should ignore alt-trans targets', () => { + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` Continue`, + ` Weiter`, + ` `, + ` src/app/auth/registration-form/registration-form.component.html`, + ` 69`, + ` `, + ` `, + ` `, + ` Content`, + ` Content`, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations['registration.submit']) + .toEqual(ɵmakeParsedTranslation(['Weiter'])); + }); + + it('should merge messages from each `` element', () => { + /** + * Source HTML: + * + * ``` + *
translatable attribute
+ * ``` + + * ``` + *
translatable element with placeholders {{ interpolation}}
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` translatable attribute`, + ` etubirtta elbatalsnart`, + ` `, + ` file.ts`, + ` 1`, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` translatable element with placeholders `, + ` tnemele elbatalsnart sredlohecalp htiw`, + ` `, + ` file.ts`, + ` 2`, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('translatable attribute')]) + .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); + expect( + result.translations[ɵcomputeMsgId( + 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], + ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); + }); + + describe('[structure errors]', () => { + it('should warn when a trans-unit has no translation target but does have a source', + () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + const result = doParse('/some/file.xlf', XLIFF); + expect(result.diagnostics.messages.length).toEqual(1); + expect(result.diagnostics.messages[0].message).toEqual([ + `Missing element ("e-language="en" target-language="fr" datatype="plaintext" original="ng2.template">`, + ` `, + ` [WARNING ->]`, + ` `, + ` `, + `"): /some/file.xlf@4:6`, + ].join('\n')); + }); + + it('should fail when a trans-unit has no translation target nor source', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail( + '/some/file.xlf', XLIFF, + /Missing required element: one of or is required/, [ + `Missing required element: one of or is required ("e-language="en" target-language="fr" datatype="plaintext" original="ng2.template">`, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + `"): /some/file.xlf@4:6`, + ].join('\n')); + }); + + it('should fail when a trans-unit has no id attribute', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Missing required "id" attribute/, [ + `Missing required "id" attribute on element. ("e-language="en" target-language="fr" datatype="plaintext" original="ng2.template">`, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + `"): /some/file.xlf@4:6`, + ].join('\n')); + }); + + it('should fail on duplicate trans-unit id', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail( + '/some/file.xlf', XLIFF, /Duplicated translations for message "deadbeef"/, [ + `Duplicated translations for message "deadbeef" ("`, + ` `, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + `"): /some/file.xlf@8:6`, + ].join('\n')); + }); + }); + + describe('[message errors]', () => { + it('should fail on unknown message tags', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` msg should contain only ph tags`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Invalid element found in message/, [ + `Error: Invalid element found in message.`, + `At /some/file.xlf@6:16:`, + `...`, + ` `, + ` [ERROR ->]msg should contain only ph tags`, + ` `, + `...`, + ``, + ].join('\n')); + }); + + it('should fail when a placeholder misses an id attribute', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /required "id" attribute/gi, [ + `Error: Missing required "id" attribute:`, + `At /some/file.xlf@6:16:`, + `...`, + ` `, + ` [ERROR ->]`, + ` `, + `...`, + ``, + ].join('\n')); + }); + }); + }); + } }); - } -}); diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts index d9b412c8bc486..bb9000d09bd89 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts @@ -9,686 +9,711 @@ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; import {ParseAnalysis, ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; import {Xliff2TranslationParser} from '../../../../src/translate/translation_files/translation_parsers/xliff2_translation_parser'; -describe( - 'Xliff2TranslationParser', () => { - describe('canParse()', () => { - it('should return true if the file contains an element with version="2.0" attribute', - () => { - const parser = new Xliff2TranslationParser(); - expect(parser.canParse( - '/some/file.xlf', - '')) - .toBeTruthy(); - expect(parser.canParse( - '/some/file.json', - '')) - .toBeTruthy(); - expect(parser.canParse('/some/file.xliff', '')).toBeTruthy(); - expect(parser.canParse('/some/file.json', '')).toBeTruthy(); - expect(parser.canParse('/some/file.xlf', '')).toBe(false); - expect(parser.canParse('/some/file.xlf', '')).toBe(false); - expect(parser.canParse('/some/file.xlf', '')).toBe(false); - expect(parser.canParse('/some/file.json', '')).toBe(false); - }); - }); - - describe('analyze', () => { - it('should return a success object if the file contains an element with version="2.0" attribute', - () => { - const parser = new Xliff2TranslationParser(); - expect(parser.analyze( - '/some/file.xlf', - '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - expect(parser.analyze( - '/some/file.json', - '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - expect(parser.analyze('/some/file.xliff', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - expect(parser.analyze('/some/file.json', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - }); - - it('should return a failure object if the file cannot be parsed as XLIFF 2.0', () => { - const parser = new Xliff2TranslationParser(); - expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ - canParse: false - })); - expect(parser.analyze('/some/file.xlf', '')) - .toEqual(jasmine.objectContaining({canParse: false})); - expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ - canParse: false - })); - expect(parser.analyze('/some/file.json', '')).toEqual(jasmine.objectContaining({ - canParse: false - })); - }); +describe('Xliff2TranslationParser', () => { + describe('canParse()', () => { + it('should return true if the file contains an element with version="2.0" attribute', + () => { + const parser = new Xliff2TranslationParser(); + expect(parser.canParse( + '/some/file.xlf', + '')) + .toBeTruthy(); + expect(parser.canParse( + '/some/file.json', + '')) + .toBeTruthy(); + expect(parser.canParse('/some/file.xliff', '')).toBeTruthy(); + expect(parser.canParse('/some/file.json', '')).toBeTruthy(); + expect(parser.canParse('/some/file.xlf', '')).toBe(false); + expect(parser.canParse('/some/file.xlf', '')).toBe(false); + expect(parser.canParse('/some/file.xlf', '')).toBe(false); + expect(parser.canParse('/some/file.json', '')).toBe(false); + }); + }); + + describe('analyze', () => { + it('should return a success object if the file contains an element with version="2.0" attribute', + () => { + const parser = new Xliff2TranslationParser(); + expect(parser.analyze( + '/some/file.xlf', + '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze( + '/some/file.json', + '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.xliff', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.json', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + }); + + it('should return a failure object if the file cannot be parsed as XLIFF 2.0', () => { + const parser = new Xliff2TranslationParser(); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.xlf', '')) + .toEqual(jasmine.objectContaining({canParse: false})); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.json', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + }); - it('should return a diagnostics object when the file is not a valid format', () => { - let result: ParseAnalysis; - const parser = new Xliff2TranslationParser(); - - result = parser.analyze('/some/file.xlf', ''); - expect(result.diagnostics.messages).toEqual([ - {type: 'warning', message: 'The XML file does not contain a root node.'} - ]); - - result = parser.analyze('/some/file.xlf', ''); - expect(result.diagnostics.messages).toEqual([{ - type: 'warning', - message: - 'The node does not have the required attribute: version="2.0". ("[WARNING ->]"): /some/file.xlf@0:0' - }]); - - result = parser.analyze('/some/file.xlf', '
'); - expect(result.diagnostics.messages).toEqual([{ - type: 'error', - message: - 'Unexpected closing tag "file". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]
"): /some/file.xlf@0:21' - }]); - }); - }); - - for (const withHint of [true, false]) { - describe( - `parse() [${withHint ? 'with' : 'without'} hint]`, () => { - const doParse: (fileName: string, XLIFF: string) => ParsedTranslationBundle = - withHint ? (fileName, XLIFF) => { - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse(fileName, XLIFF); - if (!hint) { - throw new Error('expected XLIFF to be valid'); - } - return parser.parse(fileName, XLIFF, hint); - } : (fileName, XLIFF) => { - const parser = new Xliff2TranslationParser(); - return parser.parse(fileName, XLIFF); + it('should return a diagnostics object when the file is not a valid format', () => { + let result: ParseAnalysis; + const parser = new Xliff2TranslationParser(); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([ + {type: 'warning', message: 'The XML file does not contain a root node.'} + ]); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([{ + type: 'warning', + message: + 'The node does not have the required attribute: version="2.0". ("[WARNING ->]"): /some/file.xlf@0:0' + }]); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([{ + type: 'error', + message: + 'Unexpected closing tag "file". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]"): /some/file.xlf@0:21' + }]); + }); + }); + + for (const withHint of [true, false]) { + describe( + `parse() [${withHint ? 'with' : 'without'} hint]`, () => { + const doParse: (fileName: string, XLIFF: string) => ParsedTranslationBundle = + withHint ? (fileName, XLIFF) => { + const parser = new Xliff2TranslationParser(); + const hint = parser.canParse(fileName, XLIFF); + if (!hint) { + throw new Error('expected XLIFF to be valid'); + } + return parser.parse(fileName, XLIFF, hint); + } : (fileName, XLIFF) => { + const parser = new Xliff2TranslationParser(); + return parser.parse(fileName, XLIFF); + }; + + const expectToFail: + (fileName: string, XLIFF: string, errorMatcher: RegExp, diagnosticMessage: string) => + void = withHint ? (fileName, XLIFF, _errorMatcher, diagnosticMessage) => { + const result = doParse(fileName, XLIFF); + expect(result.diagnostics.messages.length).toBeGreaterThan(0); + expect(result.diagnostics.messages.pop()!.message).toEqual(diagnosticMessage); + } : (fileName, XLIFF, errorMatcher, _diagnosticMessage) => { + expect(() => doParse(fileName, XLIFF)).toThrowError(errorMatcher); }; - const expectToFail: - (fileName: string, XLIFF: string, errorMatcher: RegExp, - diagnosticMessage: string) => void = - withHint ? (fileName, XLIFF, _errorMatcher, diagnosticMessage) => { - const result = doParse(fileName, XLIFF); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message).toEqual(diagnosticMessage); - } : (fileName, XLIFF, errorMatcher, _diagnosticMessage) => { - expect(() => doParse(fileName, XLIFF)).toThrowError(errorMatcher); - }; - - it('should extract the locale from the file contents', () => { - const XLIFF = [ - ``, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.locale).toEqual('fr'); - }); - - it('should return undefined locale if there is no locale in the file', () => { - const XLIFF = [ - ``, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.locale).toBeUndefined(); - }); - - it('should extract basic messages', () => { - /** - * Source HTML: - * - * ``` - *
translatable attribute
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` file.ts:2`, - ` `, - ` `, - ` translatable attribute`, - ` etubirtta elbatalsnart`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations[ɵcomputeMsgId('translatable attribute', '')]) - .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); - }); - - it('should extract translations with simple placeholders', () => { - /** - * Source HTML: - * - * ``` - *
translatable element with placeholders {{ interpolation}}
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` file.ts:3`, - ` `, - ` `, - ` translatable element with placeholders `, - ` tnemele elbatalsnart sredlohecalp htiw`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect( - result.translations[ɵcomputeMsgId( - 'translatable element {$START_BOLD_TEXT}with placeholders{$CLOSE_BOLD_TEXT} {$INTERPOLATION}')]) - .toEqual(ɵmakeParsedTranslation( - ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], - ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); - }); - - it('should extract nested placeholder containers (i.e. nested HTML elements)', () => { - /** - * Source HTML: - * - * ``` - *
- * translatable element with placeholders {{ interpolation}} - *
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` file.ts:3`, - ` `, - ` `, - ` translatable element with placeholders` + - ` `, - ` tnemele` + - ` elbatalsnart sredlohecalp htiw`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect( - result.translations[ɵcomputeMsgId( - 'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' + - '{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')]) - .toEqual(ɵmakeParsedTranslation( - ['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [ - 'START_TAG_SPAN', - 'INTERPOLATION', - 'CLOSE_TAG_SPAN', - 'START_BOLD_TEXT', - 'CLOSE_BOLD_TEXT', - ])); - }); - - it('should extract translations with simple ICU expressions', () => { - /** - * Source HTML: - * - * ``` - *
{VAR_PLURAL, plural, =0 {

test

} }
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` file.ts:4`, - ` `, - ` `, - ` {VAR_PLURAL, plural, =0 {test} }`, - ` {VAR_PLURAL, plural, =0 {TEST} }`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations[ɵcomputeMsgId( - '{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}test{CLOSE_PARAGRAPH}}}')]) - .toEqual(ɵmakeParsedTranslation( - ['{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}TEST{CLOSE_PARAGRAPH}}}'], [])); - }); - - it('should extract translations with duplicate source messages', () => { - /** - * Source HTML: - * - * ``` - *
foo
- *
foo
- *
foo
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` d`, - ` m`, - ` file.ts:5`, - ` `, - ` `, - ` foo`, - ` oof`, - ` `, - ` `, - ` `, - ` `, - ` d`, - ` m`, - ` file.ts:5`, - ` `, - ` `, - ` foo`, - ` toto`, - ` `, - ` `, - ` `, - ` `, - ` d`, - ` m`, - ` file.ts:5`, - ` `, - ` `, - ` foo`, - ` tata`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations[ɵcomputeMsgId('foo')]) - .toEqual(ɵmakeParsedTranslation(['oof'])); - expect(result.translations['i']).toEqual(ɵmakeParsedTranslation(['toto'])); - expect(result.translations['bar']).toEqual(ɵmakeParsedTranslation(['tata'])); - }); - - it('should extract translations with only placeholders, which are re-ordered', () => { - /** - * Source HTML: - * - * ``` - *

- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` ph names`, - ` file.ts:7`, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')]) - .toEqual(ɵmakeParsedTranslation( - ['', '', '', ''], ['TAG_IMG_1', 'TAG_IMG', 'LINE_BREAK'])); - }); - - it('should extract translations with empty target', () => { - /** - * Source HTML: - * - * ``` - *
hello
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` empty element`, - ` file.ts:8`, - ` `, - ` `, - ` hello `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect( - result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')]) - .toEqual(ɵmakeParsedTranslation([''])); - }); - - it('should extract translations with deeply nested ICUs', () => { - /** - * Source HTML: - * - * ``` - * Test: { count, plural, =0 { { sex, select, other {

deeply nested

}} } - * =other {a lot}} - * ``` - * - * Note that the message gets split into two translation units: - * * The first one contains the outer message with an `ICU` placeholder - * * The second one is the ICU expansion itself - * - * Note that special markers `VAR_PLURAL` and `VAR_SELECT` are added, which are then - * replaced by IVY at runtime with the actual values being rendered by the ICU - * expansion. - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` file.ts:10`, - ` `, - ` `, - ` Test: `, - ` Le test: `, - ` `, - ` `, - ` `, - ` `, - ` file.ts:10`, - ` `, - ` `, - ` {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {deeply nested}}} =other {a lot}}`, - ` {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {profondément imbriqué}}} =other {beaucoup}}`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations[ɵcomputeMsgId('Test: {$ICU}')]) - .toEqual(ɵmakeParsedTranslation(['Le test: ', ''], ['ICU'])); - expect( - result.translations[ɵcomputeMsgId( - '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}deeply nested{CLOSE_PARAGRAPH}}}} =other {beaucoup}}')]) - .toEqual(ɵmakeParsedTranslation([ - '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}profondément imbriqué{CLOSE_PARAGRAPH}}}} =other {beaucoup}}' + it('should extract the locale from the file contents', () => { + const XLIFF = [ + ``, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.locale).toEqual('fr'); + }); + + it('should return undefined locale if there is no locale in the file', () => { + const XLIFF = [ + ``, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.locale).toBeUndefined(); + }); + + it('should extract basic messages', () => { + /** + * Source HTML: + * + * ``` + *
translatable attribute
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` file.ts:2`, + ` `, + ` `, + ` translatable attribute`, + ` etubirtta elbatalsnart`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('translatable attribute', '')]) + .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); + }); + + it('should extract translations with simple placeholders', () => { + /** + * Source HTML: + * + * ``` + *
translatable element with placeholders {{ interpolation}}
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` file.ts:3`, + ` `, + ` `, + ` translatable element with placeholders `, + ` tnemele elbatalsnart sredlohecalp htiw`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect( + result.translations[ɵcomputeMsgId( + 'translatable element {$START_BOLD_TEXT}with placeholders{$CLOSE_BOLD_TEXT} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], + ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); + }); + + it('should extract nested placeholder containers (i.e. nested HTML elements)', () => { + /** + * Source HTML: + * + * ``` + *
+ * translatable element with placeholders {{ interpolation}} + *
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` file.ts:3`, + ` `, + ` `, + ` translatable element with placeholders` + + ` `, + ` tnemele` + + ` elbatalsnart sredlohecalp htiw`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId( + 'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' + + '{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [ + 'START_TAG_SPAN', + 'INTERPOLATION', + 'CLOSE_TAG_SPAN', + 'START_BOLD_TEXT', + 'CLOSE_BOLD_TEXT', ])); - }); - - it('should extract translations containing multiple lines', () => { - /** - * Source HTML: - * - * ``` - *
multi - * lines
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` file.ts:11,12`, - ` `, - ` `, - ` multi\nlines`, - ` multi\nlignes`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations[ɵcomputeMsgId('multi\nlines')]) - .toEqual(ɵmakeParsedTranslation(['multi\nlignes'])); - }); - - it('should extract translations with elements', () => { - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` First sentence.`, - ` Translated first sentence.`, - ` `, - ` `, - ` `, - ` `, - ` First sentence. Second sentence.`, - ` Translated first sentence.`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations['mrk-test']) - .toEqual(ɵmakeParsedTranslation(['Translated first sentence.'])); - expect(result.translations['mrk-test2']) - .toEqual(ɵmakeParsedTranslation(['Translated first sentence.'])); - }); - - it('should merge messages from each `` element', () => { - /** - * Source HTML: - * - * ``` - *
translatable attribute
- * ``` - * - * ``` - *
translatable element with placeholders {{ interpolation}}
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` file.ts:2`, - ` `, - ` `, - ` translatable attribute`, - ` etubirtta elbatalsnart`, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` file.ts:3`, - ` `, - ` `, - ` translatable element with placeholders `, - ` tnemele elbatalsnart sredlohecalp htiw`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.translations[ɵcomputeMsgId('translatable attribute', '')]) - .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); - expect( - result.translations[ɵcomputeMsgId( - 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) - .toEqual(ɵmakeParsedTranslation( - ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], - ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); - }); - - describe('[structure errors]', () => { - it('should provide a diagnostic error when a trans-unit has no translation', () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - expectToFail('/some/file.xlf', XLIFF, /Missing required element/, [ - `Missing required element ("`, - ` `, - ` `, - ` [ERROR ->]`, - ` `, - ` `, - `"): /some/file.xlf@4:6`, - ].join('\n')); - }); - + }); + + it('should extract translations with simple ICU expressions', () => { + /** + * Source HTML: + * + * ``` + *
{VAR_PLURAL, plural, =0 {

test

} }
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` file.ts:4`, + ` `, + ` `, + ` {VAR_PLURAL, plural, =0 {test} }`, + ` {VAR_PLURAL, plural, =0 {TEST} }`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId( + '{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}test{CLOSE_PARAGRAPH}}}')]) + .toEqual(ɵmakeParsedTranslation( + ['{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}TEST{CLOSE_PARAGRAPH}}}'], [])); + }); + + it('should extract translations with duplicate source messages', () => { + /** + * Source HTML: + * + * ``` + *
foo
+ *
foo
+ *
foo
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` d`, + ` m`, + ` file.ts:5`, + ` `, + ` `, + ` foo`, + ` oof`, + ` `, + ` `, + ` `, + ` `, + ` d`, + ` m`, + ` file.ts:5`, + ` `, + ` `, + ` foo`, + ` toto`, + ` `, + ` `, + ` `, + ` `, + ` d`, + ` m`, + ` file.ts:5`, + ` `, + ` `, + ` foo`, + ` tata`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('foo')]) + .toEqual(ɵmakeParsedTranslation(['oof'])); + expect(result.translations['i']).toEqual(ɵmakeParsedTranslation(['toto'])); + expect(result.translations['bar']).toEqual(ɵmakeParsedTranslation(['tata'])); + }); + + it('should extract translations with only placeholders, which are re-ordered', () => { + /** + * Source HTML: + * + * ``` + *

+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` ph names`, + ` file.ts:7`, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')]) + .toEqual(ɵmakeParsedTranslation( + ['', '', '', ''], ['TAG_IMG_1', 'TAG_IMG', 'LINE_BREAK'])); + }); + + it('should extract translations with empty target', () => { + /** + * Source HTML: + * + * ``` + *
hello
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` empty element`, + ` file.ts:8`, + ` `, + ` `, + ` hello `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')]) + .toEqual(ɵmakeParsedTranslation([''])); + }); + + it('should extract translations with deeply nested ICUs', () => { + /** + * Source HTML: + * + * ``` + * Test: { count, plural, =0 { { sex, select, other {

deeply nested

}} } + * =other {a lot}} + * ``` + * + * Note that the message gets split into two translation units: + * * The first one contains the outer message with an `ICU` placeholder + * * The second one is the ICU expansion itself + * + * Note that special markers `VAR_PLURAL` and `VAR_SELECT` are added, which are then + * replaced by IVY at runtime with the actual values being rendered by the ICU + * expansion. + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` file.ts:10`, + ` `, + ` `, + ` Test: `, + ` Le test: `, + ` `, + ` `, + ` `, + ` `, + ` file.ts:10`, + ` `, + ` `, + ` {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {deeply nested}}} =other {a lot}}`, + ` {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {profondément imbriqué}}} =other {beaucoup}}`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('Test: {$ICU}')]) + .toEqual(ɵmakeParsedTranslation(['Le test: ', ''], ['ICU'])); + expect( + result.translations[ɵcomputeMsgId( + '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}deeply nested{CLOSE_PARAGRAPH}}}} =other {beaucoup}}')]) + .toEqual(ɵmakeParsedTranslation([ + '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}profondément imbriqué{CLOSE_PARAGRAPH}}}} =other {beaucoup}}' + ])); + }); + + it('should extract translations containing multiple lines', () => { + /** + * Source HTML: + * + * ``` + *
multi + * lines
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` file.ts:11,12`, + ` `, + ` `, + ` multi\nlines`, + ` multi\nlignes`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('multi\nlines')]) + .toEqual(ɵmakeParsedTranslation(['multi\nlignes'])); + }); + + it('should extract translations with elements', () => { + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` First sentence.`, + ` Translated first sentence.`, + ` `, + ` `, + ` `, + ` `, + ` First sentence. Second sentence.`, + ` Translated first sentence.`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations['mrk-test']) + .toEqual(ɵmakeParsedTranslation(['Translated first sentence.'])); + expect(result.translations['mrk-test2']) + .toEqual(ɵmakeParsedTranslation(['Translated first sentence.'])); + }); + + it('should merge messages from each `` element', () => { + /** + * Source HTML: + * + * ``` + *
translatable attribute
+ * ``` + * + * ``` + *
translatable element with placeholders {{ interpolation}}
+ * ``` + */ + const XLIFF = [ + ``, + ` `, + ` `, + ` `, + ` file.ts:2`, + ` `, + ` `, + ` translatable attribute`, + ` etubirtta elbatalsnart`, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` file.ts:3`, + ` `, + ` `, + ` translatable element with placeholders `, + ` tnemele elbatalsnart sredlohecalp htiw`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('translatable attribute', '')]) + .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); + expect( + result.translations[ɵcomputeMsgId( + 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], + ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); + }); + + describe('[structure errors]', () => { + it('should provide a diagnostic warning when a trans-unit has no translation target but does have a source', + () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + const result = doParse('/some/file.xlf', XLIFF); + expect(result.diagnostics.messages.length).toEqual(1); + expect(result.diagnostics.messages[0].message).toEqual([ + `Missing element ("`, + ` `, + ` `, + ` [WARNING ->]`, + ` `, + ` `, + `"): /some/file.xlf@4:6`, + ].join('\n')); + }); + + it('should provide a diagnostic error when a trans-unit has no translation target nor source', + () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail( + '/some/file.xlf', XLIFF, + /Missing required element: one of or is required/, [ + `Missing required element: one of or is required ("`, + ` `, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + `"): /some/file.xlf@4:6`, + ].join('\n')); + }); + + + it('should provide a diagnostic error when a trans-unit has no id attribute', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Missing required "id" attribute/, [ + `Missing required "id" attribute on element. ("s:tc:xliff:document:2.0" srcLang="en" trgLang="fr">`, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + `"): /some/file.xlf@3:4`, + ].join('\n')); + }); - it('should provide a diagnostic error when a trans-unit has no id attribute', () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` `, + it('should provide a diagnostic error on duplicate trans-unit id', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail( + '/some/file.xlf', XLIFF, /Duplicated translations for message "deadbeef"/, [ + `Duplicated translations for message "deadbeef" ("`, ` `, ` `, - ` `, - ``, - ].join('\n'); - - expectToFail('/some/file.xlf', XLIFF, /Missing required "id" attribute/, [ - `Missing required "id" attribute on element. ("s:tc:xliff:document:2.0" srcLang="en" trgLang="fr">`, - ` `, - ` [ERROR ->]`, + ` [ERROR ->]`, ` `, ` `, - `"): /some/file.xlf@3:4`, + '"): /some/file.xlf@9:4', ].join('\n')); - }); - - it('should provide a diagnostic error on duplicate trans-unit id', () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - expectToFail( - '/some/file.xlf', XLIFF, /Duplicated translations for message "deadbeef"/, [ - `Duplicated translations for message "deadbeef" ("`, - ` `, - ` `, - ` [ERROR ->]`, - ` `, - ` `, - '"): /some/file.xlf@9:4', - ].join('\n')); - }); - }); - - describe('[message errors]', () => { - it('should provide a diagnostic error on unknown message tags', () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` msg should contain only ph and pc tags`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - expectToFail('/some/file.xlf', XLIFF, /Invalid element found in message/, [ - `Error: Invalid element found in message.`, - `At /some/file.xlf@6:16:`, - `...`, - ` `, - ` [ERROR ->]msg should contain only ph and pc tags`, - ` `, - `...`, - ``, - ].join('\n')); - }); + }); + }); + + describe('[message errors]', () => { + it('should provide a diagnostic error on unknown message tags', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` msg should contain only ph and pc tags`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Invalid element found in message/, [ + `Error: Invalid element found in message.`, + `At /some/file.xlf@6:16:`, + `...`, + ` `, + ` [ERROR ->]msg should contain only ph and pc tags`, + ` `, + `...`, + ``, + ].join('\n')); + }); - it('should provide a diagnostic error when a placeholder misses an id attribute', - () => { - const XLIFF = [ - ``, - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - - expectToFail('/some/file.xlf', XLIFF, /Missing required "equiv" attribute/, [ - `Error: Missing required "equiv" attribute:`, - `At /some/file.xlf@6:16:`, - `...`, - ` `, - ` [ERROR ->]`, - ` `, - `...`, - ``, - ].join('\n')); - }); - }); + it('should provide a diagnostic error when a placeholder misses an id attribute', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Missing required "equiv" attribute/, [ + `Error: Missing required "equiv" attribute:`, + `At /some/file.xlf@6:16:`, + `...`, + ` `, + ` [ERROR ->]`, + ` `, + `...`, + ``, + ].join('\n')); }); - } - }); + }); + }); + } +});