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' ;
8
+ import { Element , ParseErrorLevel , visitAll } from '@angular/compiler' ;
9
9
import { ɵParsedTranslation } from '@angular/localize' ;
10
10
import { extname } from 'path' ;
11
11
@@ -14,87 +14,100 @@ import {BaseVisitor} from '../base_visitor';
14
14
import { MessageSerializer } from '../message_serialization/message_serializer' ;
15
15
import { TargetMessageRenderer } from '../message_serialization/target_message_renderer' ;
16
16
17
- import { TranslationParseError } from './translation_parse_error' ;
18
17
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' ;
21
19
22
20
23
21
/**
24
22
* A translation parser that can load XB files.
25
23
*/
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 {
30
26
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' , { } ) ;
33
31
}
34
32
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 ) ;
41
39
}
42
- return bundle ;
43
40
}
44
- }
45
41
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 ;
52
54
}
53
55
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
+ }
55
70
56
- visitElement ( element : Element , bundle : ParsedTranslationBundle | undefined ) : any {
71
+ class XtbVisitor extends BaseVisitor {
72
+ visitElement ( element : Element , bundle : ParsedTranslationBundle ) : any {
57
73
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 ;
62
82
}
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 ;
71
83
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 ;
76
90
}
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 ;
92
103
}
93
104
}
94
105
break ;
95
106
96
107
default :
97
- throw new TranslationParseError ( element . sourceSpan , 'Unexpected tag' ) ;
108
+ addParseDiagnostic (
109
+ bundle . diagnostics , element . sourceSpan , `Unexpected <${ element . name } > tag.` ,
110
+ ParseErrorLevel . ERROR ) ;
98
111
}
99
112
}
100
113
}
0 commit comments