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 , Node , 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_2_0_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 : 2 .0 " / ;
17
+ import { XmlTranslationParserHint , addParseDiagnostic , addParseError , canParseXml , getAttribute , isNamedElement , parseInnerRange } from './translation_utils' ;
22
18
23
19
/**
24
20
* A translation parser that can load translations from XLIFF 2 files.
25
21
*
26
22
* http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html
27
23
*
28
24
*/
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' } ) ;
32
28
}
33
29
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 ) ;
40
36
}
41
- return bundle ;
42
37
}
43
- }
44
38
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
+ }
48
49
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
+ }
51
61
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 ;
56
67
}
57
68
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.` ) ;
70
73
}
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 ;
71
81
}
72
82
}
73
83
74
- class Xliff2TranslationVisitor extends BaseVisitor {
75
- private translations : Record < ɵMessageId , ɵParsedTranslation > = { } ;
76
84
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
+ }
82
89
83
- visitElement ( element : Element , context : any ) : any {
90
+ class Xliff2TranslationVisitor extends BaseVisitor {
91
+ visitElement ( element : Element , { bundle, unit} : TranslationVisitorContext ) : any {
84
92
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 ) ;
91
94
} 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 ) ;
98
96
} else {
99
- return visitAll ( this , element . children ) ;
97
+ visitAll ( this , element . children , { bundle , unit } ) ;
100
98
}
101
99
}
102
- }
103
100
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
+ }
108
151
}
109
152
}
110
153
@@ -118,6 +161,6 @@ function serializeTargetMessage(source: Element): ɵParsedTranslation {
118
161
return serializer . serialize ( parseInnerRange ( source ) ) ;
119
162
}
120
163
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 ' ;
123
166
}
0 commit comments