22from __future__ import unicode_literals
33
44import os
5+ import sys
56import codecs
67import logging
78
@@ -18,7 +19,7 @@ def getParser(path):
1819from .cldr import get_plural_categories
1920from .transforms import Source
2021from .merge import merge_resource
21- from .util import get_message
22+ from .errors import NotSupportedError , UnreadableReferenceError
2223
2324
2425class MergeContext (object ):
@@ -79,6 +80,10 @@ def read_ftl_resource(self, path):
7980 f = codecs .open (path , 'r' , 'utf8' )
8081 try :
8182 contents = f .read ()
83+ except UnicodeDecodeError as err :
84+ logger = logging .getLogger ('migrate' )
85+ logger .warn ('Unable to read file {}: {}' .format (path , err ))
86+ raise err
8287 finally :
8388 f .close ()
8489
@@ -94,7 +99,7 @@ def read_ftl_resource(self, path):
9499 logger = logging .getLogger ('migrate' )
95100 for annot in annots :
96101 msg = annot .message
97- logger .warn (u 'Syntax error in {}: {}' .format (path , msg ))
102+ logger .warn ('Syntax error in {}: {}' .format (path , msg ))
98103
99104 return ast
100105
@@ -105,52 +110,33 @@ def read_legacy_resource(self, path):
105110 # Transform the parsed result which is an iterator into a dict.
106111 return {entity .key : entity .val for entity in parser }
107112
108- def add_reference (self , path , realpath = None ):
109- """Add an FTL AST to this context's reference resources."""
110- fullpath = os .path .join (self .reference_dir , realpath or path )
111- try :
112- ast = self .read_ftl_resource (fullpath )
113- except IOError as err :
114- logger = logging .getLogger ('migrate' )
115- logger .error (u'Missing reference file: {}' .format (path ))
116- raise err
117- except UnicodeDecodeError as err :
118- logger = logging .getLogger ('migrate' )
119- logger .error (u'Error reading file {}: {}' .format (path , err ))
120- raise err
121- else :
122- self .reference_resources [path ] = ast
113+ def maybe_add_localization (self , path ):
114+ """Add a localization resource to migrate translations from.
123115
124- def add_localization ( self , path ):
125- """Add an existing localization resource .
116+ Only legacy resources can be added as migration sources. The resource
117+ may be missing on disk .
126118
127- If it's an FTL resource, add an FTL AST. Otherwise, it's a legacy
128- resource. Use a compare-locales parser to create a dict of (key,
129- string value) tuples.
119+ Uses a compare-locales parser to create a dict of (key, string value)
120+ tuples.
130121 """
131- fullpath = os .path .join (self .localization_dir , path )
132- if fullpath .endswith ('.ftl' ):
133- try :
134- ast = self .read_ftl_resource (fullpath )
135- except IOError :
136- logger = logging .getLogger ('migrate' )
137- logger .warn (u'Missing localization file: {}' .format (path ))
138- except UnicodeDecodeError as err :
139- logger = logging .getLogger ('migrate' )
140- logger .warn (u'Error reading file {}: {}' .format (path , err ))
141- else :
142- self .localization_resources [path ] = ast
122+ if path .endswith ('.ftl' ):
123+ error_message = (
124+ 'Migrating translations from Fluent files is not supported '
125+ '({})' .format (path ))
126+ logging .getLogger ('migrate' ).error (error_message )
127+ raise NotSupportedError (error_message )
128+
129+ try :
130+ fullpath = os .path .join (self .localization_dir , path )
131+ collection = self .read_legacy_resource (fullpath )
132+ except IOError :
133+ logger = logging .getLogger ('migrate' )
134+ logger .warn ('Missing localization file: {}' .format (path ))
143135 else :
144- try :
145- collection = self .read_legacy_resource (fullpath )
146- except IOError :
147- logger = logging .getLogger ('migrate' )
148- logger .warn (u'Missing localization file: {}' .format (path ))
149- else :
150- self .localization_resources [path ] = collection
136+ self .localization_resources [path ] = collection
151137
152- def add_transforms (self , path , transforms ):
153- """Define transforms for path.
138+ def add_transforms (self , path , reference , transforms ):
139+ """Define transforms for path using reference as template .
154140
155141 Each transform is an extended FTL node with `Transform` nodes as some
156142 values. Transforms are stored in their lazy AST form until
@@ -165,6 +151,22 @@ def get_sources(acc, cur):
165151 acc .add ((cur .path , cur .key ))
166152 return acc
167153
154+ refpath = os .path .join (self .reference_dir , reference )
155+ try :
156+ ast = self .read_ftl_resource (refpath )
157+ except IOError as err :
158+ error_message = 'Missing reference file: {}' .format (refpath )
159+ logging .getLogger ('migrate' ).error (error_message )
160+ raise UnreadableReferenceError (error_message )
161+ except UnicodeDecodeError as err :
162+ error_message = 'Error reading file {}: {}' .format (refpath , err )
163+ logging .getLogger ('migrate' ).error (error_message )
164+ raise UnreadableReferenceError (error_message )
165+ else :
166+ # The reference file will be used by the merge function as
167+ # a template for serializing the merge results.
168+ self .reference_resources [path ] = ast
169+
168170 for node in transforms :
169171 # Scan `node` for `Source` nodes and collect the information they
170172 # store into a set of dependencies.
@@ -175,17 +177,30 @@ def get_sources(acc, cur):
175177 path_transforms = self .transforms .setdefault (path , [])
176178 path_transforms += transforms
177179
180+ if path not in self .localization_resources :
181+ fullpath = os .path .join (self .localization_dir , path )
182+ try :
183+ ast = self .read_ftl_resource (fullpath )
184+ except IOError :
185+ logger = logging .getLogger ('migrate' )
186+ logger .info (
187+ 'Localization file {} does not exist and '
188+ 'it will be created' .format (path ))
189+ except UnicodeDecodeError :
190+ logger = logging .getLogger ('migrate' )
191+ logger .warn (
192+ 'Localization file {} will be re-created and some '
193+ 'translations might be lost' .format (path ))
194+ else :
195+ self .localization_resources [path ] = ast
196+
178197 def get_source (self , path , key ):
179- """Get an entity value from the localized source.
198+ """Get an entity value from a localized legacy source.
180199
181200 Used by the `Source` transform.
182201 """
183- if path .endswith ('.ftl' ):
184- resource = self .localization_resources [path ]
185- return get_message (resource .body , key )
186- else :
187- resource = self .localization_resources [path ]
188- return resource .get (key , None )
202+ resource = self .localization_resources [path ]
203+ return resource .get (key , None )
189204
190205 def merge_changeset (self , changeset = None ):
191206 """Return a generator of FTL ASTs for the changeset.
@@ -200,10 +215,11 @@ def merge_changeset(self, changeset=None):
200215 """
201216
202217 if changeset is None :
203- # Merge all known legacy translations.
218+ # Merge all known legacy translations. Used in tests.
204219 changeset = {
205220 (path , key )
206221 for path , strings in self .localization_resources .iteritems ()
222+ if not path .endswith ('.ftl' )
207223 for key in strings .iterkeys ()
208224 }
209225
@@ -240,10 +256,15 @@ def in_changeset(ident):
240256 self , reference , current , transforms , in_changeset
241257 )
242258
243- # If none of the transforms is in the given changeset, the merged
244- # snapshot is identical to the current translation. We compare
245- # JSON trees rather then use filtering by `in_changeset` to account
246- # for translations removed from `reference`.
259+ # Skip this path if the merged snapshot is identical to the current
260+ # state of the localization file. This may happen when:
261+ #
262+ # - none of the transforms is in the changset, or
263+ # - all messages which would be migrated by the context's
264+ # transforms already exist in the current state.
265+ #
266+ # We compare JSON trees rather then use filtering by `in_changeset`
267+ # to account for translations removed from `reference`.
247268 if snapshot .to_json () == current .to_json ():
248269 continue
249270
0 commit comments