OLD | NEW |
| (Empty) |
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 | |
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 |