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

Unified 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « grit/__init__.py ('k') | grit/clique_unittest.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: grit/clique.py
===================================================================
--- grit/clique.py (revision 0)
+++ grit/clique.py (revision 0)
@@ -0,0 +1,447 @@
+#!/usr/bin/python2.4
+# Copyright (c) 2011 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 types
+
+from grit import constants
+from grit import exception
+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.
+ 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()
+ extract = text[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()
+ for c in self.cliques_[message.GetId()]:
+ assert c.GetMessage().GetPresentableContent() == presentable_text
+ self.cliques_[message.GetId()].append(clique)
+ 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, one of the cliques with an ID-based description
+ - Otherwise an arbitrary clique
+ '''
+ clique_list = self.cliques_[id]
+ clique_to_ret = None
+ for clique in clique_list:
+ if not clique_to_ret:
+ clique_to_ret = clique
+
+ description = clique.GetMessage().GetDescription()
+ if description and len(description) > 0:
+ clique_to_ret = clique
+ if not description.startswith('ID:'):
+ break # this is the preferred case so we exit right away
+ return clique_to_ret
+
+ 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')
+
+ def __init__(self, uber_clique, message, translateable=True, custom_type=None):
+ '''Create a new clique initialized with just a message.
+
+ 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
+ # 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
+
Property changes on: grit/clique.py
___________________________________________________________________
Added: svn:eol-style
+ LF
« 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