OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python2.4 |
| 2 # Copyright (c) 2006-2008 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 |
| 14 import exceptions |
| 15 |
| 16 from 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 # |
| 35 def 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 |
| 50 class 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 |
| 58 class 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 |
| 102 class 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()), ph
s)) |
| 306 or |
| 307 (other_ph and len(other_ph.GetPresentation()) < len(ph.GetPresentati
on()))): |
| 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 |
| 341 class 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 |
| 456 class 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 |
OLD | NEW |