Index: grit/clique.py |
=================================================================== |
--- grit/clique.py (revision 202) |
+++ grit/clique.py (working copy) |
@@ -1,483 +0,0 @@ |
-#!/usr/bin/env python |
-# Copyright (c) 2012 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-'''Collections of messages and their translations, called cliques. Also |
-collections of cliques (uber-cliques). |
-''' |
- |
-import re |
-import types |
- |
-from grit import constants |
-from grit import exception |
-from grit import lazy_re |
-from grit import pseudo |
-from grit import pseudo_rtl |
-from grit import tclib |
- |
- |
-class UberClique(object): |
- '''A factory (NOT a singleton factory) for making cliques. It has several |
- methods for working with the cliques created using the factory. |
- ''' |
- |
- def __init__(self): |
- # A map from message ID to list of cliques whose source messages have |
- # that ID. This will contain all cliques created using this factory. |
- # Different messages can have the same ID because they have the |
- # same translateable portion and placeholder names, but occur in different |
- # places in the resource tree. |
- # |
- # Each list of cliques is kept sorted by description, to achieve |
- # stable results from the BestClique method, see below. |
- self.cliques_ = {} |
- |
- # A map of clique IDs to list of languages to indicate translations where we |
- # fell back to English. |
- self.fallback_translations_ = {} |
- |
- # A map of clique IDs to list of languages to indicate missing translations. |
- self.missing_translations_ = {} |
- |
- def _AddMissingTranslation(self, lang, clique, is_error): |
- tl = self.fallback_translations_ |
- if is_error: |
- tl = self.missing_translations_ |
- id = clique.GetId() |
- if id not in tl: |
- tl[id] = {} |
- if lang not in tl[id]: |
- tl[id][lang] = 1 |
- |
- def HasMissingTranslations(self): |
- return len(self.missing_translations_) > 0 |
- |
- def MissingTranslationsReport(self): |
- '''Returns a string suitable for printing to report missing |
- and fallback translations to the user. |
- ''' |
- def ReportTranslation(clique, langs): |
- text = clique.GetMessage().GetPresentableContent() |
- # The text 'error' (usually 'Error:' but we are conservative) |
- # can trigger some build environments (Visual Studio, we're |
- # looking at you) to consider invocation of grit to have failed, |
- # so we make sure never to output that word. |
- extract = re.sub('(?i)error', 'REDACTED', text[0:40])[0:40] |
- ellipsis = '' |
- if len(text) > 40: |
- ellipsis = '...' |
- langs_extract = langs[0:6] |
- describe_langs = ','.join(langs_extract) |
- if len(langs) > 6: |
- describe_langs += " and %d more" % (len(langs) - 6) |
- return " %s \"%s%s\" %s" % (clique.GetId(), extract, ellipsis, |
- describe_langs) |
- lines = [] |
- if len(self.fallback_translations_): |
- lines.append( |
- "WARNING: Fell back to English for the following translations:") |
- for (id, langs) in self.fallback_translations_.items(): |
- lines.append(ReportTranslation(self.cliques_[id][0], langs.keys())) |
- if len(self.missing_translations_): |
- lines.append("ERROR: The following translations are MISSING:") |
- for (id, langs) in self.missing_translations_.items(): |
- lines.append(ReportTranslation(self.cliques_[id][0], langs.keys())) |
- return '\n'.join(lines) |
- |
- def MakeClique(self, message, translateable=True): |
- '''Create a new clique initialized with a message. |
- |
- Args: |
- message: tclib.Message() |
- translateable: True | False |
- ''' |
- clique = MessageClique(self, message, translateable) |
- |
- # Enable others to find this clique by its message ID |
- if message.GetId() in self.cliques_: |
- presentable_text = clique.GetMessage().GetPresentableContent() |
- if not message.HasAssignedId(): |
- for c in self.cliques_[message.GetId()]: |
- assert c.GetMessage().GetPresentableContent() == presentable_text |
- self.cliques_[message.GetId()].append(clique) |
- # We need to keep each list of cliques sorted by description, to |
- # achieve stable results from the BestClique method, see below. |
- self.cliques_[message.GetId()].sort( |
- key=lambda c:c.GetMessage().GetDescription()) |
- else: |
- self.cliques_[message.GetId()] = [clique] |
- |
- return clique |
- |
- def FindCliqueAndAddTranslation(self, translation, language): |
- '''Adds the specified translation to the clique with the source message |
- it is a translation of. |
- |
- Args: |
- translation: tclib.Translation() |
- language: 'en' | 'fr' ... |
- |
- Return: |
- True if the source message was found, otherwise false. |
- ''' |
- if translation.GetId() in self.cliques_: |
- for clique in self.cliques_[translation.GetId()]: |
- clique.AddTranslation(translation, language) |
- return True |
- else: |
- return False |
- |
- def BestClique(self, id): |
- '''Returns the "best" clique from a list of cliques. All the cliques |
- must have the same ID. The "best" clique is chosen in the following |
- order of preference: |
- - The first clique that has a non-ID-based description. |
- - If no such clique found, the first clique with an ID-based description. |
- - Otherwise the first clique. |
- |
- This method is stable in terms of always returning a clique with |
- an identical description (on different runs of GRIT on the same |
- data) because self.cliques_ is sorted by description. |
- ''' |
- clique_list = self.cliques_[id] |
- clique_with_id = None |
- clique_default = None |
- for clique in clique_list: |
- if not clique_default: |
- clique_default = clique |
- |
- description = clique.GetMessage().GetDescription() |
- if description and len(description) > 0: |
- if not description.startswith('ID:'): |
- # this is the preferred case so we exit right away |
- return clique |
- elif not clique_with_id: |
- clique_with_id = clique |
- if clique_with_id: |
- return clique_with_id |
- else: |
- return clique_default |
- |
- def BestCliquePerId(self): |
- '''Iterates over the list of all cliques and returns the best clique for |
- each ID. This will be the first clique with a source message that has a |
- non-empty description, or an arbitrary clique if none of them has a |
- description. |
- ''' |
- for id in self.cliques_: |
- yield self.BestClique(id) |
- |
- def BestCliqueByOriginalText(self, text, meaning): |
- '''Finds the "best" (as in BestClique()) clique that has original text |
- 'text' and meaning 'meaning'. Returns None if there is no such clique. |
- ''' |
- # If needed, this can be optimized by maintaining a map of |
- # fingerprints of original text+meaning to cliques. |
- for c in self.BestCliquePerId(): |
- msg = c.GetMessage() |
- if msg.GetRealContent() == text and msg.GetMeaning() == meaning: |
- return msg |
- return None |
- |
- def AllMessageIds(self): |
- '''Returns a list of all defined message IDs. |
- ''' |
- return self.cliques_.keys() |
- |
- def AllCliques(self): |
- '''Iterates over all cliques. Note that this can return multiple cliques |
- with the same ID. |
- ''' |
- for cliques in self.cliques_.values(): |
- for c in cliques: |
- yield c |
- |
- def GenerateXtbParserCallback(self, lang, debug=False): |
- '''Creates a callback function as required by grit.xtb_reader.Parse(). |
- This callback will create Translation objects for each message from |
- the XTB that exists in this uberclique, and add them as translations for |
- the relevant cliques. The callback will add translations to the language |
- specified by 'lang' |
- |
- Args: |
- lang: 'fr' |
- debug: True | False |
- ''' |
- def Callback(id, structure): |
- if id not in self.cliques_: |
- if debug: print "Ignoring translation #%s" % id |
- return |
- |
- if debug: print "Adding translation #%s" % id |
- |
- # We fetch placeholder information from the original message (the XTB file |
- # only contains placeholder names). |
- original_msg = self.BestClique(id).GetMessage() |
- |
- translation = tclib.Translation(id=id) |
- for is_ph,text in structure: |
- if not is_ph: |
- translation.AppendText(text) |
- else: |
- found_placeholder = False |
- for ph in original_msg.GetPlaceholders(): |
- if ph.GetPresentation() == text: |
- translation.AppendPlaceholder(tclib.Placeholder( |
- ph.GetPresentation(), ph.GetOriginal(), ph.GetExample())) |
- found_placeholder = True |
- break |
- if not found_placeholder: |
- raise exception.MismatchingPlaceholders( |
- 'Translation for message ID %s had <ph name="%s"/>, no match\n' |
- 'in original message' % (id, text)) |
- self.FindCliqueAndAddTranslation(translation, lang) |
- return Callback |
- |
- |
-class CustomType(object): |
- '''A base class you should implement if you wish to specify a custom type |
- for a message clique (i.e. custom validation and optional modification of |
- translations).''' |
- |
- def Validate(self, message): |
- '''Returns true if the message (a tclib.Message object) is valid, |
- otherwise false. |
- ''' |
- raise NotImplementedError() |
- |
- def ValidateAndModify(self, lang, translation): |
- '''Returns true if the translation (a tclib.Translation object) is valid, |
- otherwise false. The language is also passed in. This method may modify |
- the translation that is passed in, if it so wishes. |
- ''' |
- raise NotImplementedError() |
- |
- def ModifyTextPart(self, lang, text): |
- '''If you call ModifyEachTextPart, it will turn around and call this method |
- for each text part of the translation. You should return the modified |
- version of the text, or just the original text to not change anything. |
- ''' |
- raise NotImplementedError() |
- |
- def ModifyEachTextPart(self, lang, translation): |
- '''Call this to easily modify one or more of the textual parts of a |
- translation. It will call ModifyTextPart for each part of the |
- translation. |
- ''' |
- contents = translation.GetContent() |
- for ix in range(len(contents)): |
- if (isinstance(contents[ix], types.StringTypes)): |
- contents[ix] = self.ModifyTextPart(lang, contents[ix]) |
- |
- |
-class OneOffCustomType(CustomType): |
- '''A very simple custom type that performs the validation expressed by |
- the input expression on all languages including the source language. |
- The expression can access the variables 'lang', 'msg' and 'text()' where 'lang' |
- is the language of 'msg', 'msg' is the message or translation being |
- validated and 'text()' returns the real contents of 'msg' (for shorthand). |
- ''' |
- def __init__(self, expression): |
- self.expr = expression |
- def Validate(self, message): |
- return self.ValidateAndModify(MessageClique.source_language, message) |
- def ValidateAndModify(self, lang, msg): |
- def text(): |
- return msg.GetRealContent() |
- return eval(self.expr, {}, |
- {'lang' : lang, |
- 'text' : text, |
- 'msg' : msg, |
- }) |
- |
- |
-class MessageClique(object): |
- '''A message along with all of its translations. Also code to bring |
- translations together with their original message.''' |
- |
- # change this to the language code of Messages you add to cliques_. |
- # TODO(joi) Actually change this based on the <grit> node's source language |
- source_language = 'en' |
- |
- # A constant translation we use when asked for a translation into the |
- # special language constants.CONSTANT_LANGUAGE. |
- CONSTANT_TRANSLATION = tclib.Translation(text='TTTTTT') |
- |
- # A pattern to match messages that are empty or whitespace only. |
- WHITESPACE_MESSAGE = lazy_re.compile(u'^\s*$') |
- |
- def __init__(self, uber_clique, message, translateable=True, custom_type=None): |
- '''Create a new clique initialized with just a message. |
- |
- Note that messages with a body comprised only of whitespace will implicitly |
- be marked non-translatable. |
- |
- Args: |
- uber_clique: Our uber-clique (collection of cliques) |
- message: tclib.Message() |
- translateable: True | False |
- custom_type: instance of clique.CustomType interface |
- ''' |
- # Our parent |
- self.uber_clique = uber_clique |
- # If not translateable, we only store the original message. |
- self.translateable = translateable |
- |
- # We implicitly mark messages that have a whitespace-only body as |
- # non-translateable. |
- if MessageClique.WHITESPACE_MESSAGE.match(message.GetRealContent()): |
- self.translateable = False |
- |
- # A mapping of language identifiers to tclib.BaseMessage and its |
- # subclasses (i.e. tclib.Message and tclib.Translation). |
- self.clique = { MessageClique.source_language : message } |
- # A list of the "shortcut groups" this clique is |
- # part of. Within any given shortcut group, no shortcut key (e.g. &J) |
- # must appear more than once in each language for all cliques that |
- # belong to the group. |
- self.shortcut_groups = [] |
- # An instance of the CustomType interface, or None. If this is set, it will |
- # be used to validate the original message and translations thereof, and |
- # will also get a chance to modify translations of the message. |
- self.SetCustomType(custom_type) |
- |
- def GetMessage(self): |
- '''Retrieves the tclib.Message that is the source for this clique.''' |
- return self.clique[MessageClique.source_language] |
- |
- def GetId(self): |
- '''Retrieves the message ID of the messages in this clique.''' |
- return self.GetMessage().GetId() |
- |
- def IsTranslateable(self): |
- return self.translateable |
- |
- def AddToShortcutGroup(self, group): |
- self.shortcut_groups.append(group) |
- |
- def SetCustomType(self, custom_type): |
- '''Makes this clique use custom_type for validating messages and |
- translations, and optionally modifying translations. |
- ''' |
- self.custom_type = custom_type |
- if custom_type and not custom_type.Validate(self.GetMessage()): |
- raise exception.InvalidMessage(self.GetMessage().GetRealContent()) |
- |
- def MessageForLanguage(self, lang, pseudo_if_no_match=True, fallback_to_english=False): |
- '''Returns the message/translation for the specified language, providing |
- a pseudotranslation if there is no available translation and a pseudo- |
- translation is requested. |
- |
- The translation of any message whatsoever in the special language |
- 'x_constant' is the message "TTTTTT". |
- |
- Args: |
- lang: 'en' |
- pseudo_if_no_match: True |
- fallback_to_english: False |
- |
- Return: |
- tclib.BaseMessage |
- ''' |
- if not self.translateable: |
- return self.GetMessage() |
- |
- if lang == constants.CONSTANT_LANGUAGE: |
- return self.CONSTANT_TRANSLATION |
- |
- for msglang in self.clique.keys(): |
- if lang == msglang: |
- return self.clique[msglang] |
- |
- if lang == constants.FAKE_BIDI: |
- return pseudo_rtl.PseudoRTLMessage(self.GetMessage()) |
- |
- if fallback_to_english: |
- self.uber_clique._AddMissingTranslation(lang, self, is_error=False) |
- return self.GetMessage() |
- |
- # If we're not supposed to generate pseudotranslations, we add an error |
- # report to a list of errors, then fail at a higher level, so that we |
- # get a list of all messages that are missing translations. |
- if not pseudo_if_no_match: |
- self.uber_clique._AddMissingTranslation(lang, self, is_error=True) |
- |
- return pseudo.PseudoMessage(self.GetMessage()) |
- |
- def AllMessagesThatMatch(self, lang_re, include_pseudo = True): |
- '''Returns a map of all messages that match 'lang', including the pseudo |
- translation if requested. |
- |
- Args: |
- lang_re: re.compile('fr|en') |
- include_pseudo: True |
- |
- Return: |
- { 'en' : tclib.Message, |
- 'fr' : tclib.Translation, |
- pseudo.PSEUDO_LANG : tclib.Translation } |
- ''' |
- if not self.translateable: |
- return [self.GetMessage()] |
- |
- matches = {} |
- for msglang in self.clique: |
- if lang_re.match(msglang): |
- matches[msglang] = self.clique[msglang] |
- |
- if include_pseudo: |
- matches[pseudo.PSEUDO_LANG] = pseudo.PseudoMessage(self.GetMessage()) |
- |
- return matches |
- |
- def AddTranslation(self, translation, language): |
- '''Add a translation to this clique. The translation must have the same |
- ID as the message that is the source for this clique. |
- |
- If this clique is not translateable, the function just returns. |
- |
- Args: |
- translation: tclib.Translation() |
- language: 'en' |
- |
- Throws: |
- grit.exception.InvalidTranslation if the translation you're trying to add |
- doesn't have the same message ID as the source message of this clique. |
- ''' |
- if not self.translateable: |
- return |
- if translation.GetId() != self.GetId(): |
- raise exception.InvalidTranslation( |
- 'Msg ID %s, transl ID %s' % (self.GetId(), translation.GetId())) |
- |
- assert not language in self.clique |
- |
- # Because two messages can differ in the original content of their |
- # placeholders yet share the same ID (because they are otherwise the |
- # same), the translation we are getting may have different original |
- # content for placeholders than our message, yet it is still the right |
- # translation for our message (because it is for the same ID). We must |
- # therefore fetch the original content of placeholders from our original |
- # English message. |
- # |
- # See grit.clique_unittest.MessageCliqueUnittest.testSemiIdenticalCliques |
- # for a concrete explanation of why this is necessary. |
- |
- original = self.MessageForLanguage(self.source_language, False) |
- if len(original.GetPlaceholders()) != len(translation.GetPlaceholders()): |
- print ("ERROR: '%s' translation of message id %s does not match" % |
- (language, translation.GetId())) |
- assert False |
- |
- transl_msg = tclib.Translation(id=self.GetId(), |
- text=translation.GetPresentableContent(), |
- placeholders=original.GetPlaceholders()) |
- |
- if self.custom_type and not self.custom_type.ValidateAndModify(language, transl_msg): |
- print "WARNING: %s translation failed validation: %s" % ( |
- language, transl_msg.GetId()) |
- |
- self.clique[language] = transl_msg |
- |