diff --git a/goldens/public-api/localize/tools/index.md b/goldens/public-api/localize/tools/index.md index abfb7df8897a3..81ad7dcabda62 100644 --- a/goldens/public-api/localize/tools/index.md +++ b/goldens/public-api/localize/tools/index.md @@ -23,8 +23,6 @@ import { ɵSourceMessage } from '@angular/localize'; export class ArbTranslationParser implements TranslationParser { // (undocumented) analyze(_filePath: string, contents: string): ParseAnalysis; - // @deprecated (undocumented) - canParse(filePath: string, contents: string): ArbJsonObject | false; // (undocumented) parse(_filePath: string, contents: string, arb?: ArbJsonObject): ParsedTranslationBundle; } @@ -96,8 +94,6 @@ export class MessageExtractor { export class SimpleJsonTranslationParser implements TranslationParser { // (undocumented) analyze(filePath: string, contents: string): ParseAnalysis; - // @deprecated (undocumented) - canParse(filePath: string, contents: string): SimpleJsonFile | false; // (undocumented) parse(_filePath: string, contents: string, json?: SimpleJsonFile): ParsedTranslationBundle; } @@ -131,10 +127,8 @@ export function unwrapSubstitutionsFromLocalizeCall(call: NodePath { // (undocumented) analyze(filePath: string, contents: string): ParseAnalysis; - // @deprecated (undocumented) - canParse(filePath: string, contents: string): XmlTranslationParserHint | false; // (undocumented) - parse(filePath: string, contents: string, hint?: XmlTranslationParserHint): ParsedTranslationBundle; + parse(filePath: string, contents: string, hint: XmlTranslationParserHint): ParsedTranslationBundle; } // @public @@ -148,10 +142,8 @@ export class Xliff1TranslationSerializer implements TranslationSerializer { export class Xliff2TranslationParser implements TranslationParser { // (undocumented) analyze(filePath: string, contents: string): ParseAnalysis; - // @deprecated (undocumented) - canParse(filePath: string, contents: string): XmlTranslationParserHint | false; // (undocumented) - parse(filePath: string, contents: string, hint?: XmlTranslationParserHint): ParsedTranslationBundle; + parse(filePath: string, contents: string, hint: XmlTranslationParserHint): ParsedTranslationBundle; } // @public @@ -172,10 +164,8 @@ export class XmbTranslationSerializer implements TranslationSerializer { export class XtbTranslationParser implements TranslationParser { // (undocumented) analyze(filePath: string, contents: string): ParseAnalysis; - // @deprecated (undocumented) - canParse(filePath: string, contents: string): XmlTranslationParserHint | false; // (undocumented) - parse(filePath: string, contents: string, hint?: XmlTranslationParserHint): ParsedTranslationBundle; + parse(filePath: string, contents: string, hint: XmlTranslationParserHint): ParsedTranslationBundle; } // (No @packageDocumentation comment for this package) diff --git a/packages/localize/tools/src/translate/translation_files/translation_parsers/arb_translation_parser.ts b/packages/localize/tools/src/translate/translation_files/translation_parsers/arb_translation_parser.ts index 036030a6642e6..a85691d3fed70 100644 --- a/packages/localize/tools/src/translate/translation_files/translation_parsers/arb_translation_parser.ts +++ b/packages/localize/tools/src/translate/translation_files/translation_parsers/arb_translation_parser.ts @@ -53,14 +53,6 @@ export interface ArbLocation { * ``` */ export class ArbTranslationParser implements TranslationParser { - /** - * @deprecated - */ - canParse(filePath: string, contents: string): ArbJsonObject|false { - const result = this.analyze(filePath, contents); - return result.canParse && result.hint; - } - analyze(_filePath: string, contents: string): ParseAnalysis { const diagnostics = new Diagnostics(); if (!contents.includes('"@@locale"')) { diff --git a/packages/localize/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts b/packages/localize/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts index 47dbfd9e1084a..ad5acced7e687 100644 --- a/packages/localize/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts +++ b/packages/localize/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts @@ -34,14 +34,6 @@ interface SimpleJsonFile { * @publicApi used by CLI */ export class SimpleJsonTranslationParser implements TranslationParser { - /** - * @deprecated - */ - canParse(filePath: string, contents: string): SimpleJsonFile|false { - const result = this.analyze(filePath, contents); - return result.canParse && result.hint; - } - analyze(filePath: string, contents: string): ParseAnalysis { const diagnostics = new Diagnostics(); // For this to be parsable, the extension must be `.json` and the contents must include "locale" diff --git a/packages/localize/tools/src/translate/translation_files/translation_parsers/translation_parser.ts b/packages/localize/tools/src/translate/translation_files/translation_parsers/translation_parser.ts index 22145ebf33d50..698220081410f 100644 --- a/packages/localize/tools/src/translate/translation_files/translation_parsers/translation_parser.ts +++ b/packages/localize/tools/src/translate/translation_files/translation_parsers/translation_parser.ts @@ -44,33 +44,21 @@ export interface ParsedTranslationBundle { /** * Implement this interface to provide a class that can parse the contents of a translation file. * - * The `canParse()` method can return a hint that can be used by the `parse()` method to speed up - * parsing. This allows the parser to do significant work to determine if the file can be parsed + * The `analyze()` method can return a hint that can be used by the `parse()` method to speed + * up parsing. This allows the parser to do significant work to determine if the file can be parsed * without duplicating the work when it comes to actually parsing the file. * * Example usage: * * ``` * const parser: TranslationParser = getParser(); - * const result = parser.canParse(filePath, content); - * if (result) { - * return parser.parse(filePath, content, result); + * const analysis = parser.analyze(filePath, content); + * if (analysis.canParse) { + * return parser.parse(filePath, content, analysis.hint); * } * ``` */ export interface TranslationParser { - /** - * Can this parser parse the given file? - * - * @deprecated Use `analyze()` instead - * - * @param filePath The absolute path to the translation file. - * @param contents The contents of the translation file. - * @returns A hint, which can be used in doing the actual parsing, if the file can be parsed by - * this parser; false otherwise. - */ - canParse(filePath: string, contents: string): Hint|false; - /** * Analyze the file to see if this parser can parse the given file. * @@ -89,22 +77,10 @@ export interface TranslationParser { * @param filePath The absolute path to the translation file. * @param contents The contents of the translation file. * @param hint A value that can be used by the parser to speed up parsing of the file. This will - * have been provided as the return result from calling `canParse()`. + * have been provided as the return result from calling `analyze()`. * @returns The translation bundle parsed from the file. * @throws No errors. If there was a problem with parsing the bundle will contain errors * in the `diagnostics` property. */ parse(filePath: string, contents: string, hint: Hint): ParsedTranslationBundle; - /** - * Parses the given file, extracting the target locale and translations. - * - * @deprecated This overload is kept for backward compatibility. Going forward use the Hint - * returned from `canParse()` so that this method can avoid duplicating effort. - * - * @param filePath The absolute path to the translation file. - * @param contents The contents of the translation file. - * @returns The translation bundle parsed from the file. - * @throws An error if there was a problem parsing this file. - */ - parse(filePath: string, contents: string): ParsedTranslationBundle; } diff --git a/packages/localize/tools/src/translate/translation_files/translation_parsers/translation_utils.ts b/packages/localize/tools/src/translate/translation_files/translation_parsers/translation_utils.ts index c310da8459a7d..b9cb20424ba69 100644 --- a/packages/localize/tools/src/translate/translation_files/translation_parsers/translation_utils.ts +++ b/packages/localize/tools/src/translate/translation_files/translation_parsers/translation_utils.ts @@ -59,7 +59,7 @@ function getInnerRange(element: Element): LexerRange { } /** - * This "hint" object is used to pass information from `canParse()` to `parse()` for + * This "hint" object is used to pass information from `analyze()` to `parse()` for * `TranslationParser`s that expect XML contents. * * This saves the `parse()` method from having to re-parse the XML. diff --git a/packages/localize/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts b/packages/localize/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts index c70a56578aff5..db88970aaabd2 100644 --- a/packages/localize/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts +++ b/packages/localize/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts @@ -24,25 +24,13 @@ import {addErrorsToBundle, addParseDiagnostic, addParseError, canParseXml, getAt * @publicApi used by CLI */ export class Xliff1TranslationParser implements TranslationParser { - /** - * @deprecated - */ - canParse(filePath: string, contents: string): XmlTranslationParserHint|false { - const result = this.analyze(filePath, contents); - return result.canParse && result.hint; - } - analyze(filePath: string, contents: string): ParseAnalysis { return canParseXml(filePath, contents, 'xliff', {version: '1.2'}); } - parse(filePath: string, contents: string, hint?: XmlTranslationParserHint): + parse(filePath: string, contents: string, hint: XmlTranslationParserHint): ParsedTranslationBundle { - if (hint) { - return this.extractBundle(hint); - } else { - return this.extractBundleDeprecated(filePath, contents); - } + return this.extractBundle(hint); } private extractBundle({element, errors}: XmlTranslationParserHint): ParsedTranslationBundle { @@ -89,28 +77,6 @@ export class Xliff1TranslationParser implements TranslationParser { - /** - * @deprecated - */ - canParse(filePath: string, contents: string): XmlTranslationParserHint|false { - const result = this.analyze(filePath, contents); - return result.canParse && result.hint; - } - analyze(filePath: string, contents: string): ParseAnalysis { return canParseXml(filePath, contents, 'xliff', {version: '2.0'}); } - parse(filePath: string, contents: string, hint?: XmlTranslationParserHint): + parse(filePath: string, contents: string, hint: XmlTranslationParserHint): ParsedTranslationBundle { - if (hint) { - return this.extractBundle(hint); - } else { - return this.extractBundleDeprecated(filePath, contents); - } + return this.extractBundle(hint); } private extractBundle({element, errors}: XmlTranslationParserHint): ParsedTranslationBundle { @@ -67,20 +55,6 @@ export class Xliff2TranslationParser implements TranslationParser { - /** - * @deprecated - */ - canParse(filePath: string, contents: string): XmlTranslationParserHint|false { - const result = this.analyze(filePath, contents); - return result.canParse && result.hint; - } - analyze(filePath: string, contents: string): ParseAnalysis { const extension = extname(filePath); if (extension !== '.xtb' && extension !== '.xmb') { @@ -43,13 +35,9 @@ export class XtbTranslationParser implements TranslationParser { jsonParser = new SimpleJsonTranslationParser(); }); - it('should call `canParse()` and `parse()` for each file', () => { + it('should call `analyze()` and `parse()` for each file', () => { const diagnostics = new Diagnostics(); const parser = new MockTranslationParser(alwaysCanParse, 'fr'); const loader = new TranslationLoader(fs, [parser], 'error', diagnostics); @@ -219,11 +219,6 @@ runInEachFileSystem(() => { private _canParse: (filePath: string) => boolean, private _locale?: string, private _translations: Record = {}) {} - canParse(filePath: string, fileContents: string) { - const result = this.analyze(filePath, fileContents); - return result.canParse && result.hint; - } - analyze(filePath: string, fileContents: string): ParseAnalysis { const diagnostics = new Diagnostics(); diagnostics.warn('This is a mock failure warning.'); diff --git a/packages/localize/tools/test/translate/translation_files/translation_parsers/arb_translation_parser_spec.ts b/packages/localize/tools/test/translate/translation_files/translation_parsers/arb_translation_parser_spec.ts index 54778105a95f1..649ce4f2fe66a 100644 --- a/packages/localize/tools/test/translate/translation_files/translation_parsers/arb_translation_parser_spec.ts +++ b/packages/localize/tools/test/translate/translation_files/translation_parsers/arb_translation_parser_spec.ts @@ -6,18 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵmakeTemplateObject} from '@angular/localize'; + import {ArbTranslationParser} from '../../../../src/translate/translation_files/translation_parsers/arb_translation_parser'; describe('SimpleArbTranslationParser', () => { - describe('canParse()', () => { + describe('analyze()', () => { it('should return true if the file extension is `.json` and contains `@@locale` property', () => { const parser = new ArbTranslationParser(); - expect(parser.canParse('/some/file.xlf', '')).toBe(false); - expect(parser.canParse('/some/file.json', 'xxx')).toBe(false); - expect(parser.canParse('/some/file.json', '{ "someKey": "someValue" }')).toBe(false); - expect(parser.canParse('/some/file.json', '{ "@@locale": "en", "someKey": "someValue" }')) - .toBeTruthy(); + expect(parser.analyze('/some/file.xlf', '').canParse).toBeFalse(); + expect(parser.analyze('/some/file.json', 'xxx').canParse).toBeFalse(); + expect(parser.analyze('/some/file.json', '{ "someKey": "someValue" }').canParse) + .toBeFalse(); + expect(parser.analyze('/some/file.json', '{ "@@locale": "en", "someKey": "someValue" }') + .canParse) + .toBeTrue(); }); }); diff --git a/packages/localize/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts b/packages/localize/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts index 20567a5465ef8..82f98a8ff408d 100644 --- a/packages/localize/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts +++ b/packages/localize/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts @@ -6,20 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ 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()', () => { + describe('analyze()', () => { it('should return true if the file extension is `.json` and contains top level `locale` and `translations` properties', () => { const parser = new SimpleJsonTranslationParser(); - expect(parser.canParse('/some/file.xlf', '')).toBe(false); - expect(parser.canParse('/some/file.json', '{}')).toBe(false); - expect(parser.canParse('/some/file.json', '{ "translations" : {} }')).toBe(false); - expect(parser.canParse('/some/file.json', '{ "locale" : "fr" }')).toBe(false); - expect(parser.canParse('/some/file.json', '{ "locale" : "fr", "translations" : {}}')) - .toBeTruthy(); + expect(parser.analyze('/some/file.xlf', '').canParse).toBeFalse(); + expect(parser.analyze('/some/file.json', '{}').canParse).toBeFalse(); + expect(parser.analyze('/some/file.json', '{ "translations" : {} }').canParse).toBeFalse(); + expect(parser.analyze('/some/file.json', '{ "locale" : "fr" }').canParse).toBeFalse(); + expect( + parser.analyze('/some/file.json', '{ "locale" : "fr", "translations" : {}}').canParse) + .toBeTrue(); }); }); @@ -46,42 +48,38 @@ describe('SimpleJsonTranslationParser', () => { }); }); - 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); - }; + describe(`parse()`, () => { + const doParse: (fileName: string, contents: string) => ParsedTranslationBundle = + (fileName, contents) => { + const parser = new SimpleJsonTranslationParser(); + const analysis = parser.analyze(fileName, contents); + if (!analysis.canParse) { + throw new Error('expected contents to be valid'); + } + return parser.parse(fileName, contents, analysis.hint); + }; - 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', `{ + 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}!': { - text: 'Bonjour, {$ph_1}!', - messageParts: ɵmakeTemplateObject(['Bonjour, ', '!'], ['Bonjour, ', '!']), - placeholderNames: ['ph_1'] - }, - }); + expect(result.translations).toEqual({ + 'Hello, {$ph_1}!': { + text: 'Bonjour, {$ph_1}!', + messageParts: ɵmakeTemplateObject(['Bonjour, ', '!'], ['Bonjour, ', '!']), + placeholderNames: ['ph_1'] + }, }); }); - } + }); }); diff --git a/packages/localize/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts b/packages/localize/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts index 4eea42a389cb5..6ae17f2e261f2 100644 --- a/packages/localize/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts +++ b/packages/localize/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts @@ -6,793 +6,781 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; + import {ParseAnalysis, ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; import {Xliff1TranslationParser} from '../../../../src/translate/translation_files/translation_parsers/xliff1_translation_parser'; -describe( - 'Xliff1TranslationParser', () => { - describe('canParse()', () => { - it('should return true only if the file contains an element with version="1.2" attribute', - () => { - const parser = new Xliff1TranslationParser(); - expect(parser.canParse( +describe('Xliff1TranslationParser', () => { + describe('analyze()', () => { + it('should return true only if the file contains an element with version="1.2" attribute', + () => { + const parser = new Xliff1TranslationParser(); + expect(parser + .analyze( '/some/file.xlf', - '')) - .toBeTruthy(); - expect(parser.canParse( + '') + .canParse) + .toBeTrue(); + expect(parser + .analyze( '/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); - }); - }); + '') + .canParse) + .toBeTrue(); + expect(parser.analyze('/some/file.xliff', '').canParse).toBeTrue(); + expect(parser.analyze('/some/file.json', '').canParse).toBeTrue(); + expect(parser.analyze('/some/file.xlf', '').canParse).toBeFalse(); + expect(parser.analyze('/some/file.xlf', '').canParse).toBeFalse(); + expect(parser.analyze('/some/file.xlf', '').canParse).toBeFalse(); + expect(parser.analyze('/some/file.json', '').canParse).toBeFalse(); + }); + }); + + describe('analyze()', () => { + it('should return a success object if the file contains an element with version="1.2" attribute', + () => { + const parser = new Xliff1TranslationParser(); + expect(parser.analyze('/some/file.xlf', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.json', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.xliff', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.json', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + }); + + it('should return a failure object if the file cannot be parsed as XLIFF 1.2', () => { + const parser = new Xliff1TranslationParser(); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.xlf', '')) + .toEqual(jasmine.objectContaining({canParse: false})); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.json', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + }); - describe('analyze()', () => { - it('should return a success object if the file contains an element with version="1.2" attribute', - () => { - const parser = new Xliff1TranslationParser(); - expect(parser.analyze('/some/file.xlf', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - expect(parser.analyze('/some/file.json', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - expect(parser.analyze('/some/file.xliff', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - expect(parser.analyze('/some/file.json', '')) - .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); - }); - - it('should return a failure object if the file cannot be parsed as XLIFF 1.2', () => { - const parser = new Xliff1TranslationParser(); - expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ - canParse: false - })); - expect(parser.analyze('/some/file.xlf', '')) - .toEqual(jasmine.objectContaining({canParse: false})); - expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ - canParse: false - })); - expect(parser.analyze('/some/file.json', '')).toEqual(jasmine.objectContaining({ - canParse: false - })); - }); - - it('should return a diagnostics object when the file is not a valid format', () => { - let result: ParseAnalysis; + it('should return a diagnostics object when the file is not a valid format', () => { + let result: ParseAnalysis; + const parser = new Xliff1TranslationParser(); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([ + {type: 'warning', message: 'The XML file does not contain a root node.'} + ]); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([{ + type: 'warning', + message: + 'The node does not have the required attribute: version="1.2". ("[WARNING ->]"): /some/file.xlf@0:0' + }]); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([{ + type: 'error', + message: + 'Unexpected closing tag "file". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]"): /some/file.xlf@0:21' + }]); + }); + }); + + describe(`parse()`, () => { + const doParse: (fileName: string, XLIFF: string) => ParsedTranslationBundle = + (fileName, XLIFF) => { const parser = new Xliff1TranslationParser(); + const analysis = parser.analyze(fileName, XLIFF); + if (!analysis.canParse) { + throw new Error('expected XLIFF to be valid'); + } + return parser.parse(fileName, XLIFF, analysis.hint); + }; + + const expectToFail: + (fileName: string, XLIFF: string, errorMatcher: RegExp, diagnosticMessage: string) => void = + (fileName, XLIFF, _errorMatcher, diagnosticMessage) => { + const result = doParse(fileName, XLIFF); + expect(result.diagnostics.messages.length).toBeGreaterThan(0); + expect(result.diagnostics.messages.pop()!.message).toEqual(diagnosticMessage); + }; + + it('should extract the locale from the last `` element to contain a `target-language` attribute', + () => { + const XLIFF = ` + + + + + + + + + + + + + + + + + + `; + const result = doParse('/some/file.xlf', XLIFF); + expect(result.locale).toEqual('de'); + }); + + it('should return an undefined locale if there is no locale in the file', () => { + const XLIFF = ` + + + + + + + `; + 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 + + + + + + `; + const result = doParse('/some/file.xlf', XLIFF); + + expect(result.translations[ɵcomputeMsgId('translatable attribute')]) + .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); + }); + + it('should extract translations with simple placeholders', () => { + /** + * Source HTML: + * + * ``` + *
translatable element with placeholders {{ interpolation}}
+ * ``` + */ + const XLIFF = ` + + + + + translatable element with placeholders + tnemele elbatalsnart sredlohecalp htiw + + file.ts + 2 + + + + + + `; + 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'])); + }); - result = parser.analyze('/some/file.xlf', ''); - expect(result.diagnostics.messages).toEqual([ - {type: 'warning', message: 'The XML file does not contain a root node.'} - ]); - - result = parser.analyze('/some/file.xlf', ''); - expect(result.diagnostics.messages).toEqual([{ - type: 'warning', - message: - 'The node does not have the required attribute: version="1.2". ("[WARNING ->]"): /some/file.xlf@0:0' - }]); - - result = parser.analyze('/some/file.xlf', '
'); - expect(result.diagnostics.messages).toEqual([{ - type: 'error', - message: - 'Unexpected closing tag "file". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]"): /some/file.xlf@0:21' - }]); - }); + it('should extract nested placeholder containers (i.e. nested HTML elements)', () => { + /** + * Source HTML: + * + * ``` + *
+ * translatable element with placeholders {{ interpolation}} + *
+ * ``` + */ + const XLIFF = ` + + + + + translatable element with placeholders + tnemele elbatalsnart sredlohecalp htiw + + file.ts + 3 + + + + + + `; + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId( + 'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' + + '{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [ + 'START_TAG_SPAN', + 'INTERPOLATION', + 'CLOSE_TAG_SPAN', + 'START_BOLD_TEXT', + 'CLOSE_BOLD_TEXT', + ])); + }); + + it('should extract translations with placeholders containing hyphens', () => { + /** + * Source HTML: + * + * ``` + *
Welcome
+ * ``` + */ + const XLIFF = ` + + + + + Welcome + + src/app/app.component.html + 1 + + Translate + + + + + `; + const result = doParse('/some/file.xlf', XLIFF); + const id = + ɵcomputeMsgId('{$START_TAG_APP_MY_COMPONENT}{$CLOSE_TAG_APP_MY_COMPONENT} Welcome'); + expect(result.translations[id]).toEqual(ɵmakeParsedTranslation(['', '', ' Translate'], [ + 'START_TAG_APP_MY_COMPONENT', 'CLOSE_TAG_APP_MY_COMPONENT' + ])); + }); + + it('should extract translations with simple ICU expressions', () => { + /** + * Source HTML: + * + * ``` + *
{VAR_PLURAL, plural, =0 {

test

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

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

deeply nested

}} } + * =other {a lot}} + * ``` + * + * Note that the message gets split into two translation units: + * * The first one contains the outer message with an `ICU` placeholder + * * The second one is the ICU expansion itself + * + * Note that special markers `VAR_PLURAL` and `VAR_SELECT` are added, which are then + * replaced by IVY at runtime with the actual values being rendered by the ICU + * expansion. + */ + const XLIFF = ` + + + + + Test: + Le test: + + file.ts + 11 + + + + {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {deeply nested}}} =other {a lot}} + {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {profondément imbriqué}}} =other {beaucoup}} + + + + + `; + const result = doParse('/some/file.xlf', XLIFF); + + expect(result.translations[ɵcomputeMsgId('Test: {$ICU}')]) + .toEqual(ɵmakeParsedTranslation(['Le test: ', ''], ['ICU'])); + + expect( + result.translations[ɵcomputeMsgId( + '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}deeply nested{CLOSE_PARAGRAPH}}}} =other {beaucoup}}')]) + .toEqual(ɵmakeParsedTranslation([ + '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}profondément imbriqué{CLOSE_PARAGRAPH}}}} =other {beaucoup}}' + ])); + }); + + it('should extract translations containing multiple lines', () => { + /** + * Source HTML: + * + * ``` + *
multi + * lines
+ * ``` + */ + const XLIFF = ` + + + + + multi\nlines + multi\nlignes + + file.ts + 12 + + + + + + `; + 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. + + + + + `; + 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 + + + + + + `; + + 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 + + + + + + `; + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('translatable attribute')]) + .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); + expect( + result.translations[ɵcomputeMsgId( + 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], + ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); + }); + + describe('[structure errors]', () => { + it('should warn when a trans-unit has no translation target but does have a source', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + const result = doParse('/some/file.xlf', XLIFF); + expect(result.diagnostics.messages.length).toEqual(1); + expect(result.diagnostics.messages[0].message).toEqual([ + `Missing element ("e-language="en" target-language="fr" datatype="plaintext" original="ng2.template">`, + ` `, + ` [WARNING ->]`, + ` `, + ` `, + `"): /some/file.xlf@4:6`, + ].join('\n')); }); - for (const withHint of [true, false]) { - describe( - `parse() [${withHint ? 'with' : 'without'} hint]`, () => { - const doParse: (fileName: string, XLIFF: string) => ParsedTranslationBundle = - withHint ? (fileName, XLIFF) => { - const parser = new Xliff1TranslationParser(); - const hint = parser.canParse(fileName, XLIFF); - if (!hint) { - throw new Error('expected XLIFF to be valid'); - } - return parser.parse(fileName, XLIFF, hint); - } : (fileName, XLIFF) => { - const parser = new Xliff1TranslationParser(); - return parser.parse(fileName, XLIFF); - }; - - const expectToFail: ( - fileName: string, XLIFF: string, errorMatcher: RegExp, - diagnosticMessage: string) => void = - withHint ? (fileName, XLIFF, _errorMatcher, diagnosticMessage) => { - const result = doParse(fileName, XLIFF); - expect(result.diagnostics.messages.length).toBeGreaterThan(0); - expect(result.diagnostics.messages.pop()!.message).toEqual(diagnosticMessage); - } : (fileName, XLIFF, errorMatcher, _diagnosticMessage) => { - expect(() => doParse(fileName, XLIFF)).toThrowError(errorMatcher); - }; - - it('should extract the locale from the last `` element to contain a `target-language` attribute', - () => { - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.locale).toEqual('de'); - }); - - it('should return an undefined locale if there is no locale in the file', () => { - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect(result.locale).toBeUndefined(); - }); - - it('should extract basic messages', () => { - /** - * Source HTML: - * - * ``` - *
translatable attribute
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` translatable attribute`, - ` etubirtta elbatalsnart`, - ` `, - ` file.ts`, - ` 1`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect(result.translations[ɵcomputeMsgId('translatable attribute')]) - .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); - }); - - it('should extract translations with simple placeholders', () => { - /** - * Source HTML: - * - * ``` - *
translatable element with placeholders {{ interpolation}}
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` translatable element with placeholders `, - ` tnemele elbatalsnart sredlohecalp htiw`, - ` `, - ` file.ts`, - ` 2`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - - expect( - result.translations[ɵcomputeMsgId( - 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) - .toEqual(ɵmakeParsedTranslation( - ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], - ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); - }); - - it('should extract nested placeholder containers (i.e. nested HTML elements)', () => { - /** - * Source HTML: - * - * ``` - *
- * translatable element with placeholders {{ interpolation}} - *
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` translatable element with placeholders `, - ` tnemele elbatalsnart sredlohecalp htiw`, - ` `, - ` file.ts`, - ` 3`, - ` `, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - expect( - result.translations[ɵcomputeMsgId( - 'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' + - '{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')]) - .toEqual(ɵmakeParsedTranslation( - ['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [ - 'START_TAG_SPAN', - 'INTERPOLATION', - 'CLOSE_TAG_SPAN', - 'START_BOLD_TEXT', - 'CLOSE_BOLD_TEXT', - ])); - }); - - it('should extract translations with placeholders containing hyphens', () => { - /** - * Source HTML: - * - * ``` - *
Welcome
- * ``` - */ - const XLIFF = [ - ``, - ` `, - ` `, - ` `, - ` Welcome`, - ` `, - ` src/app/app.component.html`, - ` 1`, - ` `, - ` Translate`, - ` `, - ` `, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xlf', XLIFF); - const id = ɵcomputeMsgId( - '{$START_TAG_APP_MY_COMPONENT}{$CLOSE_TAG_APP_MY_COMPONENT} Welcome'); - expect(result.translations[id]) - .toEqual(ɵmakeParsedTranslation( - ['', '', ' Translate'], - ['START_TAG_APP_MY_COMPONENT', 'CLOSE_TAG_APP_MY_COMPONENT'])); - }); - - it('should extract translations with simple ICU expressions', () => { - /** - * Source HTML: - * - * ``` - *
{VAR_PLURAL, plural, =0 {

test

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

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

deeply nested

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

test

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

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

deeply nested

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

test

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

+ * ``` + */ + const XLIFF = ` + + + + + ph names + file.ts:7 + + + + + + + + + `; + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')]) + .toEqual( + ɵmakeParsedTranslation(['', '', '', ''], ['TAG_IMG_1', 'TAG_IMG', 'LINE_BREAK'])); + }); + + it('should extract translations with empty target', () => { + /** + * Source HTML: + * + * ``` + *
hello
+ * ``` + */ + const XLIFF = ` + + + + + empty element + file.ts:8 + + + hello + + + + + + `; + 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}} + + + + + `; + 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 + + + + + `; + 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. + + + + + `; + 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 + + + + + `; + const result = doParse('/some/file.xlf', XLIFF); + expect(result.translations[ɵcomputeMsgId('translatable attribute', '')]) + .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart'])); + expect( + result.translations[ɵcomputeMsgId( + 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], + ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); + }); + + describe('[structure errors]', () => { + it('should provide a diagnostic warning when a trans-unit has no translation target but does have a source', + () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + const result = doParse('/some/file.xlf', XLIFF); + expect(result.diagnostics.messages.length).toEqual(1); + expect(result.diagnostics.messages[0].message).toEqual([ + `Missing element ("`, + ` `, + ` `, + ` [WARNING ->]`, + ` `, + ` `, + `"): /some/file.xlf@4:6`, + ].join('\n')); + }); + + it('should provide a diagnostic error when a trans-unit has no translation target nor source', + () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail( + '/some/file.xlf', XLIFF, + /Missing required element: one of or is required/, [ + `Missing required element: one of or is required ("`, + ` `, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + `"): /some/file.xlf@4:6`, + ].join('\n')); + }); + + + it('should provide a diagnostic error when a trans-unit has no id attribute', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Missing required "id" attribute/, [ + `Missing required "id" attribute on element. ("s:tc:xliff:document:2.0" srcLang="en" trgLang="fr">`, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + `"): /some/file.xlf@3:4`, + ].join('\n')); + }); + + it('should provide a diagnostic error on duplicate trans-unit id', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Duplicated translations for message "deadbeef"/, [ + `Duplicated translations for message "deadbeef" ("`, + ` `, + ` `, + ` [ERROR ->]`, + ` `, + ` `, + '"): /some/file.xlf@9:4', + ].join('\n')); + }); + }); + + describe('[message errors]', () => { + it('should provide a diagnostic error on unknown message tags', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` msg should contain only ph and pc tags`, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Invalid element found in message/, [ + `Error: Invalid element found in message.`, + `At /some/file.xlf@6:16:`, + `...`, + ` `, + ` [ERROR ->]msg should contain only ph and pc tags`, + ` `, + `...`, + ``, + ].join('\n')); + }); + + it('should provide a diagnostic error when a placeholder misses an id attribute', () => { + const XLIFF = [ + ``, + ``, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ``, + ].join('\n'); + + expectToFail('/some/file.xlf', XLIFF, /Missing required "equiv" attribute/, [ + `Error: Missing required "equiv" attribute:`, + `At /some/file.xlf@6:16:`, + `...`, + ` `, + ` [ERROR ->]`, + ` `, + `...`, + ``, + ].join('\n')); + }); + }); + }); }); diff --git a/packages/localize/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts b/packages/localize/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts index bc91e87d8e80b..e3a38d560dece 100644 --- a/packages/localize/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts +++ b/packages/localize/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts @@ -6,21 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; -import {ParseAnalysis, ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; + +import {ParseAnalysis, ParsedTranslationBundle,} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; import {XtbTranslationParser} from '../../../../src/translate/translation_files/translation_parsers/xtb_translation_parser'; describe('XtbTranslationParser', () => { - describe('canParse()', () => { + describe('analyze()', () => { it('should return true if the file extension is `.xtb` or `.xmb` and it contains the `` tag', () => { const parser = new XtbTranslationParser(); - expect(parser.canParse('/some/file.xtb', '')).toBeTruthy(); - expect(parser.canParse('/some/file.xmb', '')).toBeTruthy(); - expect(parser.canParse('/some/file.xtb', '')).toBeTruthy(); - expect(parser.canParse('/some/file.xmb', '')).toBeTruthy(); - expect(parser.canParse('/some/file.json', '')).toBe(false); - expect(parser.canParse('/some/file.xmb', '')).toBe(false); - expect(parser.canParse('/some/file.xtb', '')).toBe(false); + expect(parser.analyze('/some/file.xtb', '').canParse).toBeTrue(); + expect(parser.analyze('/some/file.xmb', '').canParse).toBeTrue(); + expect(parser.analyze('/some/file.xtb', '').canParse) + .toBeTrue(); + expect(parser.analyze('/some/file.xmb', '').canParse) + .toBeTrue(); + expect(parser.analyze('/some/file.json', '').canParse).toBeFalse(); + expect(parser.analyze('/some/file.xmb', '').canParse).toBeFalse(); + expect(parser.analyze('/some/file.xtb', '').canParse).toBeFalse(); }); }); @@ -43,10 +46,10 @@ describe('XtbTranslationParser', () => { expect(parser.analyze('/some/file.json', '')) .toEqual(jasmine.objectContaining({canParse: false})); expect(parser.analyze('/some/file.xmb', '')).toEqual(jasmine.objectContaining({ - canParse: false + canParse: false, })); expect(parser.analyze('/some/file.xtb', '')).toEqual(jasmine.objectContaining({ - canParse: false + canParse: false, })); }); @@ -56,188 +59,183 @@ describe('XtbTranslationParser', () => { results = parser.analyze('/some/file.xtb', ''); expect(results.diagnostics.messages).toEqual([ - {type: 'warning', message: 'The XML file does not contain a root node.'} + { + type: 'warning', + message: 'The XML file does not contain a root node.', + }, ]); results = parser.analyze('/some/file.xtb', ''); - expect(results.diagnostics.messages).toEqual([{ - type: 'error', - message: - 'Unexpected closing tag "translation". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]"): /some/file.xtb@0:19' - }]); + expect(results.diagnostics.messages).toEqual([ + { + type: 'error', + message: + 'Unexpected closing tag "translation". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]"): /some/file.xtb@0:19', + }, + ]); }); }); - 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); - }; - - const expectToFail: - (fileName: string, XLIFF: string, errorMatcher: RegExp, diagnosticMessage: string) => - void = withHint ? (fileName, XLIFF, _errorMatcher, diagnosticMessage) => { - const result = doParse(fileName, XLIFF); - expect(result.diagnostics.messages.length).toEqual(1); - expect(result.diagnostics.messages[0].message).toEqual(diagnosticMessage); - } : (fileName, XLIFF, errorMatcher, _diagnosticMessage) => { - expect(() => doParse(fileName, XLIFF)).toThrowError(errorMatcher); - }; - - it('should extract the locale from the file contents', () => { - const XTB = [ - ``, - ``, - ` rab`, - ``, - ].join('\n'); - const result = doParse('/some/file.xtb', XTB); - expect(result.locale).toEqual('fr'); - }); - - it('should extract basic messages', () => { - const XTB = [ - ``, - ``, - ` `, - ``, - ` `, - ` `, - ``, - ` `, - ` `, - `]>`, - ``, - ` rab`, - ``, - ].join('\n'); - const result = doParse('/some/file.xtb', XTB); - - expect(result.translations['8841459487341224498']).toEqual(ɵmakeParsedTranslation(['rab'])); - }); - - it('should extract translations with simple placeholders', () => { - const XTB = [ - ``, - ``, - ` rab`, - ``, - ].join('\n'); - const result = doParse('/some/file.xtb', XTB); - - expect(result.translations['8877975308926375834']) - .toEqual( - ɵmakeParsedTranslation(['', 'rab', ''], ['START_PARAGRAPH', 'CLOSE_PARAGRAPH'])); - }); - - it('should extract nested placeholder containers (i.e. nested HTML elements)', () => { - /** - * Source HTML: - * - * ``` - *
- * translatable element with placeholders {{ interpolation}} - *
- * ``` - */ - const XLIFF = [ - ``, - ``, - ` ` + - ` tnemele elbatalsnart sredlohecalp htiw` + - ``, - ``, - ].join('\n'); - const result = doParse('/some/file.xtb', XLIFF); - expect(result.translations[ɵcomputeMsgId( - 'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' + - '{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')]) - .toEqual(ɵmakeParsedTranslation( - ['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [ - 'START_TAG_SPAN', - 'INTERPOLATION', - 'CLOSE_TAG_SPAN', - 'START_BOLD_TEXT', - 'CLOSE_BOLD_TEXT', - ])); - }); - - it('should extract translations with simple ICU expressions', () => { - const XTB = [ - ``, - ``, - ` **`, - ` {VAR_PLURAL, plural, =1 {rab}}`, - ``, - ].join('\n'); - const result = doParse('/some/file.xtb', XTB); + describe(`parse()`, () => { + const doParse: (fileName: string, XTB: string) => ParsedTranslationBundle = (fileName, XTB) => { + const parser = new XtbTranslationParser(); + const analysis = parser.analyze(fileName, XTB); + if (!analysis.canParse) { + throw new Error('expected XTB to be valid'); + } + return parser.parse(fileName, XTB, analysis.hint); + }; + + const expectToFail: + (fileName: string, XLIFF: string, errorMatcher: RegExp, diagnosticMessage: string) => void = + (fileName, XLIFF, _errorMatcher, diagnosticMessage) => { + const result = doParse(fileName, XLIFF); + expect(result.diagnostics.messages.length).toEqual(1); + expect(result.diagnostics.messages[0].message).toEqual(diagnosticMessage); + }; + + it('should extract the locale from the file contents', () => { + const XTB = ` + + + rab + + `; + const result = doParse('/some/file.xtb', XTB); + expect(result.locale).toEqual('fr'); + }); - 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 basic messages', () => { + const XTB = ` + + + + + + + + ]> + + rab + + `; + const result = doParse('/some/file.xtb', XTB); + + expect(result.translations['8841459487341224498']).toEqual(ɵmakeParsedTranslation(['rab'])); + }); - it('should extract translations with duplicate source messages', () => { - const XTB = [ - ``, - ` oof`, - ` toto`, - ` tata`, - ``, - ].join('\n'); - const result = doParse('/some/file.xtb', XTB); + it('should extract translations with simple placeholders', () => { + const XTB = ` + + + rab + + `; + const result = doParse('/some/file.xtb', XTB); + + expect(result.translations['8877975308926375834']) + .toEqual(ɵmakeParsedTranslation(['', 'rab', ''], ['START_PARAGRAPH', 'CLOSE_PARAGRAPH'])); + }); - 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 nested placeholder containers (i.e. nested HTML elements)', () => { + /** + * Source HTML: + * + * ``` + *
+ * translatable element with placeholders {{ interpolation}} + *
+ * ``` + */ + const XLIFF = [ + ``, + ``, + ` ` + + ` tnemele elbatalsnart sredlohecalp htiw` + + ``, + ``, + ].join('\n'); + const result = doParse('/some/file.xtb', XLIFF); + expect(result.translations[ɵcomputeMsgId( + 'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' + + '{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')]) + .toEqual(ɵmakeParsedTranslation( + ['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [ + 'START_TAG_SPAN', + 'INTERPOLATION', + 'CLOSE_TAG_SPAN', + 'START_BOLD_TEXT', + 'CLOSE_BOLD_TEXT', + ])); + }); - it('should extract translations with only placeholders, which are re-ordered', () => { - const XTB = [ - ``, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xtb', XTB); + it('should extract translations with simple ICU expressions', () => { + const XTB = ` + + + ** + {VAR_PLURAL, plural, =1 {rab}} + + `; + const result = doParse('/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}}}'], [])); + }); - 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 duplicate source messages', () => { + const XTB = ` + + oof + toto + tata + + `; + const result = doParse('/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 empty target', () => { - /** - * Source HTML: - * - * ``` - *
hello
- * ``` - */ - const XTB = [ - ``, - ` `, - ``, - ].join('\n'); - const result = doParse('/some/file.xtb', XTB); + it('should extract translations with only placeholders, which are re-ordered', () => { + const XTB = ` + , + + + `; + 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'])); + }); - expect(result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')]) - .toEqual(ɵmakeParsedTranslation([''])); - }); + it('should extract translations with empty target', () => { + /** + * Source HTML: + * + * ``` + *
hello
+ * ``` + */ + const XTB = ` + + + + `; + 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', () => { - /** + it('should extract translations with deeply nested ICUs', () => { + /** * Source HTML: * * ``` @@ -252,157 +250,156 @@ describe('XtbTranslationParser', () => { * 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 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 + + `; + 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?`, + `Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.) ("id">{REGION_COUNT_1, plural, =0 {unused plural form} =1 {1 region} other {{REGION_COUNT_2} regions}}[ERROR ->]`, + `
"): /some/file.xtb@2:124`, + `Invalid ICU message. Missing '}'. ("n>`, + ` {REGION_COUNT_1, plural, =0 {unused plural form} =1 {1 region} other [ERROR ->]{{REGION_COUNT_2} regions}}`, + `
"): /some/file.xtb@2:97`, + ].join('\n'), + }); + }); + + 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 = [ ``, - ` 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}}' - ])); + 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 extract translations containing multiple lines', () => { - /** - * Source HTML: - * - * ``` - *
multi - * lines
- * ``` - */ + it('should throw on duplicate translation id', () => { const XTB = [ ``, - ` multi\nlignes`, + ` `, + ` `, ``, ].join('\n'); - const result = doParse('/some/file.xtb', XTB); - expect(result.translations[ɵcomputeMsgId('multi\nlines')]) - .toEqual(ɵmakeParsedTranslation(['multi\nlignes'])); + expectToFail('/some/file.xtb', XTB, /Duplicated translations for message "deadbeef"/, [ + `Duplicated translations for message "deadbeef" ("`, + ` `, + ` [ERROR ->]`, + `"): /some/file.xtb@2:2`, + ].join('\n')); }); + }); - it('should warn on unrecognised ICU messages', () => { - // See https://github.com/angular/angular/issues/14046 - + describe('[message errors]', () => { + it('should throw on unknown message tags', () => { 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?`, - `Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.) ("id">{REGION_COUNT_1, plural, =0 {unused plural form} =1 {1 region} other {{REGION_COUNT_2} regions}}[ERROR ->]
`, - `
"): /some/file.xtb@2:124`, - `Invalid ICU message. Missing '}'. ("n>`, - ` {REGION_COUNT_1, plural, =0 {unused plural form} =1 {1 region} other [ERROR ->]{{REGION_COUNT_2} regions}}`, - `
"): /some/file.xtb@2:97`, - ].join('\n') - }); + expectToFail('/some/file.xtb', XTB, /Invalid element found in message/, [ + `Error: Invalid element found in message.`, + `At /some/file.xtb@2:4:`, + `...`, + ` `, + ` [ERROR ->]`, + ` `, + `...`, + ``, + ].join('\n')); }); - 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')); - }); - }); + it('should throw when a placeholder misses a name attribute', () => { + const XTB = [ + ``, + ` `, + ``, + ].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/, [ - `Error: Invalid element found in message.`, - `At /some/file.xtb@2:4:`, - `...`, - ` `, - ` [ERROR ->]`, - ` `, - `...`, - ``, - ].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, [ - `Error: Missing required "name" attribute:`, - `At /some/file.xtb@1:29:`, - `...`, - ` [ERROR ->]`, - `...`, - ``, - ].join('\n')); - }); + expectToFail('/some/file.xtb', XTB, /required "name" attribute/gi, [ + `Error: Missing required "name" attribute:`, + `At /some/file.xtb@1:29:`, + `...`, + ` [ERROR ->]`, + `...`, + ``, + ].join('\n')); }); }); - } + }); });