Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1699)

Side by Side Diff: grit/clique.py

Issue 7994004: Initial source commit to grit-i18n project. (Closed) Base URL: http://grit-i18n.googlecode.com/svn/trunk/
Patch Set: Created 9 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « grit/__init__.py ('k') | grit/clique_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 #!/usr/bin/python2.4
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 '''Collections of messages and their translations, called cliques. Also
7 collections of cliques (uber-cliques).
8 '''
9
10 import types
11
12 from grit import constants
13 from grit import exception
14 from grit import pseudo
15 from grit import pseudo_rtl
16 from grit import tclib
17
18
19 class UberClique(object):
20 '''A factory (NOT a singleton factory) for making cliques. It has several
21 methods for working with the cliques created using the factory.
22 '''
23
24 def __init__(self):
25 # A map from message ID to list of cliques whose source messages have
26 # that ID. This will contain all cliques created using this factory.
27 # Different messages can have the same ID because they have the
28 # same translateable portion and placeholder names, but occur in different
29 # places in the resource tree.
30 self.cliques_ = {}
31
32 # A map of clique IDs to list of languages to indicate translations where we
33 # fell back to English.
34 self.fallback_translations_ = {}
35
36 # A map of clique IDs to list of languages to indicate missing translations.
37 self.missing_translations_ = {}
38
39 def _AddMissingTranslation(self, lang, clique, is_error):
40 tl = self.fallback_translations_
41 if is_error:
42 tl = self.missing_translations_
43 id = clique.GetId()
44 if id not in tl:
45 tl[id] = {}
46 if lang not in tl[id]:
47 tl[id][lang] = 1
48
49 def HasMissingTranslations(self):
50 return len(self.missing_translations_) > 0
51
52 def MissingTranslationsReport(self):
53 '''Returns a string suitable for printing to report missing
54 and fallback translations to the user.
55 '''
56 def ReportTranslation(clique, langs):
57 text = clique.GetMessage().GetPresentableContent()
58 extract = text[0:40]
59 ellipsis = ''
60 if len(text) > 40:
61 ellipsis = '...'
62 langs_extract = langs[0:6]
63 describe_langs = ','.join(langs_extract)
64 if len(langs) > 6:
65 describe_langs += " and %d more" % (len(langs) - 6)
66 return " %s \"%s%s\" %s" % (clique.GetId(), extract, ellipsis,
67 describe_langs)
68 lines = []
69 if len(self.fallback_translations_):
70 lines.append(
71 "WARNING: Fell back to English for the following translations:")
72 for (id, langs) in self.fallback_translations_.items():
73 lines.append(ReportTranslation(self.cliques_[id][0], langs.keys()))
74 if len(self.missing_translations_):
75 lines.append("ERROR: The following translations are MISSING:")
76 for (id, langs) in self.missing_translations_.items():
77 lines.append(ReportTranslation(self.cliques_[id][0], langs.keys()))
78 return '\n'.join(lines)
79
80 def MakeClique(self, message, translateable=True):
81 '''Create a new clique initialized with a message.
82
83 Args:
84 message: tclib.Message()
85 translateable: True | False
86 '''
87 clique = MessageClique(self, message, translateable)
88
89 # Enable others to find this clique by its message ID
90 if message.GetId() in self.cliques_:
91 presentable_text = clique.GetMessage().GetPresentableContent()
92 for c in self.cliques_[message.GetId()]:
93 assert c.GetMessage().GetPresentableContent() == presentable_text
94 self.cliques_[message.GetId()].append(clique)
95 else:
96 self.cliques_[message.GetId()] = [clique]
97
98 return clique
99
100 def FindCliqueAndAddTranslation(self, translation, language):
101 '''Adds the specified translation to the clique with the source message
102 it is a translation of.
103
104 Args:
105 translation: tclib.Translation()
106 language: 'en' | 'fr' ...
107
108 Return:
109 True if the source message was found, otherwise false.
110 '''
111 if translation.GetId() in self.cliques_:
112 for clique in self.cliques_[translation.GetId()]:
113 clique.AddTranslation(translation, language)
114 return True
115 else:
116 return False
117
118 def BestClique(self, id):
119 '''Returns the "best" clique from a list of cliques. All the cliques
120 must have the same ID. The "best" clique is chosen in the following
121 order of preference:
122 - The first clique that has a non-ID-based description
123 - If no such clique found, one of the cliques with an ID-based description
124 - Otherwise an arbitrary clique
125 '''
126 clique_list = self.cliques_[id]
127 clique_to_ret = None
128 for clique in clique_list:
129 if not clique_to_ret:
130 clique_to_ret = clique
131
132 description = clique.GetMessage().GetDescription()
133 if description and len(description) > 0:
134 clique_to_ret = clique
135 if not description.startswith('ID:'):
136 break # this is the preferred case so we exit right away
137 return clique_to_ret
138
139 def BestCliquePerId(self):
140 '''Iterates over the list of all cliques and returns the best clique for
141 each ID. This will be the first clique with a source message that has a
142 non-empty description, or an arbitrary clique if none of them has a
143 description.
144 '''
145 for id in self.cliques_:
146 yield self.BestClique(id)
147
148 def BestCliqueByOriginalText(self, text, meaning):
149 '''Finds the "best" (as in BestClique()) clique that has original text
150 'text' and meaning 'meaning'. Returns None if there is no such clique.
151 '''
152 # If needed, this can be optimized by maintaining a map of
153 # fingerprints of original text+meaning to cliques.
154 for c in self.BestCliquePerId():
155 msg = c.GetMessage()
156 if msg.GetRealContent() == text and msg.GetMeaning() == meaning:
157 return msg
158 return None
159
160 def AllMessageIds(self):
161 '''Returns a list of all defined message IDs.
162 '''
163 return self.cliques_.keys()
164
165 def AllCliques(self):
166 '''Iterates over all cliques. Note that this can return multiple cliques
167 with the same ID.
168 '''
169 for cliques in self.cliques_.values():
170 for c in cliques:
171 yield c
172
173 def GenerateXtbParserCallback(self, lang, debug=False):
174 '''Creates a callback function as required by grit.xtb_reader.Parse().
175 This callback will create Translation objects for each message from
176 the XTB that exists in this uberclique, and add them as translations for
177 the relevant cliques. The callback will add translations to the language
178 specified by 'lang'
179
180 Args:
181 lang: 'fr'
182 debug: True | False
183 '''
184 def Callback(id, structure):
185 if id not in self.cliques_:
186 if debug: print "Ignoring translation #%s" % id
187 return
188
189 if debug: print "Adding translation #%s" % id
190
191 # We fetch placeholder information from the original message (the XTB file
192 # only contains placeholder names).
193 original_msg = self.BestClique(id).GetMessage()
194
195 translation = tclib.Translation(id=id)
196 for is_ph,text in structure:
197 if not is_ph:
198 translation.AppendText(text)
199 else:
200 found_placeholder = False
201 for ph in original_msg.GetPlaceholders():
202 if ph.GetPresentation() == text:
203 translation.AppendPlaceholder(tclib.Placeholder(
204 ph.GetPresentation(), ph.GetOriginal(), ph.GetExample()))
205 found_placeholder = True
206 break
207 if not found_placeholder:
208 raise exception.MismatchingPlaceholders(
209 'Translation for message ID %s had <ph name="%s%/>, no match\n'
210 'in original message' % (id, text))
211 self.FindCliqueAndAddTranslation(translation, lang)
212 return Callback
213
214
215 class CustomType(object):
216 '''A base class you should implement if you wish to specify a custom type
217 for a message clique (i.e. custom validation and optional modification of
218 translations).'''
219
220 def Validate(self, message):
221 '''Returns true if the message (a tclib.Message object) is valid,
222 otherwise false.
223 '''
224 raise NotImplementedError()
225
226 def ValidateAndModify(self, lang, translation):
227 '''Returns true if the translation (a tclib.Translation object) is valid,
228 otherwise false. The language is also passed in. This method may modify
229 the translation that is passed in, if it so wishes.
230 '''
231 raise NotImplementedError()
232
233 def ModifyTextPart(self, lang, text):
234 '''If you call ModifyEachTextPart, it will turn around and call this method
235 for each text part of the translation. You should return the modified
236 version of the text, or just the original text to not change anything.
237 '''
238 raise NotImplementedError()
239
240 def ModifyEachTextPart(self, lang, translation):
241 '''Call this to easily modify one or more of the textual parts of a
242 translation. It will call ModifyTextPart for each part of the
243 translation.
244 '''
245 contents = translation.GetContent()
246 for ix in range(len(contents)):
247 if (isinstance(contents[ix], types.StringTypes)):
248 contents[ix] = self.ModifyTextPart(lang, contents[ix])
249
250
251 class OneOffCustomType(CustomType):
252 '''A very simple custom type that performs the validation expressed by
253 the input expression on all languages including the source language.
254 The expression can access the variables 'lang', 'msg' and 'text()' where 'lang '
255 is the language of 'msg', 'msg' is the message or translation being
256 validated and 'text()' returns the real contents of 'msg' (for shorthand).
257 '''
258 def __init__(self, expression):
259 self.expr = expression
260 def Validate(self, message):
261 return self.ValidateAndModify(MessageClique.source_language, message)
262 def ValidateAndModify(self, lang, msg):
263 def text():
264 return msg.GetRealContent()
265 return eval(self.expr, {},
266 {'lang' : lang,
267 'text' : text,
268 'msg' : msg,
269 })
270
271
272 class MessageClique(object):
273 '''A message along with all of its translations. Also code to bring
274 translations together with their original message.'''
275
276 # change this to the language code of Messages you add to cliques_.
277 # TODO(joi) Actually change this based on the <grit> node's source language
278 source_language = 'en'
279
280 # A constant translation we use when asked for a translation into the
281 # special language constants.CONSTANT_LANGUAGE.
282 CONSTANT_TRANSLATION = tclib.Translation(text='TTTTTT')
283
284 def __init__(self, uber_clique, message, translateable=True, custom_type=None) :
285 '''Create a new clique initialized with just a message.
286
287 Args:
288 uber_clique: Our uber-clique (collection of cliques)
289 message: tclib.Message()
290 translateable: True | False
291 custom_type: instance of clique.CustomType interface
292 '''
293 # Our parent
294 self.uber_clique = uber_clique
295 # If not translateable, we only store the original message.
296 self.translateable = translateable
297 # A mapping of language identifiers to tclib.BaseMessage and its
298 # subclasses (i.e. tclib.Message and tclib.Translation).
299 self.clique = { MessageClique.source_language : message }
300 # A list of the "shortcut groups" this clique is
301 # part of. Within any given shortcut group, no shortcut key (e.g. &J)
302 # must appear more than once in each language for all cliques that
303 # belong to the group.
304 self.shortcut_groups = []
305 # An instance of the CustomType interface, or None. If this is set, it will
306 # be used to validate the original message and translations thereof, and
307 # will also get a chance to modify translations of the message.
308 self.SetCustomType(custom_type)
309
310 def GetMessage(self):
311 '''Retrieves the tclib.Message that is the source for this clique.'''
312 return self.clique[MessageClique.source_language]
313
314 def GetId(self):
315 '''Retrieves the message ID of the messages in this clique.'''
316 return self.GetMessage().GetId()
317
318 def IsTranslateable(self):
319 return self.translateable
320
321 def AddToShortcutGroup(self, group):
322 self.shortcut_groups.append(group)
323
324 def SetCustomType(self, custom_type):
325 '''Makes this clique use custom_type for validating messages and
326 translations, and optionally modifying translations.
327 '''
328 self.custom_type = custom_type
329 if custom_type and not custom_type.Validate(self.GetMessage()):
330 raise exception.InvalidMessage(self.GetMessage().GetRealContent())
331
332 def MessageForLanguage(self, lang, pseudo_if_no_match=True, fallback_to_englis h=False):
333 '''Returns the message/translation for the specified language, providing
334 a pseudotranslation if there is no available translation and a pseudo-
335 translation is requested.
336
337 The translation of any message whatsoever in the special language
338 'x_constant' is the message "TTTTTT".
339
340 Args:
341 lang: 'en'
342 pseudo_if_no_match: True
343 fallback_to_english: False
344
345 Return:
346 tclib.BaseMessage
347 '''
348 if not self.translateable:
349 return self.GetMessage()
350
351 if lang == constants.CONSTANT_LANGUAGE:
352 return self.CONSTANT_TRANSLATION
353
354 for msglang in self.clique.keys():
355 if lang == msglang:
356 return self.clique[msglang]
357
358 if lang == constants.FAKE_BIDI:
359 return pseudo_rtl.PseudoRTLMessage(self.GetMessage())
360
361 if fallback_to_english:
362 self.uber_clique._AddMissingTranslation(lang, self, is_error=False)
363 return self.GetMessage()
364
365 # If we're not supposed to generate pseudotranslations, we add an error
366 # report to a list of errors, then fail at a higher level, so that we
367 # get a list of all messages that are missing translations.
368 if not pseudo_if_no_match:
369 self.uber_clique._AddMissingTranslation(lang, self, is_error=True)
370
371 return pseudo.PseudoMessage(self.GetMessage())
372
373 def AllMessagesThatMatch(self, lang_re, include_pseudo = True):
374 '''Returns a map of all messages that match 'lang', including the pseudo
375 translation if requested.
376
377 Args:
378 lang_re: re.compile('fr|en')
379 include_pseudo: True
380
381 Return:
382 { 'en' : tclib.Message,
383 'fr' : tclib.Translation,
384 pseudo.PSEUDO_LANG : tclib.Translation }
385 '''
386 if not self.translateable:
387 return [self.GetMessage()]
388
389 matches = {}
390 for msglang in self.clique:
391 if lang_re.match(msglang):
392 matches[msglang] = self.clique[msglang]
393
394 if include_pseudo:
395 matches[pseudo.PSEUDO_LANG] = pseudo.PseudoMessage(self.GetMessage())
396
397 return matches
398
399 def AddTranslation(self, translation, language):
400 '''Add a translation to this clique. The translation must have the same
401 ID as the message that is the source for this clique.
402
403 If this clique is not translateable, the function just returns.
404
405 Args:
406 translation: tclib.Translation()
407 language: 'en'
408
409 Throws:
410 grit.exception.InvalidTranslation if the translation you're trying to add
411 doesn't have the same message ID as the source message of this clique.
412 '''
413 if not self.translateable:
414 return
415 if translation.GetId() != self.GetId():
416 raise exception.InvalidTranslation(
417 'Msg ID %s, transl ID %s' % (self.GetId(), translation.GetId()))
418
419 assert not language in self.clique
420
421 # Because two messages can differ in the original content of their
422 # placeholders yet share the same ID (because they are otherwise the
423 # same), the translation we are getting may have different original
424 # content for placeholders than our message, yet it is still the right
425 # translation for our message (because it is for the same ID). We must
426 # therefore fetch the original content of placeholders from our original
427 # English message.
428 #
429 # See grit.clique_unittest.MessageCliqueUnittest.testSemiIdenticalCliques
430 # for a concrete explanation of why this is necessary.
431
432 original = self.MessageForLanguage(self.source_language, False)
433 if len(original.GetPlaceholders()) != len(translation.GetPlaceholders()):
434 print ("ERROR: '%s' translation of message id %s does not match" %
435 (language, translation.GetId()))
436 assert False
437
438 transl_msg = tclib.Translation(id=self.GetId(),
439 text=translation.GetPresentableContent(),
440 placeholders=original.GetPlaceholders())
441
442 if self.custom_type and not self.custom_type.ValidateAndModify(language, tra nsl_msg):
443 print "WARNING: %s translation failed validation: %s" % (
444 language, transl_msg.GetId())
445
446 self.clique[language] = transl_msg
447
OLDNEW
« no previous file with comments | « grit/__init__.py ('k') | grit/clique_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698