5
5
* Use of this source code is governed by an MIT-style license that can be
6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
- import { Element , Node , XmlParser , visitAll } from '@angular/compiler' ;
9
- import { ɵMessageId , ɵParsedTranslation } from '@angular/localize' ;
10
- import { extname } from 'path' ;
8
+ import { Element , ParseErrorLevel , visitAll } from '@angular/compiler' ;
9
+ import { ɵParsedTranslation } from '@angular/localize' ;
11
10
12
11
import { Diagnostics } from '../../../diagnostics' ;
13
12
import { BaseVisitor } from '../base_visitor' ;
14
13
import { MessageSerializer } from '../message_serialization/message_serializer' ;
15
14
import { TargetMessageRenderer } from '../message_serialization/target_message_renderer' ;
16
15
17
- import { TranslationParseError } from './translation_parse_error' ;
18
16
import { ParsedTranslationBundle , TranslationParser } from './translation_parser' ;
19
- import { getAttrOrThrow , getAttribute , parseInnerRange } from './translation_utils' ;
20
-
21
- const XLIFF_1_2_NS_REGEX = / x m l n s = " u r n : o a s i s : n a m e s : t c : x l i f f : d o c u m e n t : 1 .2 " / ;
17
+ import { XmlTranslationParserHint , addParseDiagnostic , addParseError , canParseXml , getAttribute , isNamedElement , parseInnerRange } from './translation_utils' ;
22
18
23
19
/**
24
20
* A translation parser that can load XLIFF 1.2 files.
@@ -27,68 +23,120 @@ const XLIFF_1_2_NS_REGEX = /xmlns="urn:oasis:names:tc:xliff:document:1.2"/;
27
23
* http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
28
24
*
29
25
*/
30
- export class Xliff1TranslationParser implements TranslationParser {
31
- canParse ( filePath : string , contents : string ) : boolean {
32
- return ( extname ( filePath ) === '.xlf' ) && XLIFF_1_2_NS_REGEX . test ( contents ) ;
26
+ export class Xliff1TranslationParser implements TranslationParser < XmlTranslationParserHint > {
27
+ canParse ( filePath : string , contents : string ) : XmlTranslationParserHint | false {
28
+ return canParseXml ( filePath , contents , 'xliff' , { version : '1.2' } ) ;
29
+ }
30
+
31
+ parse ( filePath : string , contents : string , hint ?: XmlTranslationParserHint ) :
32
+ ParsedTranslationBundle {
33
+ if ( hint ) {
34
+ return this . extractBundle ( hint ) ;
35
+ } else {
36
+ return this . extractBundleDeprecated ( filePath , contents ) ;
37
+ }
38
+ }
39
+
40
+ private extractBundle ( { element, errors} : XmlTranslationParserHint ) : ParsedTranslationBundle {
41
+ const diagnostics = new Diagnostics ( ) ;
42
+ errors . forEach ( e => addParseError ( diagnostics , e ) ) ;
43
+
44
+ if ( element . children . length === 0 ) {
45
+ addParseDiagnostic (
46
+ diagnostics , element . sourceSpan , 'Missing expected <file> element' ,
47
+ ParseErrorLevel . WARNING ) ;
48
+ return { locale : undefined , translations : { } , diagnostics} ;
49
+ }
50
+
51
+ const files = element . children . filter ( isNamedElement ( 'file' ) ) ;
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
+ }
61
+
62
+ const bundle = {
63
+ locale : getAttribute ( files [ 0 ] , 'target-language' ) ,
64
+ translations : { } , diagnostics,
65
+ } ;
66
+ const translationVisitor = new XliffTranslationVisitor ( ) ;
67
+ visitAll ( translationVisitor , files [ 0 ] . children , bundle ) ;
68
+
69
+ return bundle ;
33
70
}
34
71
35
- parse ( filePath : string , contents : string ) : ParsedTranslationBundle {
36
- const xmlParser = new XmlParser ( ) ;
37
- const xml = xmlParser . parse ( contents , filePath ) ;
38
- const bundle = XliffFileElementVisitor . extractBundle ( xml . rootNodes ) ;
39
- if ( bundle === undefined ) {
72
+ private extractBundleDeprecated ( filePath : string , contents : string ) {
73
+ const hint = this . canParse ( filePath , contents ) ;
74
+ if ( ! hint ) {
40
75
throw new Error ( `Unable to parse "${ filePath } " as XLIFF 1.2 format.` ) ;
41
76
}
77
+ const bundle = this . extractBundle ( hint ) ;
78
+ if ( bundle . diagnostics . hasErrors ) {
79
+ const message =
80
+ bundle . diagnostics . formatDiagnostics ( `Failed to parse "${ filePath } " as XLIFF 1.2 format` ) ;
81
+ throw new Error ( message ) ;
82
+ }
42
83
return bundle ;
43
84
}
44
85
}
45
86
46
87
class XliffFileElementVisitor extends BaseVisitor {
47
- private bundle : ParsedTranslationBundle | undefined ;
48
-
49
- static extractBundle ( xliff : Node [ ] ) : ParsedTranslationBundle | undefined {
50
- const visitor = new this ( ) ;
51
- visitAll ( visitor , xliff ) ;
52
- return visitor . bundle ;
88
+ visitElement ( fileElement : Element ) : any {
89
+ if ( fileElement . name === 'file' ) {
90
+ return { fileElement, locale : getAttribute ( fileElement , 'target-language' ) } ;
91
+ }
53
92
}
93
+ }
54
94
55
- visitElement ( element : Element ) : any {
56
- if ( element . name === 'file' ) {
57
- this . bundle = {
58
- locale : getAttribute ( element , 'target-language' ) ,
59
- translations : XliffTranslationVisitor . extractTranslations ( element ) ,
60
- diagnostics : new Diagnostics ( ) ,
61
- } ;
95
+ class XliffTranslationVisitor extends BaseVisitor {
96
+ visitElement ( element : Element , bundle : ParsedTranslationBundle ) : void {
97
+ if ( element . name === 'trans-unit' ) {
98
+ this . visitTransUnitElement ( element , bundle ) ;
62
99
} else {
63
- return visitAll ( this , element . children ) ;
100
+ visitAll ( this , element . children , bundle ) ;
64
101
}
65
102
}
66
- }
67
103
68
- class XliffTranslationVisitor extends BaseVisitor {
69
- private translations : Record < ɵMessageId , ɵParsedTranslation > = { } ;
104
+ private visitTransUnitElement ( element : Element , bundle : ParsedTranslationBundle ) : void {
105
+ // Error if no `id` attribute
106
+ const id = getAttribute ( element , 'id' ) ;
107
+ if ( id === undefined ) {
108
+ addParseDiagnostic (
109
+ bundle . diagnostics , element . sourceSpan ,
110
+ `Missing required "id" attribute on <trans-unit> element.` , ParseErrorLevel . ERROR ) ;
111
+ return ;
112
+ }
70
113
71
- static extractTranslations ( file : Element ) : Record < string , ɵParsedTranslation > {
72
- const visitor = new this ( ) ;
73
- visitAll ( visitor , file . children ) ;
74
- return visitor . translations ;
75
- }
114
+ // Error if there is already a translation with the same id
115
+ if ( bundle . translations [ id ] !== undefined ) {
116
+ addParseDiagnostic (
117
+ bundle . diagnostics , element . sourceSpan , `Duplicated translations for message "${ id } "` ,
118
+ ParseErrorLevel . ERROR ) ;
119
+ return ;
120
+ }
76
121
77
- visitElement ( element : Element ) : any {
78
- if ( element . name === 'trans-unit' ) {
79
- const id = getAttrOrThrow ( element , 'id' ) ;
80
- if ( this . translations [ id ] !== undefined ) {
81
- throw new TranslationParseError (
82
- element . sourceSpan , `Duplicated translations for message "${ id } "` ) ;
83
- }
122
+ // Error if there is no `<target>` child element
123
+ const targetMessage = element . children . find ( isNamedElement ( 'target' ) ) ;
124
+ if ( targetMessage === undefined ) {
125
+ addParseDiagnostic (
126
+ bundle . diagnostics , element . sourceSpan , 'Missing required <target> element' ,
127
+ ParseErrorLevel . ERROR ) ;
128
+ return ;
129
+ }
84
130
85
- const targetMessage = element . children . find ( isTargetElement ) ;
86
- if ( targetMessage === undefined ) {
87
- throw new TranslationParseError ( element . sourceSpan , 'Missing required <target> element' ) ;
131
+ try {
132
+ bundle . translations [ id ] = serializeTargetMessage ( targetMessage ) ;
133
+ } catch ( e ) {
134
+ // Capture any errors from serialize the target message
135
+ if ( e . span && e . msg && e . level ) {
136
+ addParseDiagnostic ( bundle . diagnostics , e . span , e . msg , e . level ) ;
137
+ } else {
138
+ throw e ;
88
139
}
89
- this . translations [ id ] = serializeTargetMessage ( targetMessage ) ;
90
- } else {
91
- return visitAll ( this , element . children ) ;
92
140
}
93
141
}
94
142
}
@@ -100,7 +148,3 @@ function serializeTargetMessage(source: Element): ɵParsedTranslation {
100
148
} ) ;
101
149
return serializer . serialize ( parseInnerRange ( source ) ) ;
102
150
}
103
-
104
- function isTargetElement ( node : Node ) : node is Element {
105
- return node instanceof Element && node . name === 'target' ;
106
- }
0 commit comments