• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# Copyright (c) 2012 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# The tclib module contains tools for aggregating, verifying, and storing
7# messages destined for the Translation Console, as well as for reading
8# translations back and outputting them in some desired format.
9#
10# This has been stripped down to include only the functionality needed by grit
11# for creating Windows .rc and .h files.  These are the only parts needed by
12# the Chrome build process.
13
14import exceptions
15
16from grit.extern import FP
17
18# This module assumes that within a bundle no two messages can have the
19# same id unless they're identical.
20
21# The basic classes defined here for external use are Message and Translation,
22# where the former is used for English messages and the latter for
23# translations. These classes have a lot of common functionality, as expressed
24# by the common parent class BaseMessage. Perhaps the most important
25# distinction is that translated text is stored in UTF-8, whereas original text
26# is stored in whatever encoding the client uses (presumably Latin-1).
27
28# --------------------
29# The public interface
30# --------------------
31
32# Generate message id from message text and meaning string (optional),
33# both in utf-8 encoding
34#
35def GenerateMessageId(message, meaning=''):
36  fp = FP.FingerPrint(message)
37  if meaning:
38    # combine the fingerprints of message and meaning
39    fp2 = FP.FingerPrint(meaning)
40    if fp < 0:
41      fp = fp2 + (fp << 1) + 1
42    else:
43      fp = fp2 + (fp << 1)
44  # To avoid negative ids we strip the high-order bit
45  return str(fp & 0x7fffffffffffffffL)
46
47# -------------------------------------------------------------------------
48# The MessageTranslationError class is used to signal tclib-specific errors.
49
50class MessageTranslationError(exceptions.Exception):
51  def __init__(self, args = ''):
52    self.args = args
53
54
55# -----------------------------------------------------------
56# The Placeholder class represents a placeholder in a message.
57
58class Placeholder(object):
59  # String representation
60  def __str__(self):
61    return '%s, "%s", "%s"' % \
62           (self.__presentation, self.__original, self.__example)
63
64  # Getters
65  def GetOriginal(self):
66    return self.__original
67
68  def GetPresentation(self):
69    return self.__presentation
70
71  def GetExample(self):
72    return self.__example
73
74  def __eq__(self, other):
75    return self.EqualTo(other, strict=1, ignore_trailing_spaces=0)
76
77  # Equality test
78  #
79  # ignore_trailing_spaces: TC is using varchar to store the
80  # phrwr fields, as a result of that, the trailing spaces
81  # are removed by MySQL when the strings are stored into TC:-(
82  # ignore_trailing_spaces parameter is used to ignore
83  # trailing spaces during equivalence comparison.
84  #
85  def EqualTo(self, other, strict = 1, ignore_trailing_spaces = 1):
86    if type(other) is not Placeholder:
87      return 0
88    if StringEquals(self.__presentation, other.__presentation,
89                    ignore_trailing_spaces):
90      if not strict or (StringEquals(self.__original, other.__original,
91                                     ignore_trailing_spaces)  and
92                        StringEquals(self.__example, other.__example,
93                                     ignore_trailing_spaces)):
94        return 1
95    return 0
96
97
98# -----------------------------------------------------------------
99# BaseMessage is the common parent class of Message and Translation.
100# It is not meant for direct use.
101
102class BaseMessage(object):
103  # Three types of message construction is supported. If the message text is a
104  # simple string with no dynamic content, you can pass it to the constructor
105  # as the "text" parameter. Otherwise, you can omit "text" and assemble the
106  # message step by step using AppendText() and AppendPlaceholder(). Or, as an
107  # alternative, you can give the constructor the "presentable" version of the
108  # message and a list of placeholders; it will then parse the presentation and
109  # build the message accordingly. For example:
110  # Message(text = "There are NUM_BUGS bugs in your code",
111  #         placeholders = [Placeholder("NUM_BUGS", "%d", "33")],
112  #         description = "Bla bla bla")
113  def __eq__(self, other):
114    # "source encoding" is nonsense, so ignore it
115    return _ObjectEquals(self, other, ['_BaseMessage__source_encoding'])
116
117  def GetName(self):
118    return self.__name
119
120  def GetSourceEncoding(self):
121    return self.__source_encoding
122
123  # Append a placeholder to the message
124  def AppendPlaceholder(self, placeholder):
125    if not isinstance(placeholder, Placeholder):
126      raise MessageTranslationError, ("Invalid message placeholder %s in "
127                                      "message %s" % (placeholder, self.GetId()))
128    # Are there other placeholders with the same presentation?
129    # If so, they need to be the same.
130    for other in self.GetPlaceholders():
131      if placeholder.GetPresentation() == other.GetPresentation():
132        if not placeholder.EqualTo(other):
133          raise MessageTranslationError, \
134                "Conflicting declarations of %s within message" % \
135                placeholder.GetPresentation()
136    # update placeholder list
137    dup = 0
138    for item in self.__content:
139      if isinstance(item, Placeholder) and placeholder.EqualTo(item):
140        dup = 1
141        break
142    if not dup:
143      self.__placeholders.append(placeholder)
144
145    # update content
146    self.__content.append(placeholder)
147
148  # Strips leading and trailing whitespace, and returns a tuple
149  # containing the leading and trailing space that was removed.
150  def Strip(self):
151    leading = trailing = ''
152    if len(self.__content) > 0:
153      s0 = self.__content[0]
154      if not isinstance(s0, Placeholder):
155        s = s0.lstrip()
156        leading = s0[:-len(s)]
157        self.__content[0] = s
158
159      s0 = self.__content[-1]
160      if not isinstance(s0, Placeholder):
161        s = s0.rstrip()
162        trailing = s0[len(s):]
163        self.__content[-1] = s
164    return leading, trailing
165
166  # Return the id of this message
167  def GetId(self):
168    if self.__id is None:
169      return self.GenerateId()
170    return self.__id
171
172  # Set the id of this message
173  def SetId(self, id):
174    if id is None:
175      self.__id = None
176    else:
177      self.__id = str(id)  # Treat numerical ids as strings
178
179  # Return content of this message as a list (internal use only)
180  def GetContent(self):
181    return self.__content
182
183  # Return a human-readable version of this message
184  def GetPresentableContent(self):
185    presentable_content = ""
186    for item in self.__content:
187      if isinstance(item, Placeholder):
188        presentable_content += item.GetPresentation()
189      else:
190        presentable_content += item
191
192    return presentable_content
193
194  # Return a fragment of a message in escaped format
195  def EscapeFragment(self, fragment):
196    return fragment.replace('%', '%%')
197
198  # Return the "original" version of this message, doing %-escaping
199  # properly.  If source_msg is specified, the placeholder original
200  # information inside source_msg will be used instead.
201  def GetOriginalContent(self, source_msg = None):
202    original_content = ""
203    for item in self.__content:
204      if isinstance(item, Placeholder):
205        if source_msg:
206          ph = source_msg.GetPlaceholder(item.GetPresentation())
207          if not ph:
208            raise MessageTranslationError, \
209                  "Placeholder %s doesn't exist in message: %s" % \
210                  (item.GetPresentation(), source_msg);
211          original_content += ph.GetOriginal()
212        else:
213          original_content += item.GetOriginal()
214      else:
215        original_content += self.EscapeFragment(item)
216    return original_content
217
218  # Return the example of this message
219  def GetExampleContent(self):
220    example_content = ""
221    for item in self.__content:
222      if isinstance(item, Placeholder):
223        example_content += item.GetExample()
224      else:
225        example_content += item
226    return example_content
227
228  # Return a list of all unique placeholders in this message
229  def GetPlaceholders(self):
230    return self.__placeholders
231
232  # Return a placeholder in this message
233  def GetPlaceholder(self, presentation):
234    for item in self.__content:
235      if (isinstance(item, Placeholder) and
236          item.GetPresentation() == presentation):
237        return item
238    return None
239
240  # Return this message's description
241  def GetDescription(self):
242    return self.__description
243
244  # Add a message source
245  def AddSource(self, source):
246    self.__sources.append(source)
247
248  # Return this message's sources as a list
249  def GetSources(self):
250    return self.__sources
251
252  # Return this message's sources as a string
253  def GetSourcesAsText(self, delimiter = "; "):
254    return delimiter.join(self.__sources)
255
256  # Set the obsolete flag for a message (internal use only)
257  def SetObsolete(self):
258    self.__obsolete = 1
259
260  # Get the obsolete flag for a message (internal use only)
261  def IsObsolete(self):
262    return self.__obsolete
263
264  # Get the sequence number (0 by default)
265  def GetSequenceNumber(self):
266    return self.__sequence_number
267
268  # Set the sequence number
269  def SetSequenceNumber(self, number):
270    self.__sequence_number = number
271
272  # Increment instance counter
273  def AddInstance(self):
274    self.__num_instances += 1
275
276  # Return instance count
277  def GetNumInstances(self):
278    return self.__num_instances
279
280  def GetErrors(self, from_tc=0):
281    """
282    Returns a description of the problem if the message is not
283    syntactically valid, or None if everything is fine.
284
285    Args:
286      from_tc: indicates whether this message came from the TC. We let
287      the TC get away with some things we normally wouldn't allow for
288      historical reasons.
289    """
290    # check that placeholders are unambiguous
291    pos = 0
292    phs = {}
293    for item in self.__content:
294      if isinstance(item, Placeholder):
295        phs[pos] = item
296        pos += len(item.GetPresentation())
297      else:
298        pos += len(item)
299    presentation = self.GetPresentableContent()
300    for ph in self.GetPlaceholders():
301      for pos in FindOverlapping(presentation, ph.GetPresentation()):
302        # message contains the same text as a placeholder presentation
303        other_ph = phs.get(pos)
304        if ((not other_ph
305             and not IsSubstringInPlaceholder(pos, len(ph.GetPresentation()), phs))
306            or
307            (other_ph and len(other_ph.GetPresentation()) < len(ph.GetPresentation()))):
308          return  "message contains placeholder name '%s':\n%s" % (
309            ph.GetPresentation(), presentation)
310    return None
311
312
313  def __CopyTo(self, other):
314    """
315    Returns a copy of this BaseMessage.
316    """
317    assert isinstance(other,  self.__class__) or isinstance(self, other.__class__)
318    other.__source_encoding = self.__source_encoding
319    other.__content         = self.__content[:]
320    other.__description     = self.__description
321    other.__id              = self.__id
322    other.__num_instances   = self.__num_instances
323    other.__obsolete        = self.__obsolete
324    other.__name            = self.__name
325    other.__placeholders    = self.__placeholders[:]
326    other.__sequence_number = self.__sequence_number
327    other.__sources         = self.__sources[:]
328
329    return other
330
331  def HasText(self):
332    """Returns true iff this message has anything other than placeholders."""
333    for item in self.__content:
334      if not isinstance(item, Placeholder):
335        return True
336    return False
337
338# --------------------------------------------------------
339# The Message class represents original (English) messages
340
341class Message(BaseMessage):
342  # See BaseMessage constructor
343  def __init__(self, source_encoding, text=None, id=None,
344               description=None, meaning="", placeholders=None,
345               source=None, sequence_number=0, clone_from=None,
346               time_created=0, name=None, is_hidden = 0):
347
348    if clone_from is not None:
349      BaseMessage.__init__(self, None, clone_from=clone_from)
350      self.__meaning = clone_from.__meaning
351      self.__time_created = clone_from.__time_created
352      self.__is_hidden = clone_from.__is_hidden
353      return
354
355    BaseMessage.__init__(self, source_encoding, text, id, description,
356                         placeholders, source, sequence_number,
357                         name=name)
358    self.__meaning = meaning
359    self.__time_created = time_created
360    self.SetIsHidden(is_hidden)
361
362  # String representation
363  def __str__(self):
364    s = 'source: %s, id: %s, content: "%s", meaning: "%s", ' \
365        'description: "%s"' % \
366        (self.GetSourcesAsText(), self.GetId(), self.GetPresentableContent(),
367         self.__meaning, self.GetDescription())
368    if self.GetName() is not None:
369      s += ', name: "%s"' % self.GetName()
370    placeholders = self.GetPlaceholders()
371    for i in range(len(placeholders)):
372      s += ", placeholder[%d]: %s" % (i, placeholders[i])
373    return s
374
375  # Strips leading and trailing whitespace, and returns a tuple
376  # containing the leading and trailing space that was removed.
377  def Strip(self):
378    leading = trailing = ''
379    content = self.GetContent()
380    if len(content) > 0:
381      s0 = content[0]
382      if not isinstance(s0, Placeholder):
383        s = s0.lstrip()
384        leading = s0[:-len(s)]
385        content[0] = s
386
387      s0 = content[-1]
388      if not isinstance(s0, Placeholder):
389        s = s0.rstrip()
390        trailing = s0[len(s):]
391        content[-1] = s
392    return leading, trailing
393
394  # Generate an id by hashing message content
395  def GenerateId(self):
396    self.SetId(GenerateMessageId(self.GetPresentableContent(),
397                                 self.__meaning))
398    return self.GetId()
399
400  def GetMeaning(self):
401    return self.__meaning
402
403  def GetTimeCreated(self):
404    return self.__time_created
405
406  # Equality operator
407  def EqualTo(self, other, strict = 1):
408    # Check id, meaning, content
409    if self.GetId() != other.GetId():
410      return 0
411    if self.__meaning != other.__meaning:
412      return 0
413    if self.GetPresentableContent() != other.GetPresentableContent():
414      return 0
415    # Check descriptions if comparison is strict
416    if (strict and
417        self.GetDescription() is not None and
418        other.GetDescription() is not None and
419        self.GetDescription() != other.GetDescription()):
420      return 0
421    # Check placeholders
422    ph1 = self.GetPlaceholders()
423    ph2 = other.GetPlaceholders()
424    if len(ph1) != len(ph2):
425      return 0
426    for i in range(len(ph1)):
427      if not ph1[i].EqualTo(ph2[i], strict):
428        return 0
429
430    return 1
431
432  def Copy(self):
433    """
434    Returns a copy of this Message.
435    """
436    assert isinstance(self, Message)
437    return Message(None, clone_from=self)
438
439  def SetIsHidden(self, is_hidden):
440    """Sets whether this message should be hidden.
441
442    Args:
443      is_hidden : 0 or 1 - if the message should be hidden, 0 otherwise
444    """
445    if is_hidden not in [0, 1]:
446      raise  MessageTranslationError, "is_hidden must be 0 or 1, got %s"
447    self.__is_hidden = is_hidden
448
449  def IsHidden(self):
450    """Returns 1 if this message is hidden, and 0 otherwise."""
451    return self.__is_hidden
452
453# ----------------------------------------------------
454# The Translation class represents translated messages
455
456class Translation(BaseMessage):
457  # See BaseMessage constructor
458  def __init__(self, source_encoding, text=None, id=None,
459               description=None, placeholders=None, source=None,
460               sequence_number=0, clone_from=None, ignore_ph_errors=0,
461               name=None):
462    if clone_from is not None:
463      BaseMessage.__init__(self, None, clone_from=clone_from)
464      return
465
466    BaseMessage.__init__(self, source_encoding, text, id, description,
467                         placeholders, source, sequence_number,
468                         ignore_ph_errors=ignore_ph_errors, name=name)
469
470  # String representation
471  def __str__(self):
472    s = 'source: %s, id: %s, content: "%s", description: "%s"' % \
473        (self.GetSourcesAsText(), self.GetId(), self.GetPresentableContent(),
474         self.GetDescription());
475    placeholders = self.GetPlaceholders()
476    for i in range(len(placeholders)):
477      s += ", placeholder[%d]: %s" % (i, placeholders[i])
478    return s
479
480  # Equality operator
481  def EqualTo(self, other, strict=1):
482    # Check id and content
483    if self.GetId() != other.GetId():
484      return 0
485    if self.GetPresentableContent() != other.GetPresentableContent():
486      return 0
487    # Check placeholders
488    ph1 = self.GetPlaceholders()
489    ph2 = other.GetPlaceholders()
490    if len(ph1) != len(ph2):
491      return 0
492    for i in range(len(ph1)):
493      if not ph1[i].EqualTo(ph2[i], strict):
494        return 0
495
496    return 1
497
498  def Copy(self):
499    """
500    Returns a copy of this Translation.
501    """
502    return Translation(None, clone_from=self)
503
504