OLD | NEW |
(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 |
OLD | NEW |