From ec32eba02c411b363b1195dfb84781bec05747a8 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Fri, 3 Jul 2020 15:45:15 +0100 Subject: [PATCH] feat(localize): expose `canParse()` diagnostics (#37909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When loading a translation file we ask each `TranslationParser` whether it can parse the file. Occasionally, this check can find errors in the file that would be useful to the developer. For example if the file has invalid XML. This commit deprecates the previous `canParse()` method and replaces it with a new `analyze()` method. This returns an object that includes a boolean `canParse` and then either a `hint` if it can parse the file, or a `diagnostics` object filled with any messages that can be used to diagnose problems with the format of the file. Closes #37901 PR Close #37909 --- .../translation_files/translation_loader.ts | 23 ++++--- .../simple_json_translation_parser.ts | 38 ++++++++++-- .../translation_parsers/translation_parser.ts | 34 +++++++++++ .../translation_parsers/translation_utils.ts | 19 ++++-- .../xliff1_translation_parser.ts | 10 +++- .../xliff2_translation_parser.ts | 10 +++- .../xtb_translation_parser.ts | 16 ++++- .../translation_loader_spec.ts | 19 ++++-- .../translation_parsers/simple_json_spec.ts | 23 +++++++ .../xliff1_translation_parser_spec.ts | 56 ++++++++++++++++- .../xliff2_translation_parser_spec.ts | 60 ++++++++++++++++++- .../xtb_translation_parser_spec.ts | 48 ++++++++++++++- 12 files changed, 326 insertions(+), 30 deletions(-) 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'));