Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9f68ff9

Browse files
petebacondarwinmatsko
authored andcommitted
fix(localize): improve matching and parsing of XTB translation files (#35793)
This commit improves the `canParse()` method to check that the file is valid XML and has the expected root node. Previously it was relying upon a regular expression to do this. PR Close #35793
1 parent 689964b commit 9f68ff9

File tree

2 files changed

+432
-84
lines changed

2 files changed

+432
-84
lines changed

‎packages/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser.ts

Lines changed: 71 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {Element, Node, XmlParser, visitAll} from '@angular/compiler';
8+
import {Element, ParseErrorLevel, visitAll} from '@angular/compiler';
99
import {ɵParsedTranslation} from '@angular/localize';
1010
import {extname} from 'path';
1111

@@ -14,87 +14,100 @@ import {BaseVisitor} from '../base_visitor';
1414
import {MessageSerializer} from '../message_serialization/message_serializer';
1515
import {TargetMessageRenderer} from '../message_serialization/target_message_renderer';
1616

17-
import {TranslationParseError} from './translation_parse_error';
1817
import {ParsedTranslationBundle, TranslationParser} from './translation_parser';
19-
import {getAttrOrThrow, parseInnerRange} from './translation_utils';
20-
18+
import {XmlTranslationParserHint, addParseDiagnostic, addParseError, canParseXml, getAttribute, parseInnerRange} from './translation_utils';
2119

2220

2321
/**
2422
* A translation parser that can load XB files.
2523
*/
26-
export class XtbTranslationParser implements TranslationParser {
27-
constructor(private diagnostics: Diagnostics = new Diagnostics()) {}
28-
29-
canParse(filePath: string, contents: string): boolean {
24+
export class XtbTranslationParser implements TranslationParser<XmlTranslationParserHint> {
25+
canParse(filePath: string, contents: string): XmlTranslationParserHint|false {
3026
const extension = extname(filePath);
31-
return (extension === '.xtb' || extension === '.xmb') &&
32-
contents.includes('<translationbundle');
27+
if (extension !== '.xtb' && extension !== '.xmb') {
28+
return false;
29+
}
30+
return canParseXml(filePath, contents, 'translationbundle', {});
3331
}
3432

35-
parse(filePath: string, contents: string): ParsedTranslationBundle {
36-
const xmlParser = new XmlParser();
37-
const xml = xmlParser.parse(contents, filePath);
38-
const bundle = XtbVisitor.extractBundle(this.diagnostics, xml.rootNodes);
39-
if (bundle === undefined) {
40-
throw new Error(`Unable to parse "${filePath}" as XTB/XMB format.`);
33+
parse(filePath: string, contents: string, hint?: XmlTranslationParserHint):
34+
ParsedTranslationBundle {
35+
if (hint) {
36+
return this.extractBundle(hint);
37+
} else {
38+
return this.extractBundleDeprecated(filePath, contents);
4139
}
42-
return bundle;
4340
}
44-
}
4541

46-
class XtbVisitor extends BaseVisitor {
47-
static extractBundle(diagnostics: Diagnostics, messageBundles: Node[]): ParsedTranslationBundle
48-
|undefined {
49-
const visitor = new this(diagnostics);
50-
const bundles: ParsedTranslationBundle[] = visitAll(visitor, messageBundles, undefined);
51-
return bundles[0];
42+
private extractBundle({element, errors}: XmlTranslationParserHint): ParsedTranslationBundle {
43+
const langAttr = element.attrs.find((attr) => attr.name === 'lang');
44+
const bundle: ParsedTranslationBundle = {
45+
locale: langAttr && langAttr.value,
46+
translations: {},
47+
diagnostics: new Diagnostics()
48+
};
49+
errors.forEach(e => addParseError(bundle.diagnostics, e));
50+
51+
const bundleVisitor = new XtbVisitor();
52+
visitAll(bundleVisitor, element.children, bundle);
53+
return bundle;
5254
}
5355

54-
constructor(private diagnostics: Diagnostics) { super(); }
56+
private extractBundleDeprecated(filePath: string, contents: string) {
57+
const hint = this.canParse(filePath, contents);
58+
if (!hint) {
59+
throw new Error(`Unable to parse "${filePath}" as XMB/XTB format.`);
60+
}
61+
const bundle = this.extractBundle(hint);
62+
if (bundle.diagnostics.hasErrors) {
63+
const message =
64+
bundle.diagnostics.formatDiagnostics(`Failed to parse "${filePath}" as XMB/XTB format`);
65+
throw new Error(message);
66+
}
67+
return bundle;
68+
}
69+
}
5570

56-
visitElement(element: Element, bundle: ParsedTranslationBundle|undefined): any {
71+
class XtbVisitor extends BaseVisitor {
72+
visitElement(element: Element, bundle: ParsedTranslationBundle): any {
5773
switch (element.name) {
58-
case 'translationbundle':
59-
if (bundle) {
60-
throw new TranslationParseError(
61-
element.sourceSpan, '<translationbundle> elements can not be nested');
74+
case 'translation':
75+
// Error if no `id` attribute
76+
const id = getAttribute(element, 'id');
77+
if (id === undefined) {
78+
addParseDiagnostic(
79+
bundle.diagnostics, element.sourceSpan,
80+
`Missing required "id" attribute on <trans-unit> element.`, ParseErrorLevel.ERROR);
81+
return;
6282
}
63-
const langAttr = element.attrs.find((attr) => attr.name === 'lang');
64-
bundle = {
65-
locale: langAttr && langAttr.value,
66-
translations: {},
67-
diagnostics: this.diagnostics
68-
};
69-
visitAll(this, element.children, bundle);
70-
return bundle;
7183

72-
case 'translation':
73-
if (!bundle) {
74-
throw new TranslationParseError(
75-
element.sourceSpan, '<translation> must be inside a <translationbundle>');
84+
// Error if there is already a translation with the same id
85+
if (bundle.translations[id] !== undefined) {
86+
addParseDiagnostic(
87+
bundle.diagnostics, element.sourceSpan, `Duplicated translations for message "${id}"`,
88+
ParseErrorLevel.ERROR);
89+
return;
7690
}
77-
const id = getAttrOrThrow(element, 'id');
78-
if (bundle.translations.hasOwnProperty(id)) {
79-
throw new TranslationParseError(
80-
element.sourceSpan, `Duplicated translations for message "${id}"`);
81-
} else {
82-
try {
83-
bundle.translations[id] = serializeTargetMessage(element);
84-
} catch (error) {
85-
if (typeof error === 'string') {
86-
this.diagnostics.warn(
87-
`Could not parse message with id "${id}" - perhaps it has an unrecognised ICU format?\n` +
88-
error);
89-
} else {
90-
throw error;
91-
}
91+
92+
try {
93+
bundle.translations[id] = serializeTargetMessage(element);
94+
} catch (error) {
95+
if (typeof error === 'string') {
96+
bundle.diagnostics.warn(
97+
`Could not parse message with id "${id}" - perhaps it has an unrecognised ICU format?\n` +
98+
error);
99+
} else if (error.span && error.msg && error.level) {
100+
addParseDiagnostic(bundle.diagnostics, error.span, error.msg, error.level);
101+
} else {
102+
throw error;
92103
}
93104
}
94105
break;
95106

96107
default:
97-
throw new TranslationParseError(element.sourceSpan, 'Unexpected tag');
108+
addParseDiagnostic(
109+
bundle.diagnostics, element.sourceSpan, `Unexpected <${element.name}> tag.`,
110+
ParseErrorLevel.ERROR);
98111
}
99112
}
100113
}

0 commit comments

Comments
 (0)