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

Side by Side Diff: grit/clique.py

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