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