195640e3a20adea634b4df4ccf8c93f411184c438joi@chromium.org#!/usr/bin/env python
295640e3a20adea634b4df4ccf8c93f411184c438joi@chromium.org# Copyright (c) 2012 The Chromium Authors. All rights reserved.
301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org# Use of this source code is governed by a BSD-style license that can be
401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org# found in the LICENSE file.
501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org'''Collections of messages and their translations, called cliques.  Also
701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgcollections of cliques (uber-cliques).
801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org'''
901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
1078ea93aa6fc9476de2a2bd3b3d337190275bcbdcjoi@chromium.orgimport re
1101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgimport types
1201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
1301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgfrom grit import constants
1401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgfrom grit import exception
151eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.orgfrom grit import lazy_re
1601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgfrom grit import pseudo
1701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgfrom grit import pseudo_rtl
1801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgfrom grit import tclib
1901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
2001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
2101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgclass UberClique(object):
2201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  '''A factory (NOT a singleton factory) for making cliques.  It has several
2301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  methods for working with the cliques created using the factory.
2401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  '''
2501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
2601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def __init__(self):
2701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # A map from message ID to list of cliques whose source messages have
2801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # that ID.  This will contain all cliques created using this factory.
2901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # Different messages can have the same ID because they have the
3001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # same translateable portion and placeholder names, but occur in different
3101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # places in the resource tree.
32834f421e536722c36c18a89469f367eb28868068joi@chromium.org    #
33834f421e536722c36c18a89469f367eb28868068joi@chromium.org    # Each list of cliques is kept sorted by description, to achieve
34834f421e536722c36c18a89469f367eb28868068joi@chromium.org    # stable results from the BestClique method, see below.
3501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.cliques_ = {}
3601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
3701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # A map of clique IDs to list of languages to indicate translations where we
3801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # fell back to English.
3901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.fallback_translations_ = {}
4001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
4101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # A map of clique IDs to list of languages to indicate missing translations.
4201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.missing_translations_ = {}
4301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
4401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def _AddMissingTranslation(self, lang, clique, is_error):
4501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    tl = self.fallback_translations_
4601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if is_error:
4701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      tl = self.missing_translations_
4801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    id = clique.GetId()
4901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if id not in tl:
5001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      tl[id] = {}
5101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if lang not in tl[id]:
5201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      tl[id][lang] = 1
5301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
5401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def HasMissingTranslations(self):
5501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return len(self.missing_translations_) > 0
5601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
5701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def MissingTranslationsReport(self):
5801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Returns a string suitable for printing to report missing
5901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    and fallback translations to the user.
6001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
6101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    def ReportTranslation(clique, langs):
6201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      text = clique.GetMessage().GetPresentableContent()
6378ea93aa6fc9476de2a2bd3b3d337190275bcbdcjoi@chromium.org      # The text 'error' (usually 'Error:' but we are conservative)
6478ea93aa6fc9476de2a2bd3b3d337190275bcbdcjoi@chromium.org      # can trigger some build environments (Visual Studio, we're
6578ea93aa6fc9476de2a2bd3b3d337190275bcbdcjoi@chromium.org      # looking at you) to consider invocation of grit to have failed,
6678ea93aa6fc9476de2a2bd3b3d337190275bcbdcjoi@chromium.org      # so we make sure never to output that word.
6778ea93aa6fc9476de2a2bd3b3d337190275bcbdcjoi@chromium.org      extract = re.sub('(?i)error', 'REDACTED', text[0:40])[0:40]
6801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      ellipsis = ''
6901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      if len(text) > 40:
7001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        ellipsis = '...'
7101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      langs_extract = langs[0:6]
7201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      describe_langs = ','.join(langs_extract)
7301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      if len(langs) > 6:
7401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        describe_langs += " and %d more" % (len(langs) - 6)
7501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return "  %s \"%s%s\" %s" % (clique.GetId(), extract, ellipsis,
7601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org                                   describe_langs)
7701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    lines = []
7801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if len(self.fallback_translations_):
7901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      lines.append(
8001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        "WARNING: Fell back to English for the following translations:")
8101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      for (id, langs) in self.fallback_translations_.items():
8201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        lines.append(ReportTranslation(self.cliques_[id][0], langs.keys()))
8301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if len(self.missing_translations_):
8401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      lines.append("ERROR: The following translations are MISSING:")
8501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      for (id, langs) in self.missing_translations_.items():
8601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        lines.append(ReportTranslation(self.cliques_[id][0], langs.keys()))
8701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return '\n'.join(lines)
8801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
8901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def MakeClique(self, message, translateable=True):
9001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Create a new clique initialized  with a message.
9101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
9201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Args:
9301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      message: tclib.Message()
9401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      translateable: True | False
9501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
9601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    clique = MessageClique(self, message, translateable)
9701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
9801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # Enable others to find this clique by its message ID
9901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if message.GetId() in self.cliques_:
10001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      presentable_text = clique.GetMessage().GetPresentableContent()
101ca6c225d0059dd17b476c934e744a3c4f60e2df2joi@chromium.org      if not message.HasAssignedId():
102ca6c225d0059dd17b476c934e744a3c4f60e2df2joi@chromium.org        for c in self.cliques_[message.GetId()]:
103ca6c225d0059dd17b476c934e744a3c4f60e2df2joi@chromium.org          assert c.GetMessage().GetPresentableContent() == presentable_text
10401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      self.cliques_[message.GetId()].append(clique)
105834f421e536722c36c18a89469f367eb28868068joi@chromium.org      # We need to keep each list of cliques sorted by description, to
106834f421e536722c36c18a89469f367eb28868068joi@chromium.org      # achieve stable results from the BestClique method, see below.
107834f421e536722c36c18a89469f367eb28868068joi@chromium.org      self.cliques_[message.GetId()].sort(
108834f421e536722c36c18a89469f367eb28868068joi@chromium.org          key=lambda c:c.GetMessage().GetDescription())
10901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    else:
11001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      self.cliques_[message.GetId()] = [clique]
11101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
11201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return clique
11301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
11401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def FindCliqueAndAddTranslation(self, translation, language):
11501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Adds the specified translation to the clique with the source message
11601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    it is a translation of.
11701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
11801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Args:
11901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      translation: tclib.Translation()
12001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      language: 'en' | 'fr' ...
12101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
12201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Return:
12301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      True if the source message was found, otherwise false.
12401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
12501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if translation.GetId() in self.cliques_:
12601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      for clique in self.cliques_[translation.GetId()]:
12701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        clique.AddTranslation(translation, language)
12801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return True
12901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    else:
13001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return False
13101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
13201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def BestClique(self, id):
13301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Returns the "best" clique from a list of cliques.  All the cliques
13401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    must have the same ID.  The "best" clique is chosen in the following
13501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    order of preference:
136834f421e536722c36c18a89469f367eb28868068joi@chromium.org    - The first clique that has a non-ID-based description.
137834f421e536722c36c18a89469f367eb28868068joi@chromium.org    - If no such clique found, the first clique with an ID-based description.
138834f421e536722c36c18a89469f367eb28868068joi@chromium.org    - Otherwise the first clique.
139834f421e536722c36c18a89469f367eb28868068joi@chromium.org
140834f421e536722c36c18a89469f367eb28868068joi@chromium.org    This method is stable in terms of always returning a clique with
141834f421e536722c36c18a89469f367eb28868068joi@chromium.org    an identical description (on different runs of GRIT on the same
142834f421e536722c36c18a89469f367eb28868068joi@chromium.org    data) because self.cliques_ is sorted by description.
14301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
14401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    clique_list = self.cliques_[id]
145834f421e536722c36c18a89469f367eb28868068joi@chromium.org    clique_with_id = None
146834f421e536722c36c18a89469f367eb28868068joi@chromium.org    clique_default = None
14701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    for clique in clique_list:
148834f421e536722c36c18a89469f367eb28868068joi@chromium.org      if not clique_default:
149834f421e536722c36c18a89469f367eb28868068joi@chromium.org        clique_default = clique
15001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
15101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      description = clique.GetMessage().GetDescription()
15201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      if description and len(description) > 0:
15301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        if not description.startswith('ID:'):
154834f421e536722c36c18a89469f367eb28868068joi@chromium.org          # this is the preferred case so we exit right away
155834f421e536722c36c18a89469f367eb28868068joi@chromium.org          return clique
156834f421e536722c36c18a89469f367eb28868068joi@chromium.org        elif not clique_with_id:
157834f421e536722c36c18a89469f367eb28868068joi@chromium.org          clique_with_id = clique
158834f421e536722c36c18a89469f367eb28868068joi@chromium.org    if clique_with_id:
159834f421e536722c36c18a89469f367eb28868068joi@chromium.org      return clique_with_id
160834f421e536722c36c18a89469f367eb28868068joi@chromium.org    else:
161834f421e536722c36c18a89469f367eb28868068joi@chromium.org      return clique_default
16201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
16301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def BestCliquePerId(self):
16401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Iterates over the list of all cliques and returns the best clique for
16501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    each ID.  This will be the first clique with a source message that has a
16601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    non-empty description, or an arbitrary clique if none of them has a
16701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    description.
16801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
16901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    for id in self.cliques_:
17001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      yield self.BestClique(id)
17101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
17201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def BestCliqueByOriginalText(self, text, meaning):
17301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Finds the "best" (as in BestClique()) clique that has original text
17401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    'text' and meaning 'meaning'.  Returns None if there is no such clique.
17501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
17601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # If needed, this can be optimized by maintaining a map of
17701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # fingerprints of original text+meaning to cliques.
17801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    for c in self.BestCliquePerId():
17901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      msg = c.GetMessage()
18001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      if msg.GetRealContent() == text and msg.GetMeaning() == meaning:
18101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        return msg
18201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return None
18301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
18401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def AllMessageIds(self):
18501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Returns a list of all defined message IDs.
18601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
18701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return self.cliques_.keys()
18801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
18901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def AllCliques(self):
19001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Iterates over all cliques.  Note that this can return multiple cliques
19101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    with the same ID.
19201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
19301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    for cliques in self.cliques_.values():
19401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      for c in cliques:
19501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        yield c
19601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
19701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def GenerateXtbParserCallback(self, lang, debug=False):
19801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Creates a callback function as required by grit.xtb_reader.Parse().
19901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    This callback will create Translation objects for each message from
20001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    the XTB that exists in this uberclique, and add them as translations for
20101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    the relevant cliques.  The callback will add translations to the language
20201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    specified by 'lang'
20301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
20401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Args:
20501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      lang: 'fr'
20601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      debug: True | False
20701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
20801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    def Callback(id, structure):
20901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      if id not in self.cliques_:
21001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        if debug: print "Ignoring translation #%s" % id
21101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        return
21201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
21301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      if debug: print "Adding translation #%s" % id
21401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
21501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      # We fetch placeholder information from the original message (the XTB file
21601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      # only contains placeholder names).
21701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      original_msg = self.BestClique(id).GetMessage()
21801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
21901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      translation = tclib.Translation(id=id)
22001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      for is_ph,text in structure:
22101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        if not is_ph:
22201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org          translation.AppendText(text)
22301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        else:
22401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org          found_placeholder = False
22501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org          for ph in original_msg.GetPlaceholders():
22601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org            if ph.GetPresentation() == text:
22701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org              translation.AppendPlaceholder(tclib.Placeholder(
22801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org                ph.GetPresentation(), ph.GetOriginal(), ph.GetExample()))
22901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org              found_placeholder = True
23001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org              break
23101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org          if not found_placeholder:
23201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org            raise exception.MismatchingPlaceholders(
233bd79a1642abbe801db78778a59cdafc10e70bcccjoi@chromium.org              'Translation for message ID %s had <ph name="%s"/>, no match\n'
23401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org              'in original message' % (id, text))
23501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      self.FindCliqueAndAddTranslation(translation, lang)
23601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return Callback
23701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
23801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
23901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgclass CustomType(object):
24001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  '''A base class you should implement if you wish to specify a custom type
24101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  for a message clique (i.e. custom validation and optional modification of
24201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  translations).'''
24301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
24401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def Validate(self, message):
24501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Returns true if the message (a tclib.Message object) is valid,
24601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    otherwise false.
24701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
24801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    raise NotImplementedError()
24901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
25001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def ValidateAndModify(self, lang, translation):
25101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Returns true if the translation (a tclib.Translation object) is valid,
25201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    otherwise false.  The language is also passed in.  This method may modify
25301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    the translation that is passed in, if it so wishes.
25401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
25501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    raise NotImplementedError()
25601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
25701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def ModifyTextPart(self, lang, text):
25801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''If you call ModifyEachTextPart, it will turn around and call this method
25901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    for each text part of the translation.  You should return the modified
26001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    version of the text, or just the original text to not change anything.
26101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
26201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    raise NotImplementedError()
26301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
26401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def ModifyEachTextPart(self, lang, translation):
26501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Call this to easily modify one or more of the textual parts of a
26601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    translation.  It will call ModifyTextPart for each part of the
26701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    translation.
26801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
26901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    contents = translation.GetContent()
27001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    for ix in range(len(contents)):
27101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      if (isinstance(contents[ix], types.StringTypes)):
27201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        contents[ix] = self.ModifyTextPart(lang, contents[ix])
27301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
27401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
27501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgclass OneOffCustomType(CustomType):
27601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  '''A very simple custom type that performs the validation expressed by
27701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  the input expression on all languages including the source language.
27801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  The expression can access the variables 'lang', 'msg' and 'text()' where 'lang'
27901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  is the language of 'msg', 'msg' is the message or translation being
28001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  validated and 'text()' returns the real contents of 'msg' (for shorthand).
28101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  '''
28201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def __init__(self, expression):
28301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.expr = expression
28401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def Validate(self, message):
28501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return self.ValidateAndModify(MessageClique.source_language, message)
28601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def ValidateAndModify(self, lang, msg):
28701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    def text():
28801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return msg.GetRealContent()
28901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return eval(self.expr, {},
29001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org            {'lang' : lang,
29101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org             'text' : text,
29201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org             'msg' : msg,
29301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org             })
29401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
29501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
29601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.orgclass MessageClique(object):
29701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  '''A message along with all of its translations.  Also code to bring
29801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  translations together with their original message.'''
29901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
30001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  # change this to the language code of Messages you add to cliques_.
30101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  # TODO(joi) Actually change this based on the <grit> node's source language
30201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  source_language = 'en'
30301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
30401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  # A constant translation we use when asked for a translation into the
30501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  # special language constants.CONSTANT_LANGUAGE.
30601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  CONSTANT_TRANSLATION = tclib.Translation(text='TTTTTT')
30701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
3081eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org  # A pattern to match messages that are empty or whitespace only.
309eda8d69f7c49419eef6e96a1cd42af26a864d794joi@chromium.org  WHITESPACE_MESSAGE = lazy_re.compile(u'^\s*$')
3101eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org
31101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def __init__(self, uber_clique, message, translateable=True, custom_type=None):
31201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Create a new clique initialized with just a message.
31301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
3141eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org    Note that messages with a body comprised only of whitespace will implicitly
3151eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org    be marked non-translatable.
3161eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org
31701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Args:
31801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      uber_clique: Our uber-clique (collection of cliques)
31901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      message: tclib.Message()
32001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      translateable: True | False
32101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      custom_type: instance of clique.CustomType interface
32201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
32301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # Our parent
32401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.uber_clique = uber_clique
32501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # If not translateable, we only store the original message.
32601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.translateable = translateable
3271eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org
3281eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org    # We implicitly mark messages that have a whitespace-only body as
3291eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org    # non-translateable.
3301eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org    if MessageClique.WHITESPACE_MESSAGE.match(message.GetRealContent()):
3311eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org      self.translateable = False
3321eddb32f808ac5930f24b1954b5c949b273a3869joi@chromium.org
33301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # A mapping of language identifiers to tclib.BaseMessage and its
33401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # subclasses (i.e. tclib.Message and tclib.Translation).
33501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.clique = { MessageClique.source_language : message }
33601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # A list of the "shortcut groups" this clique is
33701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # part of.  Within any given shortcut group, no shortcut key (e.g. &J)
33801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # must appear more than once in each language for all cliques that
33901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # belong to the group.
34001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.shortcut_groups = []
34101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # An instance of the CustomType interface, or None.  If this is set, it will
34201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # be used to validate the original message and translations thereof, and
34301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # will also get a chance to modify translations of the message.
34401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.SetCustomType(custom_type)
34501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
34601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def GetMessage(self):
34701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Retrieves the tclib.Message that is the source for this clique.'''
34801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return self.clique[MessageClique.source_language]
34901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
35001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def GetId(self):
35101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Retrieves the message ID of the messages in this clique.'''
35201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return self.GetMessage().GetId()
35301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
35401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def IsTranslateable(self):
35501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return self.translateable
35601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
35701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def AddToShortcutGroup(self, group):
35801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.shortcut_groups.append(group)
35901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
36001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def SetCustomType(self, custom_type):
36101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Makes this clique use custom_type for validating messages and
36201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    translations, and optionally modifying translations.
36301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
36401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.custom_type = custom_type
36501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if custom_type and not custom_type.Validate(self.GetMessage()):
36601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      raise exception.InvalidMessage(self.GetMessage().GetRealContent())
36701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
36801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def MessageForLanguage(self, lang, pseudo_if_no_match=True, fallback_to_english=False):
36901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Returns the message/translation for the specified language, providing
37001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    a pseudotranslation if there is no available translation and a pseudo-
37101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    translation is requested.
37201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
37301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    The translation of any message whatsoever in the special language
37401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    'x_constant' is the message "TTTTTT".
37501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
37601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Args:
37701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      lang: 'en'
37801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      pseudo_if_no_match: True
37901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      fallback_to_english: False
38001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
38101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Return:
38201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      tclib.BaseMessage
38301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
38401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if not self.translateable:
38501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return self.GetMessage()
38601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
38701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if lang == constants.CONSTANT_LANGUAGE:
38801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return self.CONSTANT_TRANSLATION
38901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
39001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    for msglang in self.clique.keys():
39101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      if lang == msglang:
39201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        return self.clique[msglang]
39301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
39401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if lang == constants.FAKE_BIDI:
39501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return pseudo_rtl.PseudoRTLMessage(self.GetMessage())
39601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
39701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if fallback_to_english:
39801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      self.uber_clique._AddMissingTranslation(lang, self, is_error=False)
39901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return self.GetMessage()
40001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
40101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # If we're not supposed to generate pseudotranslations, we add an error
40201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # report to a list of errors, then fail at a higher level, so that we
40301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # get a list of all messages that are missing translations.
40401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if not pseudo_if_no_match:
40501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      self.uber_clique._AddMissingTranslation(lang, self, is_error=True)
40601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
40701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return pseudo.PseudoMessage(self.GetMessage())
40801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
40901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def AllMessagesThatMatch(self, lang_re, include_pseudo = True):
41001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Returns a map of all messages that match 'lang', including the pseudo
41101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    translation if requested.
41201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
41301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Args:
41401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      lang_re: re.compile('fr|en')
41501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      include_pseudo: True
41601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
41701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Return:
41801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      { 'en' : tclib.Message,
41901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        'fr' : tclib.Translation,
42001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        pseudo.PSEUDO_LANG : tclib.Translation }
42101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
42201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if not self.translateable:
42301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return [self.GetMessage()]
42401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
42501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    matches = {}
42601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    for msglang in self.clique:
42701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      if lang_re.match(msglang):
42801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        matches[msglang] = self.clique[msglang]
42901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
43001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if include_pseudo:
43101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      matches[pseudo.PSEUDO_LANG] = pseudo.PseudoMessage(self.GetMessage())
43201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
43301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    return matches
43401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
43501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org  def AddTranslation(self, translation, language):
43601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''Add a translation to this clique.  The translation must have the same
43701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    ID as the message that is the source for this clique.
43801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
43901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    If this clique is not translateable, the function just returns.
44001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
44101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Args:
44201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      translation: tclib.Translation()
44301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      language: 'en'
44401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
44501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    Throws:
44601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      grit.exception.InvalidTranslation if the translation you're trying to add
44701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      doesn't have the same message ID as the source message of this clique.
44801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    '''
44901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if not self.translateable:
45001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      return
45101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if translation.GetId() != self.GetId():
45201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      raise exception.InvalidTranslation(
45301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        'Msg ID %s, transl ID %s' % (self.GetId(), translation.GetId()))
45401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
45501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    assert not language in self.clique
45601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
45701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # Because two messages can differ in the original content of their
45801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # placeholders yet share the same ID (because they are otherwise the
45901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # same), the translation we are getting may have different original
46001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # content for placeholders than our message, yet it is still the right
46101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # translation for our message (because it is for the same ID).  We must
46201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # therefore fetch the original content of placeholders from our original
46301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # English message.
46401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    #
46501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # See grit.clique_unittest.MessageCliqueUnittest.testSemiIdenticalCliques
46601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    # for a concrete explanation of why this is necessary.
46701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
46801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    original = self.MessageForLanguage(self.source_language, False)
46901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if len(original.GetPlaceholders()) != len(translation.GetPlaceholders()):
47001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      print ("ERROR: '%s' translation of message id %s does not match" %
47101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org             (language, translation.GetId()))
47201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      assert False
47301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
47401b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    transl_msg = tclib.Translation(id=self.GetId(),
47501b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org                                   text=translation.GetPresentableContent(),
47601b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org                                   placeholders=original.GetPlaceholders())
47701b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
47801b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    if self.custom_type and not self.custom_type.ValidateAndModify(language, transl_msg):
47901b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org      print "WARNING: %s translation failed validation: %s" % (
48001b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org        language, transl_msg.GetId())
48101b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
48201b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org    self.clique[language] = transl_msg
48301b3bc768461bd303bff39f8cd1663682254e407joi@chromium.org
484