diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts index 2f3276f95c..9a9452b41d 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts @@ -7,6 +7,7 @@ */ import {ɵmakeTemplateObject} from '@angular/localize'; import {SimpleJsonTranslationParser} from '../../../../src/translate/translation_files/translation_parsers/simple_json_translation_parser'; +import {ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; describe('SimpleJsonTranslationParser', () => { describe('canParse()', () => { @@ -22,27 +23,41 @@ describe('SimpleJsonTranslationParser', () => { }); }); - describe('parse()', () => { - it('should extract the locale from the JSON contents', () => { - const parser = new SimpleJsonTranslationParser(); - const result = parser.parse('/some/file.json', '{"locale": "en", "translations": {}}'); - expect(result.locale).toEqual('en'); - }); + for (const withHint of [true, false]) { + describe(`parse() [${withHint ? 'with' : 'without'} hint]`, () => { + const doParse: (fileName: string, contents: string) => ParsedTranslationBundle = + withHint ? (fileName, contents) => { + const parser = new SimpleJsonTranslationParser(); + const hint = parser.canParse(fileName, contents); + if (!hint) { + throw new Error('expected contents to be valid'); + } + return parser.parse(fileName, contents, hint); + } : (fileName, contents) => { + const parser = new SimpleJsonTranslationParser(); + return parser.parse(fileName, contents); + }; - it('should extract and process the translations from the JSON contents', () => { - const parser = new SimpleJsonTranslationParser(); - const result = parser.parse('/some/file.json', `{ + + it('should extract the locale from the JSON contents', () => { + const result = doParse('/some/file.json', '{"locale": "en", "translations": {}}'); + expect(result.locale).toEqual('en'); + }); + + it('should extract and process the translations from the JSON contents', () => { + const result = doParse('/some/file.json', `{ "locale": "fr", "translations": { "Hello, {$ph_1}!": "Bonjour, {$ph_1}!" } }`); - expect(result.translations).toEqual({ - 'Hello, {$ph_1}!': { - messageParts: ɵmakeTemplateObject(['Bonjour, ', '!'], ['Bonjour, ', '!']), - placeholderNames: ['ph_1'] - }, + expect(result.translations).toEqual({ + 'Hello, {$ph_1}!': { + messageParts: ɵmakeTemplateObject(['Bonjour, ', '!'], ['Bonjour, ', '!']), + placeholderNames: ['ph_1'] + }, + }); }); }); - }); -}); \ No newline at end of file + } +}); 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 6edfa714b9..0657fddc82 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 @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; +import {ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; import {Xliff1TranslationParser} from '../../../../src/translate/translation_files/translation_parsers/xliff1_translation_parser'; describe('Xliff1TranslationParser', () => { @@ -30,1281 +31,636 @@ describe('Xliff1TranslationParser', () => { }); }); - describe('parse() [without hint]', () => { - it('should extract the locale from the last `` element to contain a `target-language` attribute', - () => { - const XLIFF = ` - - - - - - - - - - - - - - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.locale).toEqual('de'); - }); + 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); + }; - it('should return an undefined locale if there is no locale in the file', () => { - const XLIFF = ` - - - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/some/file.xlf', XLIFF); - expect(result.locale).toBeUndefined(); - }); + 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 basic messages', () => { - /** - * Source HTML: - * - * ``` - *
translatable attribute
- * ``` - */ - const XLIFF = ` - - - - - translatable attribute - etubirtta elbatalsnart - - file.ts - 1 - - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/some/file.xlf', XLIFF); + 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'); + }); - 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 - - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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 translations with placeholders containing hyphens', () => { - /** - * Source HTML: - * - * ``` - *
Welcome
- * ``` - */ - const XLIFF = ` - - - - - Welcome - - src/app/app.component.html - 1 - - Translate - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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} } - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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 - - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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 - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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 - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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}} - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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 - - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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. - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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 - - - - - `; - - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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 - - - - - `; - const parser = new Xliff1TranslationParser(); - const result = parser.parse('/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 throw when a trans-unit has no translation', () => { - const XLIFF = ` - - - - - - - - - - `; - - expect(() => { - const parser = new Xliff1TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/Missing required element/); + 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); - it('should throw when a trans-unit has no id attribute', () => { - const XLIFF = ` - - - - - - - - - - - `; - - expect(() => { - const parser = new Xliff1TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/Missing required "id" attribute/); + expect(result.translations[ɵcomputeMsgId('translatable attribute')]) + .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); }); - it('should throw on duplicate trans-unit id', () => { - const XLIFF = ` - - - - - - - - - - - - - - - `; + 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(() => { - const parser = new Xliff1TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/Duplicated translations for message "deadbeef"/); - }); - }); - - describe('[message errors]', () => { - it('should throw on unknown message tags', () => { - const XLIFF = ` - - - - - - - msg should contain only ph tags - - - - `; - - expect(() => { - const parser = new Xliff1TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/Invalid element found in message/); + 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 throw when a placeholder misses an id attribute', () => { - const XLIFF = ` - - - - - - - - - - - `; - - expect(() => { - const parser = new Xliff1TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/required "id" attribute/gi); + 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' + ])); }); - }); - }); - describe('parse() [with hint]', () => { - it('should extract the locale from the last `` element to contain a `target-language` attribute', - () => { - const XLIFF = ` - - - - - - - - - - - - - - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.locale).toEqual('de'); - }); + 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); - it('should return an undefined locale if there is no locale in the file', () => { - const XLIFF = ` - - - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.locale).toBeUndefined(); - }); + 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 basic messages', () => { - /** - * Source HTML: - * - * ``` - *
translatable attribute
- * ``` - */ - const XLIFF = ` - - - - - translatable attribute - etubirtta elbatalsnart - - file.ts - 1 - - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); + 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('translatable attribute')]) - .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); - }); + 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 simple placeholders', () => { - /** - * Source HTML: - * - * ``` - *
translatable element with placeholders {{ interpolation}}
- * ``` - */ - const XLIFF = ` - - - - - translatable element with placeholders - tnemele elbatalsnart sredlohecalp htiw - - file.ts - 2 - - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); + 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( - '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 translations with placeholders containing hyphens', () => { - /** - * Source HTML: - * - * ``` - *
Welcome
- * ``` - */ - const XLIFF = ` - - - - - Welcome - - src/app/app.component.html - 1 - - Translate - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - 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} } - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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}} - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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. - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - - `; - - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - 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 - - - - - `; - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 = ` - - - - - - - - - - `; - - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) + expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')]) .toEqual( - `Missing required element ("ge="en" target-language="fr" datatype="plaintext" original="ng2.template">\n` + - ` \n` + - ` [ERROR ->]\n` + - ` \n` + - ` \n` + - `"): /some/file.xlf@5:10`); + ɵ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); - it('should provide a diagnostic error when a trans-unit has no id attribute', () => { - const XLIFF = ` - - - - - - - - - - - `; - - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual( - `Missing required "id" attribute on element. ("ge="en" target-language="fr" datatype="plaintext" original="ng2.template">\n` + - ` \n` + - ` [ERROR ->]\n` + - ` \n` + - ` \n` + - `"): /some/file.xlf@5:10`); + expect(result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')]) + .toEqual(ɵmakeParsedTranslation([''])); }); - it('should provide a diagnostic error on duplicate trans-unit id', () => { - const XLIFF = ` - - - - - - - - - - - - - - - `; + 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); - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual( - `Duplicated translations for message "deadbeef" ("\n` + - ` \n` + - `
\n` + - ` [ERROR ->]\n` + - ` \n` + - ` \n` + - `"): /some/file.xlf@9:10`); + 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 throw 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 throw 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 throw 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 throw on unknown message tags', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` msg should contain only ph tags`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Invalid element found in message/, [ + `Invalid element found in message. ("`, + ` `, + ` `, + ` [ERROR ->]msg should contain only ph tags`, + ` `, + ` `, + `"): /some/file.xlf@6:16`, + ].join('\n')); + }); + + it('should throw when a placeholder misses an id attribute', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /required "id" attribute/gi, [ + `Missing required "id" attribute: ("`, + ` `, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + `"): /some/file.xlf@6:16`, + ].join('\n')); + }); }); }); - - describe('[message errors]', () => { - it('should provide a diagnostic error on unknown message tags', () => { - const XLIFF = ` - - - - - - - msg should contain only ph tags - - - - `; - - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual( - `Invalid element found in message. ("\n` + - ` \n` + - ` \n` + - ` [ERROR ->]msg should contain only ph tags\n` + - ` \n` + - ` \n` + - `"): /some/file.xlf@7:20`); - }); - - it('should provide a diagnostic error when a placeholder misses an id attribute', () => { - const XLIFF = ` - - - - - - - - - - - `; - - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual( - `Missing required "id" attribute: ("\n` + - ` \n` + - ` \n` + - ` [ERROR ->]\n` + - ` \n` + - ` \n` + - `"): /some/file.xlf@7:20`); - }); - }); - }); + } }); 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 438106dd4c..5be22dfc80 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 @@ -6,1126 +6,583 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; +import {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('parse() [without hint]', () => { - it('should extract the locale from the file contents', () => { - const XLIFF = ` - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/some/file.xlf', XLIFF); - expect(result.locale).toEqual('fr'); - }); - - it('should return undefined locale if there is no locale in the file', () => { - const XLIFF = ` - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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 - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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 - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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 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} } - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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 - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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 - - - - - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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 - - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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}} - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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 - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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. - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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 - - - - `; - const parser = new Xliff2TranslationParser(); - const result = parser.parse('/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 throw when a trans-unit has no translation', () => { - const XLIFF = ` - - - - - - - - - `; - - expect(() => { - const parser = new Xliff2TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/Missing required element/); +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); + }); }); + 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 throw when a trans-unit has no id attribute', () => { - const 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); + }; - expect(() => { - const parser = new Xliff2TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/Missing required "id" attribute/); - }); + 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 throw on duplicate trans-unit id', () => { - const XLIFF = ` - - - - - - - - - - - - - - - - `; + 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(); + }); - expect(() => { - const parser = new Xliff2TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/Duplicated translations for message "deadbeef"/); - }); - }); + 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'])); + }); - describe('[message errors]', () => { - it('should throw on unknown message tags', () => { - const XLIFF = ` - - - - - - msg should contain only ph and pc tags - - - - `; + 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{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], + ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); + }); - expect(() => { - const parser = new Xliff2TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/Invalid element found in message/); - }); + 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 throw when a placeholder misses an id attribute', () => { - const XLIFF = ` - - - - - - - - - - `; + 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'])); + }); - expect(() => { - const parser = new Xliff2TranslationParser(); - parser.parse('/some/file.xlf', XLIFF); - }).toThrowError(/Missing required "equiv" attribute/); - }); - }); - }); + 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'])); + }); - describe('parse() [with hint]', () => { - it('should extract the locale from the file contents', () => { - const XLIFF = ` - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); + 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 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 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 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/, [ + `Invalid element found in message. ("`, + ` `, + ` `, + ` [ERROR ->]msg should contain only ph and pc tags`, + ` `, + ` `, + `"): /some/file.xlf@6:16`, + ].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/, [ + `Missing required "equiv" attribute: ("`, + ` `, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + `"): /some/file.xlf@6:16`, + ].join('\n')); + }); + }); + }); } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.locale).toEqual('fr'); }); - - it('should return undefined locale if there is no locale in the file', () => { - const XLIFF = ` - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.locale).toBeUndefined(); - }); - - it('should extract basic messages', () => { - /** - * Source HTML: - * - * ``` - *
translatable attribute
- * ``` - */ - const XLIFF = ` - - - - - file.ts:2 - - - translatable attribute - etubirtta elbatalsnart - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 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} } - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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}} - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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. - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 - - - - `; - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - - 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 = ` - - - - - - - - - `; - - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message).toEqual(`Missing required element (" - - - [ERROR ->] - - -"): /some/file.xlf@4:12`); - }); - - - it('should provide a diagnostic error when a trans-unit has no id attribute', () => { - const XLIFF = ` - - - - - - - - - - `; - - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual( - `Missing required "id" attribute on element. ("ocument:2.0" srcLang="en" trgLang="fr"> - - [ERROR ->] - - -"): /some/file.xlf@3:10`); - }); - - it('should provide a diagnostic error on duplicate trans-unit id', () => { - const XLIFF = ` - - - - - - - - - - - - - - - - `; - - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual(`Duplicated translations for message "deadbeef" (" - - - [ERROR ->] - - -"): /some/file.xlf@9:10`); - }); - }); - - describe('[message errors]', () => { - it('should provide a diagnostic error on unknown message tags', () => { - const XLIFF = ` - - - - - - msg should contain only ph and pc tags - - - - `; - - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message).toEqual(`Invalid element found in message. (" - - - [ERROR ->]msg should contain only ph and pc tags - - -"): /some/file.xlf@6:22`); - }); - - it('should provide a diagnostic error when a placeholder misses an id attribute', () => { - const XLIFF = ` - - - - - - - - - - `; - - const parser = new Xliff2TranslationParser(); - const hint = parser.canParse('/some/file.xlf', XLIFF); - if (!hint) { - return fail('expected XLIFF to be valid'); - } - const result = parser.parse('/some/file.xlf', XLIFF, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual(`Missing required "equiv" attribute: (" - - - [ERROR ->] - - -"): /some/file.xlf@6:22`); - }); - }); - }); -}); diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts index dbb44a9a94..5d3c15390c 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; -import {Diagnostics} from '../../../../src/diagnostics'; +import {ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; import {XtbTranslationParser} from '../../../../src/translate/translation_files/translation_parsers/xtb_translation_parser'; describe('XtbTranslationParser', () => { @@ -24,593 +24,299 @@ describe('XtbTranslationParser', () => { }); }); - describe('parse() [without hint]', () => { - it('should extract the locale from the file contents', () => { - const XTB = ` - - rab - `; - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - expect(result.locale).toEqual('fr'); - }); + for (const withHint of [true, false]) { + describe(`parse() [${withHint ? 'with' : 'without'} hint]`, () => { + const doParse: (fileName: string, XTB: string) => ParsedTranslationBundle = + withHint ? (fileName, XTB) => { + const parser = new XtbTranslationParser(); + const hint = parser.canParse(fileName, XTB); + if (!hint) { + throw new Error('expected XTB to be valid'); + } + return parser.parse(fileName, XTB, hint); + } : (fileName, XTB) => { + const parser = new XtbTranslationParser(); + return parser.parse(fileName, XTB); + }; - it('should extract basic messages', () => { - const XTB = ` - - + 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); + }; - - - - - - ]> - - rab - `; - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - - expect(result.translations['8841459487341224498']).toEqual(ɵmakeParsedTranslation(['rab'])); - }); - - it('should extract translations with simple placeholders', () => { - const XTB = ` - - rab - `; - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - - expect(result.translations['8877975308926375834']) - .toEqual(ɵmakeParsedTranslation(['', 'rab', ''], ['START_PARAGRAPH', 'CLOSE_PARAGRAPH'])); - }); - - it('should extract translations with simple ICU expressions', () => { - const XTB = ` - - ** - {VAR_PLURAL, plural, =1 {rab}} - `; - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - - expect(result.translations['7717087045075616176']) - .toEqual(ɵmakeParsedTranslation(['*', '*'], ['ICU'])); - expect(result.translations['5115002811911870583']) - .toEqual(ɵmakeParsedTranslation( - ['{VAR_PLURAL, plural, =1 {{START_PARAGRAPH}rab{CLOSE_PARAGRAPH}}}'], [])); - }); - - it('should extract translations with duplicate source messages', () => { - const XTB = ` - - oof - toto - tata - `; - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - - 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', () => { - const XTB = ` - - - `; - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - - 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 XTB = ` - - - `; - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - - 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 XTB = ` - - Le test: - {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {profondément imbriqué}}} =other {beaucoup}} - `; - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - - 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 XTB = ` - - multi\nlignes - `; - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - - expect(result.translations[ɵcomputeMsgId('multi\nlines')]) - .toEqual(ɵmakeParsedTranslation(['multi\nlignes'])); - }); - - it('should warn on unrecognised ICU messages', () => { - // See https://github.com/angular/angular/issues/14046 - - const XTB = ` - - This is a valid message - {REGION_COUNT_1, plural, =0 {unused plural form} =1 {1 region} other {{REGION_COUNT_2} regions}} - `; - - // Parsing the file should not fail - const parser = new XtbTranslationParser(); - const result = parser.parse('/some/file.xtb', XTB); - - // We should be able to read the valid message - expect(result.translations['valid']) - .toEqual(ɵmakeParsedTranslation(['This is a valid message'])); - - // Trying to access the invalid message should fail - expect(result.translations['invalid']).toBeUndefined(); - expect(result.diagnostics.messages).toContain({ - type: 'warning', - message: - `Could not parse message with id "invalid" - perhaps it has an unrecognised ICU format?\n` + - `Error: Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)\n` + - `Error: Invalid ICU message. Missing '}'.` - }); - }); - - describe('[structure errors]', () => { - it('should throw when there are nested translationbundle tags', () => { - const XTB = - ''; - - expect(() => { - const parser = new XtbTranslationParser(); - parser.parse('/some/file.xtb', XTB); - }).toThrowError(`Failed to parse "/some/file.xtb" as XMB/XTB format -ERRORS: - - Unexpected tag. ("[ERROR ->]"): /some/file.xtb@0:19`); + it('should extract the locale from the file contents', () => { + const XTB = [ + ``, + ``, + ` rab`, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XTB); + expect(result.locale).toEqual('fr'); }); - it('should throw when a translation has no id attribute', () => { - const XTB = ` - - - `; + it('should extract basic messages', () => { + const XTB = [ + ``, + ``, + ` `, + ``, + ` `, + ` `, + ``, + ` `, + ` `, + `]>`, + ``, + ` rab`, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XTB); - expect(() => { - const parser = new XtbTranslationParser(); - parser.parse('/some/file.xtb', XTB); - }).toThrowError(/Missing required "id" attribute/); + expect(result.translations['8841459487341224498']).toEqual(ɵmakeParsedTranslation(['rab'])); }); - it('should throw on duplicate translation id', () => { - const XTB = ` - - - - `; + it('should extract translations with simple placeholders', () => { + const XTB = [ + ``, + ``, + ` rab`, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XTB); - expect(() => { - const parser = new XtbTranslationParser(); - parser.parse('/some/file.xtb', XTB); - }).toThrowError(/Duplicated translations for message "deadbeef"/); - }); - }); - - describe('[message errors]', () => { - it('should throw on unknown message tags', () => { - const XTB = ` - - - - - `; - - expect(() => { - const parser = new XtbTranslationParser(); - parser.parse('/some/file.xtb', XTB); - }).toThrowError(/Invalid element found in message/); - }); - - it('should throw when a placeholder misses a name attribute', () => { - const XTB = ` - - - `; - - expect(() => { - const parser = new XtbTranslationParser(); - parser.parse('/some/file.xtb', XTB); - }).toThrowError(/required "name" attribute/gi); - }); - }); - }); - - describe('parse() [with hint]', () => { - it('should extract the locale from the file contents', () => { - const XTB = ` - - rab - `; - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - expect(result.locale).toEqual('fr'); - }); - - it('should extract basic messages', () => { - const XTB = ` - - - - - - - - - ]> - - rab - `; - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - - expect(result.translations['8841459487341224498']).toEqual(ɵmakeParsedTranslation(['rab'])); - }); - - it('should extract translations with simple placeholders', () => { - const XTB = ` - - rab - `; - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - - expect(result.translations['8877975308926375834']) - .toEqual(ɵmakeParsedTranslation(['', 'rab', ''], ['START_PARAGRAPH', 'CLOSE_PARAGRAPH'])); - }); - - it('should extract translations with simple ICU expressions', () => { - const XTB = ` - - ** - {VAR_PLURAL, plural, =1 {rab}} - `; - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - - expect(result.translations['7717087045075616176']) - .toEqual(ɵmakeParsedTranslation(['*', '*'], ['ICU'])); - expect(result.translations['5115002811911870583']) - .toEqual(ɵmakeParsedTranslation( - ['{VAR_PLURAL, plural, =1 {{START_PARAGRAPH}rab{CLOSE_PARAGRAPH}}}'], [])); - }); - - it('should extract translations with duplicate source messages', () => { - const XTB = ` - - oof - toto - tata - `; - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - - 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', () => { - const XTB = ` - - - `; - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - - 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 XTB = ` - - - `; - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - - 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 XTB = ` - - Le test: - {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {profondément imbriqué}}} =other {beaucoup}} - `; - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - - 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 XTB = ` - - multi\nlignes - `; - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - - expect(result.translations[ɵcomputeMsgId('multi\nlines')]) - .toEqual(ɵmakeParsedTranslation(['multi\nlignes'])); - }); - - it('should warn on unrecognised ICU messages', () => { - // See https://github.com/angular/angular/issues/14046 - - const XTB = ` - - This is a valid message - {REGION_COUNT_1, plural, =0 {unused plural form} =1 {1 region} other {{REGION_COUNT_2} regions}} - `; - - // Parsing the file should not fail - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - - // We should be able to read the valid message - expect(result.translations['valid']) - .toEqual(ɵmakeParsedTranslation(['This is a valid message'])); - - // Trying to access the invalid message should fail - expect(result.translations['invalid']).toBeUndefined(); - expect(result.diagnostics.messages).toContain({ - type: 'warning', - message: - `Could not parse message with id "invalid" - perhaps it has an unrecognised ICU format?\n` + - `Error: Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)\n` + - `Error: Invalid ICU message. Missing '}'.` - }); - }); - - describe('[structure errors]', () => { - it('should throw when there are nested translationbundle tags', () => { - const XTB = - ''; - - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) + expect(result.translations['8877975308926375834']) .toEqual( - `Unexpected tag. ("[ERROR ->]"): /some/file.xtb@0:19`); + ɵmakeParsedTranslation(['', 'rab', ''], ['START_PARAGRAPH', 'CLOSE_PARAGRAPH'])); }); - it('should throw when a translation has no id attribute', () => { - const XTB = ` - - - `; + it('should extract translations with simple ICU expressions', () => { + const XTB = [ + ``, + ``, + ` **`, + ` {VAR_PLURAL, plural, =1 {rab}}`, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XTB); - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual(`Missing required "id" attribute on element. (" - - [ERROR ->] - "): /some/file.xtb@2:12`); + expect(result.translations['7717087045075616176']) + .toEqual(ɵmakeParsedTranslation(['*', '*'], ['ICU'])); + expect(result.translations['5115002811911870583']) + .toEqual(ɵmakeParsedTranslation( + ['{VAR_PLURAL, plural, =1 {{START_PARAGRAPH}rab{CLOSE_PARAGRAPH}}}'], [])); }); - it('should throw on duplicate translation id', () => { - const XTB = ` - - - - `; + it('should extract translations with duplicate source messages', () => { + const XTB = [ + ``, + ` oof`, + ` toto`, + ` tata`, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XTB); - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual(`Duplicated translations for message "deadbeef" (" - - - [ERROR ->] - "): /some/file.xtb@3:12`); + 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', () => { + const XTB = [ + ``, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XTB); + + 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 XTB = [ + ``, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XTB); + + 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 XTB = [ + ``, + ` Le test: `, + ` {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {profondément imbriqué}}} =other {beaucoup}}`, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XTB); + + 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 XTB = [ + ``, + ` multi\nlignes`, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XTB); + + expect(result.translations[ɵcomputeMsgId('multi\nlines')]) + .toEqual(ɵmakeParsedTranslation(['multi\nlignes'])); + }); + + it('should warn on unrecognised ICU messages', () => { + // See https://github.com/angular/angular/issues/14046 + + const XTB = [ + ``, + ` This is a valid message`, + ` {REGION_COUNT_1, plural, =0 {unused plural form} =1 {1 region} other {{REGION_COUNT_2} regions}}`, + ``, + ].join('\n'); + + // Parsing the file should not fail + const result = doParse('/some/file.xtb', XTB); + + // We should be able to read the valid message + expect(result.translations['valid']) + .toEqual(ɵmakeParsedTranslation(['This is a valid message'])); + + // Trying to access the invalid message should fail + expect(result.translations['invalid']).toBeUndefined(); + expect(result.diagnostics.messages).toContain({ + type: 'warning', + message: + `Could not parse message with id "invalid" - perhaps it has an unrecognised ICU format?\n` + + `Error: Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)\n` + + `Error: Invalid ICU message. Missing '}'.` + }); + }); + + describe('[structure errors]', () => { + it('should throw when there are nested translationbundle tags', () => { + const XTB = + ''; + + expectToFail( + '/some/file.xtb', XTB, /Failed to parse "\/some\/file.xtb" as XMB\/XTB format/, + `Unexpected tag. ("[ERROR ->]"): /some/file.xtb@0:19`); + }); + + it('should throw when a translation has no id attribute', () => { + const XTB = [ + ``, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xtb', XTB, /Missing required "id" attribute/, [ + `Missing required "id" attribute on element. ("`, + ` [ERROR ->]`, + `"): /some/file.xtb@1:2`, + ].join('\n')); + }); + + it('should throw on duplicate translation id', () => { + const XTB = [ + ``, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xtb', XTB, /Duplicated translations for message "deadbeef"/, [ + `Duplicated translations for message "deadbeef" ("`, + ` `, + ` [ERROR ->]`, + `"): /some/file.xtb@2:2`, + ].join('\n')); + }); + }); + + describe('[message errors]', () => { + it('should throw on unknown message tags', () => { + const XTB = [ + ``, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xtb', XTB, /Invalid element found in message/, [ + `Invalid element found in message. ("`, + ` `, + ` [ERROR ->]`, + ` `, + `"): /some/file.xtb@2:4`, + ].join('\n')); + }); + + it('should throw when a placeholder misses a name attribute', () => { + const XTB = [ + ``, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xtb', XTB, /required "name" attribute/gi, [ + `Missing required "name" attribute: ("`, + ` [ERROR ->]`, + `"): /some/file.xtb@1:29`, + ].join('\n')); + }); }); }); - - describe('[message errors]', () => { - it('should throw on unknown message tags', () => { - const XTB = ` - - - - - `; - - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message).toEqual(`Invalid element found in message. (" - - - [ERROR ->] - - "): /some/file.xtb@3:14`); - }); - - it('should throw when a placeholder misses a name attribute', () => { - const XTB = ` - - - `; - - const parser = new XtbTranslationParser(); - const hint = parser.canParse('/some/file.xtb', XTB); - if (!hint) { - return fail('expected XTB to be valid'); - } - const result = parser.parse('/some/file.xtb', XTB, hint); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message) - .toEqual(`Missing required "name" attribute: (" - - [ERROR ->] - "): /some/file.xtb@2:39`); - }); - }); - }); + } });