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

Skip to content

Commit 08071e5

Browse files
petebacondarwinmatsko
authored andcommitted
fix(localize): improve matching and parsing of XLIFF 2.0 translation files (#35793)
Previously, the `Xliff2TranslationParser` only matched files that had a narrow choice of extensions (e.g. `xlf`) and also relied upon a regular expression match of an optional XML namespace directive. This commit relaxes the requirement on both of these and, instead, relies upon parsing the file into XML and identifying an element of the form `<xliff version="2.0">` which is the minimal requirement for such files. PR Close #35793
1 parent 350ac11 commit 08071e5

File tree

2 files changed

+675
-83
lines changed

2 files changed

+675
-83
lines changed

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

Lines changed: 110 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,106 +5,149 @@
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';
9-
import {ɵMessageId, ɵParsedTranslation} from '@angular/localize';
10-
import {extname} from 'path';
8+
import {Element, Node, ParseErrorLevel, visitAll} from '@angular/compiler';
9+
import {ɵParsedTranslation} from '@angular/localize';
1110

1211
import {Diagnostics} from '../../../diagnostics';
1312
import {BaseVisitor} from '../base_visitor';
1413
import {MessageSerializer} from '../message_serialization/message_serializer';
1514
import {TargetMessageRenderer} from '../message_serialization/target_message_renderer';
1615

17-
import {TranslationParseError} from './translation_parse_error';
1816
import {ParsedTranslationBundle, TranslationParser} from './translation_parser';
19-
import {getAttrOrThrow, getAttribute, parseInnerRange} from './translation_utils';
20-
21-
const XLIFF_2_0_NS_REGEX = /xmlns="urn:oasis:names:tc:xliff:document:2.0"/;
17+
import {XmlTranslationParserHint, addParseDiagnostic, addParseError, canParseXml, getAttribute, isNamedElement, parseInnerRange} from './translation_utils';
2218

2319
/**
2420
* A translation parser that can load translations from XLIFF 2 files.
2521
*
2622
* http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html
2723
*
2824
*/
29-
export class Xliff2TranslationParser implements TranslationParser {
30-
canParse(filePath: string, contents: string): boolean {
31-
return (extname(filePath) === '.xlf') && XLIFF_2_0_NS_REGEX.test(contents);
25+
export class Xliff2TranslationParser implements TranslationParser<XmlTranslationParserHint> {
26+
canParse(filePath: string, contents: string): XmlTranslationParserHint|false {
27+
return canParseXml(filePath, contents, 'xliff', {version: '2.0'});
3228
}
3329

34-
parse(filePath: string, contents: string): ParsedTranslationBundle {
35-
const xmlParser = new XmlParser();
36-
const xml = xmlParser.parse(contents, filePath);
37-
const bundle = Xliff2TranslationBundleVisitor.extractBundle(xml.rootNodes);
38-
if (bundle === undefined) {
39-
throw new Error(`Unable to parse "${filePath}" as XLIFF 2.0 format.`);
30+
parse(filePath: string, contents: string, hint?: XmlTranslationParserHint):
31+
ParsedTranslationBundle {
32+
if (hint) {
33+
return this.extractBundle(hint);
34+
} else {
35+
return this.extractBundleDeprecated(filePath, contents);
4036
}
41-
return bundle;
4237
}
43-
}
4438

45-
interface BundleVisitorContext {
46-
parsedLocale?: string;
47-
}
39+
private extractBundle({element, errors}: XmlTranslationParserHint): ParsedTranslationBundle {
40+
const diagnostics = new Diagnostics();
41+
errors.forEach(e => addParseError(diagnostics, e));
42+
43+
if (element.children.length === 0) {
44+
addParseDiagnostic(
45+
diagnostics, element.sourceSpan, 'Missing expected <file> element',
46+
ParseErrorLevel.WARNING);
47+
return {locale: undefined, translations: {}, diagnostics};
48+
}
4849

49-
class Xliff2TranslationBundleVisitor extends BaseVisitor {
50-
private bundle: ParsedTranslationBundle|undefined;
50+
const locale = getAttribute(element, 'trgLang');
51+
const files = element.children.filter(isFileElement);
52+
if (files.length === 0) {
53+
addParseDiagnostic(
54+
diagnostics, element.sourceSpan, 'No <file> elements found in <xliff>',
55+
ParseErrorLevel.WARNING);
56+
} else if (files.length > 1) {
57+
addParseDiagnostic(
58+
diagnostics, files[1].sourceSpan, 'More than one <file> element found in <xliff>',
59+
ParseErrorLevel.WARNING);
60+
}
5161

52-
static extractBundle(xliff: Node[]): ParsedTranslationBundle|undefined {
53-
const visitor = new this();
54-
visitAll(visitor, xliff, {});
55-
return visitor.bundle;
62+
const bundle = {locale, translations: {}, diagnostics};
63+
const translationVisitor = new Xliff2TranslationVisitor();
64+
visitAll(translationVisitor, files[0].children, {bundle});
65+
66+
return bundle;
5667
}
5768

58-
visitElement(element: Element, {parsedLocale}: BundleVisitorContext): any {
59-
if (element.name === 'xliff') {
60-
parsedLocale = getAttribute(element, 'trgLang');
61-
return visitAll(this, element.children, {parsedLocale});
62-
} else if (element.name === 'file') {
63-
this.bundle = {
64-
locale: parsedLocale,
65-
translations: Xliff2TranslationVisitor.extractTranslations(element),
66-
diagnostics: new Diagnostics(),
67-
};
68-
} else {
69-
return visitAll(this, element.children, {parsedLocale});
69+
private extractBundleDeprecated(filePath: string, contents: string) {
70+
const hint = this.canParse(filePath, contents);
71+
if (!hint) {
72+
throw new Error(`Unable to parse "${filePath}" as XLIFF 2.0 format.`);
7073
}
74+
const bundle = this.extractBundle(hint);
75+
if (bundle.diagnostics.hasErrors) {
76+
const message =
77+
bundle.diagnostics.formatDiagnostics(`Failed to parse "${filePath}" as XLIFF 2.0 format`);
78+
throw new Error(message);
79+
}
80+
return bundle;
7181
}
7282
}
7383

74-
class Xliff2TranslationVisitor extends BaseVisitor {
75-
private translations: Record<ɵMessageId, ɵParsedTranslation> = {};
7684

77-
static extractTranslations(file: Element): Record<string, ɵParsedTranslation> {
78-
const visitor = new this();
79-
visitAll(visitor, file.children);
80-
return visitor.translations;
81-
}
85+
interface TranslationVisitorContext {
86+
unit?: string;
87+
bundle: ParsedTranslationBundle;
88+
}
8289

83-
visitElement(element: Element, context: any): any {
90+
class Xliff2TranslationVisitor extends BaseVisitor {
91+
visitElement(element: Element, {bundle, unit}: TranslationVisitorContext): any {
8492
if (element.name === 'unit') {
85-
const externalId = getAttrOrThrow(element, 'id');
86-
if (this.translations[externalId] !== undefined) {
87-
throw new TranslationParseError(
88-
element.sourceSpan, `Duplicated translations for message "${externalId}"`);
89-
}
90-
visitAll(this, element.children, {unit: externalId});
93+
this.visitUnitElement(element, bundle);
9194
} else if (element.name === 'segment') {
92-
assertTranslationUnit(element, context);
93-
const targetMessage = element.children.find(isTargetElement);
94-
if (targetMessage === undefined) {
95-
throw new TranslationParseError(element.sourceSpan, 'Missing required <target> element');
96-
}
97-
this.translations[context.unit] = serializeTargetMessage(targetMessage);
95+
this.visitSegmentElement(element, bundle, unit);
9896
} else {
99-
return visitAll(this, element.children);
97+
visitAll(this, element.children, {bundle, unit});
10098
}
10199
}
102-
}
103100

104-
function assertTranslationUnit(segment: Element, context: any) {
105-
if (context === undefined || context.unit === undefined) {
106-
throw new TranslationParseError(
107-
segment.sourceSpan, 'Invalid <segment> element: should be a child of a <unit> element.');
101+
private visitUnitElement(element: Element, bundle: ParsedTranslationBundle): void {
102+
// Error if no `id` attribute
103+
const externalId = getAttribute(element, 'id');
104+
if (externalId === undefined) {
105+
addParseDiagnostic(
106+
bundle.diagnostics, element.sourceSpan,
107+
`Missing required "id" attribute on <trans-unit> element.`, ParseErrorLevel.ERROR);
108+
return;
109+
}
110+
111+
// Error if there is already a translation with the same id
112+
if (bundle.translations[externalId] !== undefined) {
113+
addParseDiagnostic(
114+
bundle.diagnostics, element.sourceSpan,
115+
`Duplicated translations for message "${externalId}"`, ParseErrorLevel.ERROR);
116+
return;
117+
}
118+
119+
visitAll(this, element.children, {bundle, unit: externalId});
120+
}
121+
122+
private visitSegmentElement(
123+
element: Element, bundle: ParsedTranslationBundle, unit: string|undefined): void {
124+
// A `<segment>` element must be below a `<unit>` element
125+
if (unit === undefined) {
126+
addParseDiagnostic(
127+
bundle.diagnostics, element.sourceSpan,
128+
'Invalid <segment> element: should be a child of a <unit> element.',
129+
ParseErrorLevel.ERROR);
130+
return;
131+
}
132+
133+
const targetMessage = element.children.find(isNamedElement('target'));
134+
if (targetMessage === undefined) {
135+
addParseDiagnostic(
136+
bundle.diagnostics, element.sourceSpan, 'Missing required <target> element',
137+
ParseErrorLevel.ERROR);
138+
return;
139+
}
140+
141+
try {
142+
bundle.translations[unit] = serializeTargetMessage(targetMessage);
143+
} catch (e) {
144+
// Capture any errors from serialize the target message
145+
if (e.span && e.msg && e.level) {
146+
addParseDiagnostic(bundle.diagnostics, e.span, e.msg, e.level);
147+
} else {
148+
throw e;
149+
}
150+
}
108151
}
109152
}
110153

@@ -118,6 +161,6 @@ function serializeTargetMessage(source: Element): ɵParsedTranslation {
118161
return serializer.serialize(parseInnerRange(source));
119162
}
120163

121-
function isTargetElement(node: Node): node is Element {
122-
return node instanceof Element && node.name === 'target';
164+
function isFileElement(node: Node): node is Element {
165+
return node instanceof Element && node.name === 'file';
123166
}

0 commit comments

Comments
 (0)