1
1
from __future__ import absolute_import , unicode_literals
2
2
3
+ from collections import OrderedDict
4
+
3
5
import babel
4
6
import babel .numbers
5
7
import babel .plural
6
8
7
9
from fluent .syntax import FluentParser
8
- from fluent .syntax .ast import Message , Term
10
+ from fluent .syntax .ast import Junk , Message , Term
9
11
10
12
from .builtins import BUILTINS
13
+ from .compiler import compile_messages
14
+ from .errors import FluentDuplicateMessageId , FluentJunkFound
11
15
from .prepare import Compiler
12
- from .resolver import ResolverEnvironment , CurrentEnvironment
16
+ from .resolver import CurrentEnvironment , ResolverEnvironment
13
17
from .utils import ATTRIBUTE_SEPARATOR , TERM_SIGIL , ast_to_id , native_to_fluent
14
18
15
19
16
- class FluentBundle (object ):
20
+ class FluentBundleBase (object ):
17
21
"""
18
22
Message contexts are single-language stores of translations. They are
19
23
responsible for parsing translation resources in the Fluent syntax and can
@@ -33,27 +37,60 @@ def __init__(self, locales, functions=None, use_isolating=True):
33
37
_functions .update (functions )
34
38
self ._functions = _functions
35
39
self .use_isolating = use_isolating
36
- self ._messages_and_terms = {}
37
- self ._compiled = {}
38
- self ._compiler = Compiler ()
40
+ self ._messages_and_terms = OrderedDict ()
41
+ self ._parsing_issues = []
39
42
self ._babel_locale = self ._get_babel_locale ()
40
43
self ._plural_form = babel .plural .to_python (self ._babel_locale .plural_form )
41
44
42
45
def add_messages (self , source ):
43
46
parser = FluentParser ()
44
47
resource = parser .parse (source )
45
- # TODO - warn/error about duplicates
46
48
for item in resource .body :
47
49
if isinstance (item , (Message , Term )):
48
50
full_id = ast_to_id (item )
49
- if full_id not in self ._messages_and_terms :
51
+ if full_id in self ._messages_and_terms :
52
+ self ._parsing_issues .append ((full_id , FluentDuplicateMessageId (
53
+ "Additional definition for '{0}' discarded." .format (full_id ))))
54
+ else :
50
55
self ._messages_and_terms [full_id ] = item
56
+ elif isinstance (item , Junk ):
57
+ self ._parsing_issues .append (
58
+ (None , FluentJunkFound ("Junk found: " +
59
+ '; ' .join (a .message for a in item .annotations ),
60
+ item .annotations )))
51
61
52
62
def has_message (self , message_id ):
53
63
if message_id .startswith (TERM_SIGIL ) or ATTRIBUTE_SEPARATOR in message_id :
54
64
return False
55
65
return message_id in self ._messages_and_terms
56
66
67
+ def _get_babel_locale (self ):
68
+ for l in self .locales :
69
+ try :
70
+ return babel .Locale .parse (l .replace ('-' , '_' ))
71
+ except babel .UnknownLocaleError :
72
+ continue
73
+ # TODO - log error
74
+ return babel .Locale .default ()
75
+
76
+ def format (self , message_id , args = None ):
77
+ raise NotImplementedError ()
78
+
79
+ def check_messages (self ):
80
+ """
81
+ Check messages for errors and return as a list of two tuples:
82
+ (message ID or None, exception object)
83
+ """
84
+ raise NotImplementedError ()
85
+
86
+
87
+ class InterpretingFluentBundle (FluentBundleBase ):
88
+
89
+ def __init__ (self , locales , functions = None , use_isolating = True ):
90
+ super (InterpretingFluentBundle , self ).__init__ (locales , functions = functions , use_isolating = use_isolating )
91
+ self ._compiled = {}
92
+ self ._compiler = Compiler ()
93
+
57
94
def lookup (self , full_id ):
58
95
if full_id not in self ._compiled :
59
96
entry_id = full_id .split (ATTRIBUTE_SEPARATOR , 1 )[0 ]
@@ -83,11 +120,55 @@ def format(self, message_id, args=None):
83
120
errors = errors )
84
121
return [resolve (env ), errors ]
85
122
86
- def _get_babel_locale (self ):
87
- for l in self .locales :
88
- try :
89
- return babel .Locale .parse (l .replace ('-' , '_' ))
90
- except babel .UnknownLocaleError :
91
- continue
92
- # TODO - log error
93
- return babel .Locale .default ()
123
+ def check_messages (self ):
124
+ return self ._parsing_issues [:]
125
+
126
+
127
+ class CompilingFluentBundle (FluentBundleBase ):
128
+ def __init__ (self , * args , ** kwargs ):
129
+ super (CompilingFluentBundle , self ).__init__ (* args , ** kwargs )
130
+ self ._mark_dirty ()
131
+
132
+ def _mark_dirty (self ):
133
+ self ._is_dirty = True
134
+ # Clear out old compilation errors, they might not apply if we
135
+ # re-compile:
136
+ self ._compilation_errors = []
137
+ self .format = self ._compile_and_format
138
+
139
+ def _mark_clean (self ):
140
+ self ._is_dirty = False
141
+ self .format = self ._format
142
+
143
+ def add_messages (self , source ):
144
+ super (CompilingFluentBundle , self ).add_messages (source )
145
+ self ._mark_dirty ()
146
+
147
+ def _compile (self ):
148
+ self ._compiled_messages , self ._compilation_errors = compile_messages (
149
+ self ._messages_and_terms ,
150
+ self ._babel_locale ,
151
+ use_isolating = self .use_isolating ,
152
+ functions = self ._functions )
153
+ self ._mark_clean ()
154
+
155
+ # 'format' is the hot path for many scenarios, so we try to optimize it. To
156
+ # avoid having to check '_is_dirty' inside 'format', we switch 'format' from
157
+ # '_compile_and_format' to '_format' when compilation is done. This gives us
158
+ # about 10% improvement for the simplest (but most common) case of an
159
+ # entirely static string.
160
+ def _compile_and_format (self , message_id , args = None ):
161
+ self ._compile ()
162
+ return self ._format (message_id , args )
163
+
164
+ def _format (self , message_id , args = None ):
165
+ errors = []
166
+ return self ._compiled_messages [message_id ](args , errors ), errors
167
+
168
+ def check_messages (self ):
169
+ if self ._is_dirty :
170
+ self ._compile ()
171
+ return self ._parsing_issues + self ._compilation_errors
172
+
173
+
174
+ FluentBundle = InterpretingFluentBundle
0 commit comments