diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts index 1e71e45bcb..8323c0821a 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts @@ -9,7 +9,7 @@ import {AbsoluteFsPath, FileSystem} from '@angular/compiler-cli/src/ngtsc/file_s import {DiagnosticHandlingStrategy, Diagnostics} from '../../diagnostics'; import {TranslationBundle} from '../translator'; -import {TranslationParser} from './translation_parsers/translation_parser'; +import {ParseAnalysis, TranslationParser} from './translation_parsers/translation_parser'; /** * Use this class to load a collection of translation files from disk. @@ -57,14 +57,16 @@ export class TranslationLoader { private loadBundle(filePath: AbsoluteFsPath, providedLocale: string|undefined): TranslationBundle { const fileContents = this.fs.readFile(filePath); + const unusedParsers = new Map, ParseAnalysis>(); for (const translationParser of this.translationParsers) { - const result = translationParser.canParse(filePath, fileContents); - if (!result) { + const result = translationParser.analyze(filePath, fileContents); + if (!result.canParse) { + unusedParsers.set(translationParser, result); continue; } const {locale: parsedLocale, translations, diagnostics} = - translationParser.parse(filePath, fileContents, result); + translationParser.parse(filePath, fileContents, result.hint); if (diagnostics.hasErrors) { throw new Error(diagnostics.formatDiagnostics( `The translation file "${filePath}" could not be parsed.`)); @@ -90,13 +92,20 @@ export class TranslationLoader { return {locale, translations, diagnostics}; } + + const diagnosticsMessages: string[] = []; + for (const [parser, result] of unusedParsers.entries()) { + diagnosticsMessages.push(result.diagnostics.formatDiagnostics( + `\n${parser.constructor.name} cannot parse translation file.`)); + } throw new Error( - `There is no "TranslationParser" that can parse this translation file: ${filePath}.`); + `There is no "TranslationParser" that can parse this translation file: ${filePath}.` + + diagnosticsMessages.join('\n')); } /** - * There is more than one `filePath` for this locale, so load each as a bundle and then merge them - * all together. + * There is more than one `filePath` for this locale, so load each as a bundle and then merge + * them all together. */ private mergeBundles(filePaths: AbsoluteFsPath[], providedLocale: string|undefined): TranslationBundle { diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts index 898cd1330a..2ee09b5fac 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts @@ -8,7 +8,8 @@ import {ɵMessageId, ɵParsedTranslation, ɵparseTranslation} from '@angular/localize'; import {extname} from 'path'; import {Diagnostics} from '../../../diagnostics'; -import {ParsedTranslationBundle, TranslationParser} from './translation_parser'; + +import {ParseAnalysis, ParsedTranslationBundle, TranslationParser} from './translation_parser'; /** * A translation parser that can parse JSON that has the form: @@ -26,15 +27,42 @@ import {ParsedTranslationBundle, TranslationParser} from './translation_parser'; * @see SimpleJsonTranslationSerializer */ export class SimpleJsonTranslationParser implements TranslationParser { + /** + * @deprecated + */ canParse(filePath: string, contents: string): Object|false { + const result = this.analyze(filePath, contents); + return result.canParse && result.hint; + } + + analyze(filePath: string, contents: string): ParseAnalysis { + const diagnostics = new Diagnostics(); if (extname(filePath) !== '.json') { - return false; + diagnostics.warn('File does not have .json extension.'); + return {canParse: false, diagnostics}; } try { const json = JSON.parse(contents); - return (typeof json.locale === 'string' && typeof json.translations === 'object') && json; - } catch { - return false; + if (json.locale === undefined) { + diagnostics.warn('Required "locale" property missing.'); + return {canParse: false, diagnostics}; + } + if (typeof json.locale !== 'string') { + diagnostics.warn('The "locale" property is not a string.'); + return {canParse: false, diagnostics}; + } + if (json.translations === undefined) { + diagnostics.warn('Required "translations" property missing.'); + return {canParse: false, diagnostics}; + } + if (typeof json.translations !== 'object') { + diagnostics.warn('The "translations" is not an object.'); + return {canParse: false, diagnostics}; + } + return {canParse: true, diagnostics, hint: json}; + } catch (e) { + diagnostics.warn('File is not valid JSON.'); + return {canParse: false, diagnostics}; } } diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts index 490a1b19c1..514ff7bdec 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts @@ -8,6 +8,29 @@ import {ɵMessageId, ɵParsedTranslation} from '@angular/localize/private'; import {Diagnostics} from '../../../diagnostics'; +/** + * Indicates that a parser can parse a given file, with a hint that can be used to speed up actual + * parsing. + */ +export interface CanParseAnalysis { + canParse: true; + diagnostics: Diagnostics; + hint: Hint; +} + +/** + * Indicates that a parser cannot parse a given file with diagnostics as why this is. + * */ +export interface CannotParseAnalysis { + canParse: false; + diagnostics: Diagnostics; +} + +/** + * Information about whether a `TranslationParser` can parse a given file. + */ +export type ParseAnalysis = CanParseAnalysis|CannotParseAnalysis; + /** * An object that holds translations that have been parsed from a translation file. */ @@ -38,6 +61,8 @@ 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 @@ -45,6 +70,15 @@ export interface TranslationParser { */ canParse(filePath: string, contents: string): Hint|false; + /** + * Analyze the file to see if this parser can parse the given file. + * + * @param filePath The absolute path to the translation file. + * @param contents The contents of the translation file. + * @returns Information indicating whether the file can be parsed by this parser. + */ + analyze(filePath: string, contents: string): ParseAnalysis; + /** * Parses the given file, extracting the target locale and translations. * diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_utils.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_utils.ts index fe19bdf96e..e45738740f 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_utils.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_utils.ts @@ -8,6 +8,7 @@ import {Element, LexerRange, Node, ParseError, ParseErrorLevel, ParseSourceSpan, XmlParser} from '@angular/compiler'; import {Diagnostics} from '../../../diagnostics'; import {TranslationParseError} from './translation_parse_error'; +import {ParseAnalysis} from './translation_parser'; export function getAttrOrThrow(element: Element, attrName: string): string { const attrValue = getAttribute(element, attrName); @@ -81,25 +82,33 @@ export interface XmlTranslationParserHint { */ export function canParseXml( filePath: string, contents: string, rootNodeName: string, - attributes: Record): XmlTranslationParserHint|false { + attributes: Record): ParseAnalysis { + const diagnostics = new Diagnostics(); const xmlParser = new XmlParser(); const xml = xmlParser.parse(contents, filePath); if (xml.rootNodes.length === 0 || xml.errors.some(error => error.level === ParseErrorLevel.ERROR)) { - return false; + xml.errors.forEach(e => addParseError(diagnostics, e)); + return {canParse: false, diagnostics}; } const rootElements = xml.rootNodes.filter(isNamedElement(rootNodeName)); const rootElement = rootElements[0]; if (rootElement === undefined) { - return false; + diagnostics.warn(`The XML file does not contain a <${rootNodeName}> root node.`); + return {canParse: false, diagnostics}; } for (const attrKey of Object.keys(attributes)) { const attr = rootElement.attrs.find(attr => attr.name === attrKey); if (attr === undefined || attr.value !== attributes[attrKey]) { - return false; + addParseDiagnostic( + diagnostics, rootElement.sourceSpan, + `The <${rootNodeName}> node does not have the required attribute: ${attrKey}="${ + attributes[attrKey]}".`, + ParseErrorLevel.WARNING); + return {canParse: false, diagnostics}; } } @@ -110,7 +119,7 @@ export function canParseXml( ParseErrorLevel.WARNING)); } - return {element: rootElement, errors: xml.errors}; + return {canParse: true, diagnostics, hint: {element: rootElement, errors: xml.errors}}; } /** diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts index 664ec1c354..0f0636b10e 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts @@ -13,7 +13,7 @@ import {BaseVisitor} from '../base_visitor'; import {MessageSerializer} from '../message_serialization/message_serializer'; import {TargetMessageRenderer} from '../message_serialization/target_message_renderer'; -import {ParsedTranslationBundle, TranslationParser} from './translation_parser'; +import {ParseAnalysis, ParsedTranslationBundle, TranslationParser} from './translation_parser'; import {addParseDiagnostic, addParseError, canParseXml, getAttribute, isNamedElement, parseInnerRange, XmlTranslationParserHint} from './translation_utils'; /** @@ -25,7 +25,15 @@ import {addParseDiagnostic, addParseError, canParseXml, getAttribute, isNamedEle * @see Xliff1TranslationSerializer */ 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'}); } diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts index 7c83283e19..5304665cde 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts @@ -13,7 +13,7 @@ import {BaseVisitor} from '../base_visitor'; import {MessageSerializer} from '../message_serialization/message_serializer'; import {TargetMessageRenderer} from '../message_serialization/target_message_renderer'; -import {ParsedTranslationBundle, TranslationParser} from './translation_parser'; +import {ParseAnalysis, ParsedTranslationBundle, TranslationParser} from './translation_parser'; import {addParseDiagnostic, addParseError, canParseXml, getAttribute, isNamedElement, parseInnerRange, XmlTranslationParserHint} from './translation_utils'; /** @@ -24,7 +24,15 @@ import {addParseDiagnostic, addParseError, canParseXml, getAttribute, isNamedEle * @see Xliff2TranslationSerializer */ 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 { return canParseXml(filePath, contents, 'xliff', {version: '2.0'}); } diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser.ts index 494158dd8c..e20260896e 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser.ts @@ -14,7 +14,7 @@ import {BaseVisitor} from '../base_visitor'; import {MessageSerializer} from '../message_serialization/message_serializer'; import {TargetMessageRenderer} from '../message_serialization/target_message_renderer'; -import {ParsedTranslationBundle, TranslationParser} from './translation_parser'; +import {ParseAnalysis, ParsedTranslationBundle, TranslationParser} from './translation_parser'; import {addParseDiagnostic, addParseError, canParseXml, getAttribute, parseInnerRange, XmlTranslationParserHint} from './translation_utils'; @@ -26,10 +26,20 @@ import {addParseDiagnostic, addParseError, canParseXml, getAttribute, parseInner * @see XmbTranslationSerializer */ export class XtbTranslationParser 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') { - return false; + const diagnostics = new Diagnostics(); + diagnostics.warn('Must have xtb or xmb extension.'); + return {canParse: false, diagnostics}; } return canParseXml(filePath, contents, 'translationbundle', {}); } @@ -81,7 +91,7 @@ class XtbVisitor extends BaseVisitor { if (id === undefined) { addParseDiagnostic( bundle.diagnostics, element.sourceSpan, - `Missing required "id" attribute on element.`, ParseErrorLevel.ERROR); + `Missing required "id" attribute on element.`, ParseErrorLevel.ERROR); return; } diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts index 3b2c54c148..0c52eba69c 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts @@ -12,7 +12,7 @@ import {ɵParsedTranslation, ɵparseTranslation} from '@angular/localize'; import {DiagnosticHandlingStrategy, Diagnostics} from '../../../src/diagnostics'; import {TranslationLoader} from '../../../src/translate/translation_files/translation_loader'; import {SimpleJsonTranslationParser} from '../../../src/translate/translation_files/translation_parsers/simple_json_translation_parser'; -import {TranslationParser} from '../../../src/translate/translation_files/translation_parsers/translation_parser'; +import {ParseAnalysis, TranslationParser} from '../../../src/translate/translation_files/translation_parsers/translation_parser'; runInEachFileSystem(() => { describe('TranslationLoader', () => { @@ -204,8 +204,11 @@ runInEachFileSystem(() => { const parser = new MockTranslationParser(neverCanParse); const loader = new TranslationLoader(fs, [parser], 'error', diagnostics); expect(() => loader.loadBundles([[enTranslationPath], [frTranslationPath]], [])) - .toThrowError(`There is no "TranslationParser" that can parse this translation file: ${ - enTranslationPath}.`); + .toThrowError( + `There is no "TranslationParser" that can parse this translation file: ${ + enTranslationPath}.\n` + + `MockTranslationParser cannot parse translation file.\n` + + `WARNINGS:\n - This is a mock failure warning.`); }); }); }); @@ -217,8 +220,16 @@ runInEachFileSystem(() => { 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.'); this.log.push(`canParse(${filePath}, ${fileContents})`); - return this._canParse(filePath); + return this._canParse(filePath) ? {canParse: true, hint: true, diagnostics} : + {canParse: false, diagnostics}; } parse(filePath: string, fileContents: string) { diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts index 7469c733a9..20567a5465 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts @@ -23,6 +23,29 @@ describe('SimpleJsonTranslationParser', () => { }); }); + describe('analyze()', () => { + it('should return a success object if the file extension is `.json` and contains top level `locale` and `translations` properties', + () => { + const parser = new SimpleJsonTranslationParser(); + expect(parser.analyze('/some/file.json', '{ "locale" : "fr", "translations" : {}}')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + }); + + it('should return a failure object if the file is not a valid format', () => { + const parser = new SimpleJsonTranslationParser(); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.json', '{}')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.json', '{ "translations" : {} }')) + .toEqual(jasmine.objectContaining({canParse: false})); + expect(parser.analyze('/some/file.json', '{ "locale" : "fr" }')) + .toEqual(jasmine.objectContaining({canParse: false})); + }); + }); + for (const withHint of [true, false]) { describe(`parse() [${withHint ? 'with' : 'without'} hint]`, () => { const doParse: (fileName: string, contents: string) => ParsedTranslationBundle = diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts index 466bc9bbc1..de347c5de8 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; -import {ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; +import {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', () => { @@ -31,6 +31,60 @@ describe('Xliff1TranslationParser', () => { }); }); + describe('analyze()', () => { + it('should return a success object if the file contains an element with version="1.2" attribute', + () => { + const parser = new Xliff1TranslationParser(); + expect(parser.analyze('/some/file.xlf', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.json', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.xliff', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.json', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + }); + + it('should return a failure object if the file cannot be parsed as XLIFF 1.2', () => { + const parser = new Xliff1TranslationParser(); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.xlf', '')) + .toEqual(jasmine.objectContaining({canParse: false})); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.json', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + }); + + it('should return a diagnostics object when the file is not a valid format', () => { + let result: ParseAnalysis; + const parser = new Xliff1TranslationParser(); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([ + {type: 'warning', message: 'The XML file does not contain a root node.'} + ]); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([{ + type: 'warning', + message: + 'The node does not have the required attribute: version="1.2". ("[WARNING ->]"): /some/file.xlf@0:0' + }]); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([{ + type: 'error', + message: + 'Unexpected closing tag "file". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]"): /some/file.xlf@0:21' + }]); + }); + }); + for (const withHint of [true, false]) { describe(`parse() [${withHint ? 'with' : 'without'} hint]`, () => { const doParse: (fileName: string, XLIFF: string) => ParsedTranslationBundle = diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts index 116a535ac6..82ef7484ad 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; -import {ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; +import {ParseAnalysis, ParsedTranslationBundle} from '../../../../src/translate/translation_files/translation_parsers/translation_parser'; import {Xliff2TranslationParser} from '../../../../src/translate/translation_files/translation_parsers/xliff2_translation_parser'; describe( @@ -32,6 +32,64 @@ describe( }); }); + describe('analyze', () => { + it('should return a success object if the file contains an element with version="2.0" attribute', + () => { + const parser = new Xliff2TranslationParser(); + expect(parser.analyze( + '/some/file.xlf', + '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze( + '/some/file.json', + '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.xliff', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.json', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + }); + + it('should return a failure object if the file cannot be parsed as XLIFF 2.0', () => { + const parser = new Xliff2TranslationParser(); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.xlf', '')) + .toEqual(jasmine.objectContaining({canParse: false})); + expect(parser.analyze('/some/file.xlf', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.json', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + }); + + it('should return a diagnostics object when the file is not a valid format', () => { + let result: ParseAnalysis; + const parser = new Xliff2TranslationParser(); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([ + {type: 'warning', message: 'The XML file does not contain a root node.'} + ]); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([{ + type: 'warning', + message: + 'The node does not have the required attribute: version="2.0". ("[WARNING ->]"): /some/file.xlf@0:0' + }]); + + result = parser.analyze('/some/file.xlf', ''); + expect(result.diagnostics.messages).toEqual([{ + type: 'error', + message: + 'Unexpected closing tag "file". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("[ERROR ->]"): /some/file.xlf@0:21' + }]); + }); + }); + for (const withHint of [true, false]) { describe( `parse() [${withHint ? 'with' : 'without'} hint]`, () => { diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts index df055b20ec..77a568044d 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {ɵcomputeMsgId, ɵmakeParsedTranslation} from '@angular/localize'; -import {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', () => { @@ -24,6 +24,50 @@ describe('XtbTranslationParser', () => { }); }); + describe('analyze()', () => { + it('should return a success object if the file extension is `.xtb` or `.xmb` and it contains the `` tag', + () => { + const parser = new XtbTranslationParser(); + expect(parser.analyze('/some/file.xtb', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.xmb', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.xtb', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + expect(parser.analyze('/some/file.xmb', '')) + .toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)})); + }); + + it('should return a failure object if the file is not valid XTB', () => { + const parser = new XtbTranslationParser(); + expect(parser.analyze('/some/file.json', '')) + .toEqual(jasmine.objectContaining({canParse: false})); + expect(parser.analyze('/some/file.xmb', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + expect(parser.analyze('/some/file.xtb', '')).toEqual(jasmine.objectContaining({ + canParse: false + })); + }); + + it('should return a diagnostics object when the file is not a valid format', () => { + let results: ParseAnalysis; + const parser = new XtbTranslationParser(); + + results = parser.analyze('/some/file.xtb', ''); + expect(results.diagnostics.messages).toEqual([ + {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' + }]); + }); + }); + for (const withHint of [true, false]) { describe(`parse() [${withHint ? 'with' : 'without'} hint]`, () => { const doParse: (fileName: string, XTB: string) => ParsedTranslationBundle = @@ -261,7 +305,7 @@ describe('XtbTranslationParser', () => { ].join('\n'); expectToFail('/some/file.xtb', XTB, /Missing required "id" attribute/, [ - `Missing required "id" attribute on element. ("`, + `Missing required "id" attribute on element. ("`, ` [ERROR ->]`, `"): /some/file.xtb@1:2`, ].join('\n'));