feat(localize): expose `canParse()` diagnostics (#37909)
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
This commit is contained in:
parent
596fbb3f06
commit
ec32eba02c
|
@ -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<TranslationParser<any>, ParseAnalysis<any>>();
|
||||
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 {
|
||||
|
|
|
@ -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<Object> {
|
||||
/**
|
||||
* @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<Object> {
|
||||
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};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Hint> {
|
||||
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<Hint> = CanParseAnalysis<Hint>|CannotParseAnalysis;
|
||||
|
||||
/**
|
||||
* An object that holds translations that have been parsed from a translation file.
|
||||
*/
|
||||
|
@ -38,6 +61,8 @@ export interface TranslationParser<Hint = true> {
|
|||
/**
|
||||
* 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<Hint = true> {
|
|||
*/
|
||||
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<Hint>;
|
||||
|
||||
/**
|
||||
* Parses the given file, extracting the target locale and translations.
|
||||
*
|
||||
|
|
|
@ -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<string, string>): XmlTranslationParserHint|false {
|
||||
attributes: Record<string, string>): ParseAnalysis<XmlTranslationParserHint> {
|
||||
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}};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<XmlTranslationParserHint> {
|
||||
/**
|
||||
* @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<XmlTranslationParserHint> {
|
||||
return canParseXml(filePath, contents, 'xliff', {version: '1.2'});
|
||||
}
|
||||
|
||||
|
|
|
@ -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<XmlTranslationParserHint> {
|
||||
/**
|
||||
* @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<XmlTranslationParserHint> {
|
||||
return canParseXml(filePath, contents, 'xliff', {version: '2.0'});
|
||||
}
|
||||
|
||||
|
|
|
@ -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<XmlTranslationParserHint> {
|
||||
/**
|
||||
* @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<XmlTranslationParserHint> {
|
||||
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 <trans-unit> element.`, ParseErrorLevel.ERROR);
|
||||
`Missing required "id" attribute on <translation> element.`, ParseErrorLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string, ɵParsedTranslation> = {}) {}
|
||||
|
||||
canParse(filePath: string, fileContents: string) {
|
||||
const result = this.analyze(filePath, fileContents);
|
||||
return result.canParse && result.hint;
|
||||
}
|
||||
|
||||
analyze(filePath: string, fileContents: string): ParseAnalysis<true> {
|
||||
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) {
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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 <xliff> element with version="1.2" attribute',
|
||||
() => {
|
||||
const parser = new Xliff1TranslationParser();
|
||||
expect(parser.analyze('/some/file.xlf', '<xliff version="1.2">'))
|
||||
.toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)}));
|
||||
expect(parser.analyze('/some/file.json', '<xliff version="1.2">'))
|
||||
.toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)}));
|
||||
expect(parser.analyze('/some/file.xliff', '<xliff version="1.2">'))
|
||||
.toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)}));
|
||||
expect(parser.analyze('/some/file.json', '<xliff version="1.2">'))
|
||||
.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', '<xliff>')).toEqual(jasmine.objectContaining({
|
||||
canParse: false
|
||||
}));
|
||||
expect(parser.analyze('/some/file.xlf', '<xliff version="2.0">'))
|
||||
.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<any>;
|
||||
const parser = new Xliff1TranslationParser();
|
||||
|
||||
result = parser.analyze('/some/file.xlf', '<moo>');
|
||||
expect(result.diagnostics.messages).toEqual([
|
||||
{type: 'warning', message: 'The XML file does not contain a <xliff> root node.'}
|
||||
]);
|
||||
|
||||
result = parser.analyze('/some/file.xlf', '<xliff version="2.0">');
|
||||
expect(result.diagnostics.messages).toEqual([{
|
||||
type: 'warning',
|
||||
message:
|
||||
'The <xliff> node does not have the required attribute: version="1.2". ("[WARNING ->]<xliff version="2.0">"): /some/file.xlf@0:0'
|
||||
}]);
|
||||
|
||||
result = parser.analyze('/some/file.xlf', '<xliff version="1.2"></file>');
|
||||
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 ("<xliff version="1.2">[ERROR ->]</file>"): /some/file.xlf@0:21'
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
for (const withHint of [true, false]) {
|
||||
describe(`parse() [${withHint ? 'with' : 'without'} hint]`, () => {
|
||||
const doParse: (fileName: string, XLIFF: string) => ParsedTranslationBundle =
|
||||
|
|
|
@ -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 <xliff> element with version="2.0" attribute',
|
||||
() => {
|
||||
const parser = new Xliff2TranslationParser();
|
||||
expect(parser.analyze(
|
||||
'/some/file.xlf',
|
||||
'<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0">'))
|
||||
.toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)}));
|
||||
expect(parser.analyze(
|
||||
'/some/file.json',
|
||||
'<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0">'))
|
||||
.toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)}));
|
||||
expect(parser.analyze('/some/file.xliff', '<xliff version="2.0">'))
|
||||
.toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)}));
|
||||
expect(parser.analyze('/some/file.json', '<xliff version="2.0">'))
|
||||
.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', '<xliff>')).toEqual(jasmine.objectContaining({
|
||||
canParse: false
|
||||
}));
|
||||
expect(parser.analyze('/some/file.xlf', '<xliff version="1.2">'))
|
||||
.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<any>;
|
||||
const parser = new Xliff2TranslationParser();
|
||||
|
||||
result = parser.analyze('/some/file.xlf', '<moo>');
|
||||
expect(result.diagnostics.messages).toEqual([
|
||||
{type: 'warning', message: 'The XML file does not contain a <xliff> root node.'}
|
||||
]);
|
||||
|
||||
result = parser.analyze('/some/file.xlf', '<xliff version="1.2">');
|
||||
expect(result.diagnostics.messages).toEqual([{
|
||||
type: 'warning',
|
||||
message:
|
||||
'The <xliff> node does not have the required attribute: version="2.0". ("[WARNING ->]<xliff version="1.2">"): /some/file.xlf@0:0'
|
||||
}]);
|
||||
|
||||
result = parser.analyze('/some/file.xlf', '<xliff version="2.0"></file>');
|
||||
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 ("<xliff version="2.0">[ERROR ->]</file>"): /some/file.xlf@0:21'
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
for (const withHint of [true, false]) {
|
||||
describe(
|
||||
`parse() [${withHint ? 'with' : 'without'} hint]`, () => {
|
||||
|
|
|
@ -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 `<translationbundle>` tag',
|
||||
() => {
|
||||
const parser = new XtbTranslationParser();
|
||||
expect(parser.analyze('/some/file.xtb', '<translationbundle>'))
|
||||
.toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)}));
|
||||
expect(parser.analyze('/some/file.xmb', '<translationbundle>'))
|
||||
.toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)}));
|
||||
expect(parser.analyze('/some/file.xtb', '<translationbundle lang="en">'))
|
||||
.toEqual(jasmine.objectContaining({canParse: true, hint: jasmine.any(Object)}));
|
||||
expect(parser.analyze('/some/file.xmb', '<translationbundle lang="en">'))
|
||||
.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', '<translationbundle>'))
|
||||
.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<any>;
|
||||
const parser = new XtbTranslationParser();
|
||||
|
||||
results = parser.analyze('/some/file.xtb', '<moo>');
|
||||
expect(results.diagnostics.messages).toEqual([
|
||||
{type: 'warning', message: 'The XML file does not contain a <translationbundle> root node.'}
|
||||
]);
|
||||
|
||||
results = parser.analyze('/some/file.xtb', '<translationbundle></translation>');
|
||||
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 ("<translationbundle>[ERROR ->]</translation>"): /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 <trans-unit> element. ("<translationbundle>`,
|
||||
`Missing required "id" attribute on <translation> element. ("<translationbundle>`,
|
||||
` [ERROR ->]<translation></translation>`,
|
||||
`</translationbundle>"): /some/file.xtb@1:2`,
|
||||
].join('\n'));
|
||||
|
|
Loading…
Reference in New Issue