| 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 |