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')); }); - } - }); + }); + }); + } +});