comparison babel/messages/catalog.py @ 202:d3c272492053

Added `--no-fuzzy-matching` to the frontends and also `--previous` which adds the old msgid's as comments. The latest closes #31.
author palgarvio
date Tue, 03 Jul 2007 17:55:34 +0000
parents 2f0161df6a38
children 3476d17c9909
comparison
equal deleted inserted replaced
201:10e8d072e2d1 202:d3c272492053
38 38
39 class Message(object): 39 class Message(object):
40 """Representation of a single message in a catalog.""" 40 """Representation of a single message in a catalog."""
41 41
42 def __init__(self, id, string=u'', locations=(), flags=(), auto_comments=(), 42 def __init__(self, id, string=u'', locations=(), flags=(), auto_comments=(),
43 user_comments=()): 43 user_comments=(), old_msgid=()):
44 """Create the message object. 44 """Create the message object.
45 45
46 :param id: the message ID, or a ``(singular, plural)`` tuple for 46 :param id: the message ID, or a ``(singular, plural)`` tuple for
47 pluralizable messages 47 pluralizable messages
48 :param string: the translated message string, or a 48 :param string: the translated message string, or a
49 ``(singular, plural)`` tuple for pluralizable messages 49 ``(singular, plural)`` tuple for pluralizable messages
50 :param locations: a sequence of ``(filenname, lineno)`` tuples 50 :param locations: a sequence of ``(filenname, lineno)`` tuples
51 :param flags: a set or sequence of flags 51 :param flags: a set or sequence of flags
52 :param auto_comments: a sequence of automatic comments for the message 52 :param auto_comments: a sequence of automatic comments for the message
53 :param user_comments: a sequence of user comments for the message 53 :param user_comments: a sequence of user comments for the message
54 :param old_message: the old message ID, or a ``(singular, plural)``
55 tuple for old pluralizable messages
54 """ 56 """
55 self.id = id #: The message ID 57 self.id = id #: The message ID
56 if not string and self.pluralizable: 58 if not string and self.pluralizable:
57 string = (u'', u'') 59 string = (u'', u'')
58 self.string = string #: The message translation 60 self.string = string #: The message translation
62 self.flags.add('python-format') 64 self.flags.add('python-format')
63 else: 65 else:
64 self.flags.discard('python-format') 66 self.flags.discard('python-format')
65 self.auto_comments = list(auto_comments) 67 self.auto_comments = list(auto_comments)
66 self.user_comments = list(user_comments) 68 self.user_comments = list(user_comments)
69 if isinstance(old_msgid, basestring):
70 self.old_msgid = [old_msgid]
71 else:
72 self.old_msgid = list(old_msgid)
67 73
68 def __repr__(self): 74 def __repr__(self):
69 return '<%s %r (flags: %r)>' % (type(self).__name__, self.id, 75 return '<%s %r (flags: %r)>' % (type(self).__name__, self.id,
70 list(self.flags)) 76 list(self.flags))
71 77
72 def fuzzy(self): 78 def fuzzy(self):
73 return 'fuzzy' in self.flags 79 return 'fuzzy' in self.flags
74 fuzzy = property(fuzzy, doc="""\ 80 fuzzy = property(fuzzy, doc="""\
75 Whether the translation is fuzzy. 81 Whether the translation is fuzzy.
76 82
77 >>> Message('foo').fuzzy 83 >>> Message('foo').fuzzy
78 False 84 False
79 >>> msg = Message('foo', 'foo', flags=['fuzzy']) 85 >>> msg = Message('foo', 'foo', flags=['fuzzy'])
80 >>> msg.fuzzy 86 >>> msg.fuzzy
81 True 87 True
82 >>> msg 88 >>> msg
83 <Message 'foo' (flags: ['fuzzy'])> 89 <Message 'foo' (flags: ['fuzzy'])>
84 90
85 :type: `bool` 91 :type: `bool`
86 """) 92 """)
87 93
88 def pluralizable(self): 94 def pluralizable(self):
89 return isinstance(self.id, (list, tuple)) 95 return isinstance(self.id, (list, tuple))
90 pluralizable = property(pluralizable, doc="""\ 96 pluralizable = property(pluralizable, doc="""\
91 Whether the message is plurizable. 97 Whether the message is plurizable.
92 98
93 >>> Message('foo').pluralizable 99 >>> Message('foo').pluralizable
94 False 100 False
95 >>> Message(('foo', 'bar')).pluralizable 101 >>> Message(('foo', 'bar')).pluralizable
96 True 102 True
97 103
98 :type: `bool` 104 :type: `bool`
99 """) 105 """)
100 106
101 def python_format(self): 107 def python_format(self):
102 ids = self.id 108 ids = self.id
103 if not isinstance(ids, (list, tuple)): 109 if not isinstance(ids, (list, tuple)):
104 ids = [ids] 110 ids = [ids]
105 return bool(filter(None, [PYTHON_FORMAT(id) for id in ids])) 111 return bool(filter(None, [PYTHON_FORMAT(id) for id in ids]))
106 python_format = property(python_format, doc="""\ 112 python_format = property(python_format, doc="""\
107 Whether the message contains Python-style parameters. 113 Whether the message contains Python-style parameters.
108 114
109 >>> Message('foo %(name)s bar').python_format 115 >>> Message('foo %(name)s bar').python_format
110 True 116 True
111 >>> Message(('foo %(name)s', 'foo %(name)s')).python_format 117 >>> Message(('foo %(name)s', 'foo %(name)s')).python_format
112 True 118 True
113 119
114 :type: `bool` 120 :type: `bool`
115 """) 121 """)
116 122
117 123
118 DEFAULT_HEADER = u"""\ 124 DEFAULT_HEADER = u"""\
130 project=None, version=None, copyright_holder=None, 136 project=None, version=None, copyright_holder=None,
131 msgid_bugs_address=None, creation_date=None, 137 msgid_bugs_address=None, creation_date=None,
132 revision_date=None, last_translator=None, charset='utf-8', 138 revision_date=None, last_translator=None, charset='utf-8',
133 fuzzy=True): 139 fuzzy=True):
134 """Initialize the catalog object. 140 """Initialize the catalog object.
135 141
136 :param locale: the locale identifier or `Locale` object, or `None` 142 :param locale: the locale identifier or `Locale` object, or `None`
137 if the catalog is not bound to a locale (which basically 143 if the catalog is not bound to a locale (which basically
138 means it's a template) 144 means it's a template)
139 :param domain: the message domain 145 :param domain: the message domain
140 :param header_comment: the header comment as string, or `None` for the 146 :param header_comment: the header comment as string, or `None` for the
195 def _set_header_comment(self, string): 201 def _set_header_comment(self, string):
196 self._header_comment = string 202 self._header_comment = string
197 203
198 header_comment = property(_get_header_comment, _set_header_comment, doc="""\ 204 header_comment = property(_get_header_comment, _set_header_comment, doc="""\
199 The header comment for the catalog. 205 The header comment for the catalog.
200 206
201 >>> catalog = Catalog(project='Foobar', version='1.0', 207 >>> catalog = Catalog(project='Foobar', version='1.0',
202 ... copyright_holder='Foo Company') 208 ... copyright_holder='Foo Company')
203 >>> print catalog.header_comment 209 >>> print catalog.header_comment
204 # Translations template for Foobar. 210 # Translations template for Foobar.
205 # Copyright (C) 2007 Foo Company 211 # Copyright (C) 2007 Foo Company
206 # This file is distributed under the same license as the Foobar project. 212 # This file is distributed under the same license as the Foobar project.
207 # FIRST AUTHOR <EMAIL@ADDRESS>, 2007. 213 # FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
208 # 214 #
209 215
210 The header can also be set from a string. Any known upper-case variables 216 The header can also be set from a string. Any known upper-case variables
211 will be replaced when the header is retrieved again: 217 will be replaced when the header is retrieved again:
212 218
213 >>> catalog = Catalog(project='Foobar', version='1.0', 219 >>> catalog = Catalog(project='Foobar', version='1.0',
214 ... copyright_holder='Foo Company') 220 ... copyright_holder='Foo Company')
215 >>> catalog.header_comment = '''\\ 221 >>> catalog.header_comment = '''\\
216 ... # The POT for my really cool PROJECT project. 222 ... # The POT for my really cool PROJECT project.
217 ... # Copyright (C) 1990-2003 ORGANIZATION 223 ... # Copyright (C) 1990-2003 ORGANIZATION
280 if 'charset' in params: 286 if 'charset' in params:
281 self.charset = params['charset'].lower() 287 self.charset = params['charset'].lower()
282 288
283 mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\ 289 mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\
284 The MIME headers of the catalog, used for the special ``msgid ""`` entry. 290 The MIME headers of the catalog, used for the special ``msgid ""`` entry.
285 291
286 The behavior of this property changes slightly depending on whether a locale 292 The behavior of this property changes slightly depending on whether a locale
287 is set or not, the latter indicating that the catalog is actually a template 293 is set or not, the latter indicating that the catalog is actually a template
288 for actual translations. 294 for actual translations.
289 295
290 Here's an example of the output for such a catalog template: 296 Here's an example of the output for such a catalog template:
291 297
292 >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC) 298 >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC)
293 >>> catalog = Catalog(project='Foobar', version='1.0', 299 >>> catalog = Catalog(project='Foobar', version='1.0',
294 ... creation_date=created) 300 ... creation_date=created)
295 >>> for name, value in catalog.mime_headers: 301 >>> for name, value in catalog.mime_headers:
296 ... print '%s: %s' % (name, value) 302 ... print '%s: %s' % (name, value)
302 Language-Team: LANGUAGE <LL@li.org> 308 Language-Team: LANGUAGE <LL@li.org>
303 MIME-Version: 1.0 309 MIME-Version: 1.0
304 Content-Type: text/plain; charset=utf-8 310 Content-Type: text/plain; charset=utf-8
305 Content-Transfer-Encoding: 8bit 311 Content-Transfer-Encoding: 8bit
306 Generated-By: Babel ... 312 Generated-By: Babel ...
307 313
308 And here's an example of the output when the locale is set: 314 And here's an example of the output when the locale is set:
309 315
310 >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC) 316 >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC)
311 >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0', 317 >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0',
312 ... creation_date=created, revision_date=revised, 318 ... creation_date=created, revision_date=revised,
313 ... last_translator='John Doe <jd@example.com>') 319 ... last_translator='John Doe <jd@example.com>')
314 >>> for name, value in catalog.mime_headers: 320 >>> for name, value in catalog.mime_headers:
322 Plural-Forms: nplurals=2; plural=(n != 1) 328 Plural-Forms: nplurals=2; plural=(n != 1)
323 MIME-Version: 1.0 329 MIME-Version: 1.0
324 Content-Type: text/plain; charset=utf-8 330 Content-Type: text/plain; charset=utf-8
325 Content-Transfer-Encoding: 8bit 331 Content-Transfer-Encoding: 8bit
326 Generated-By: Babel ... 332 Generated-By: Babel ...
327 333
328 :type: `list` 334 :type: `list`
329 """) 335 """)
330 336
331 def num_plurals(self): 337 def num_plurals(self):
332 num = 2 338 num = 2
336 elif self.locale.language in PLURALS: 342 elif self.locale.language in PLURALS:
337 num = PLURALS[self.locale.language][0] 343 num = PLURALS[self.locale.language][0]
338 return num 344 return num
339 num_plurals = property(num_plurals, doc="""\ 345 num_plurals = property(num_plurals, doc="""\
340 The number of plurals used by the locale. 346 The number of plurals used by the locale.
341 347
342 >>> Catalog(locale='en').num_plurals 348 >>> Catalog(locale='en').num_plurals
343 2 349 2
344 >>> Catalog(locale='cs_CZ').num_plurals 350 >>> Catalog(locale='cs_CZ').num_plurals
345 3 351 3
346 352
347 :type: `int` 353 :type: `int`
348 """) 354 """)
349 355
350 def plural_forms(self): 356 def plural_forms(self):
351 num, expr = ('INTEGER', 'EXPRESSION') 357 num, expr = ('INTEGER', 'EXPRESSION')
355 elif self.locale.language in PLURALS: 361 elif self.locale.language in PLURALS:
356 num, expr = PLURALS[self.locale.language] 362 num, expr = PLURALS[self.locale.language]
357 return 'nplurals=%s; plural=%s' % (num, expr) 363 return 'nplurals=%s; plural=%s' % (num, expr)
358 plural_forms = property(plural_forms, doc="""\ 364 plural_forms = property(plural_forms, doc="""\
359 Return the plural forms declaration for the locale. 365 Return the plural forms declaration for the locale.
360 366
361 >>> Catalog(locale='en').plural_forms 367 >>> Catalog(locale='en').plural_forms
362 'nplurals=2; plural=(n != 1)' 368 'nplurals=2; plural=(n != 1)'
363 >>> Catalog(locale='pt_BR').plural_forms 369 >>> Catalog(locale='pt_BR').plural_forms
364 'nplurals=2; plural=(n > 1)' 370 'nplurals=2; plural=(n > 1)'
365 371
366 :type: `str` 372 :type: `str`
367 """) 373 """)
368 374
369 def __contains__(self, id): 375 def __contains__(self, id):
370 """Return whether the catalog has a message with the specified ID.""" 376 """Return whether the catalog has a message with the specified ID."""
371 return self._key_for(id) in self._messages 377 return self._key_for(id) in self._messages
372 378
373 def __len__(self): 379 def __len__(self):
374 """The number of messages in the catalog. 380 """The number of messages in the catalog.
375 381
376 This does not include the special ``msgid ""`` entry. 382 This does not include the special ``msgid ""`` entry.
377 """ 383 """
378 return len(self._messages) 384 return len(self._messages)
379 385
380 def __iter__(self): 386 def __iter__(self):
381 """Iterates through all the entries in the catalog, in the order they 387 """Iterates through all the entries in the catalog, in the order they
382 were added, yielding a `Message` object for every entry. 388 were added, yielding a `Message` object for every entry.
383 389
384 :rtype: ``iterator`` 390 :rtype: ``iterator``
385 """ 391 """
386 buf = [] 392 buf = []
387 for name, value in self.mime_headers: 393 for name, value in self.mime_headers:
388 buf.append('%s: %s' % (name, value)) 394 buf.append('%s: %s' % (name, value))
405 if key in self._messages: 411 if key in self._messages:
406 del self._messages[key] 412 del self._messages[key]
407 413
408 def __getitem__(self, id): 414 def __getitem__(self, id):
409 """Return the message with the specified ID. 415 """Return the message with the specified ID.
410 416
411 :param id: the message ID 417 :param id: the message ID
412 :return: the message with the specified ID, or `None` if no such message 418 :return: the message with the specified ID, or `None` if no such message
413 is in the catalog 419 is in the catalog
414 :rtype: `Message` 420 :rtype: `Message`
415 """ 421 """
416 return self._messages.get(self._key_for(id)) 422 return self._messages.get(self._key_for(id))
417 423
418 def __setitem__(self, id, message): 424 def __setitem__(self, id, message):
419 """Add or update the message with the specified ID. 425 """Add or update the message with the specified ID.
420 426
421 >>> catalog = Catalog() 427 >>> catalog = Catalog()
422 >>> catalog[u'foo'] = Message(u'foo') 428 >>> catalog[u'foo'] = Message(u'foo')
423 >>> catalog[u'foo'] 429 >>> catalog[u'foo']
424 <Message u'foo' (flags: [])> 430 <Message u'foo' (flags: [])>
425 431
426 If a message with that ID is already in the catalog, it is updated 432 If a message with that ID is already in the catalog, it is updated
427 to include the locations and flags of the new message. 433 to include the locations and flags of the new message.
428 434
429 >>> catalog = Catalog() 435 >>> catalog = Catalog()
430 >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)]) 436 >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)])
431 >>> catalog[u'foo'].locations 437 >>> catalog[u'foo'].locations
432 [('main.py', 1)] 438 [('main.py', 1)]
433 >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)]) 439 >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)])
434 >>> catalog[u'foo'].locations 440 >>> catalog[u'foo'].locations
435 [('main.py', 1), ('utils.py', 5)] 441 [('main.py', 1), ('utils.py', 5)]
436 442
437 :param id: the message ID 443 :param id: the message ID
438 :param message: the `Message` object 444 :param message: the `Message` object
439 """ 445 """
440 assert isinstance(message, Message), 'expected a Message object' 446 assert isinstance(message, Message), 'expected a Message object'
441 key = self._key_for(id) 447 key = self._key_for(id)
461 if isinstance(id, (list, tuple)): 467 if isinstance(id, (list, tuple)):
462 assert isinstance(message.string, (list, tuple)) 468 assert isinstance(message.string, (list, tuple))
463 self._messages[key] = message 469 self._messages[key] = message
464 470
465 def add(self, id, string=None, locations=(), flags=(), auto_comments=(), 471 def add(self, id, string=None, locations=(), flags=(), auto_comments=(),
466 user_comments=()): 472 user_comments=(), old_message=()):
467 """Add or update the message with the specified ID. 473 """Add or update the message with the specified ID.
468 474
469 >>> catalog = Catalog() 475 >>> catalog = Catalog()
470 >>> catalog.add(u'foo') 476 >>> catalog.add(u'foo')
471 >>> catalog[u'foo'] 477 >>> catalog[u'foo']
472 <Message u'foo' (flags: [])> 478 <Message u'foo' (flags: [])>
473 479
474 This method simply constructs a `Message` object with the given 480 This method simply constructs a `Message` object with the given
475 arguments and invokes `__setitem__` with that object. 481 arguments and invokes `__setitem__` with that object.
476 482
477 :param id: the message ID, or a ``(singular, plural)`` tuple for 483 :param id: the message ID, or a ``(singular, plural)`` tuple for
478 pluralizable messages 484 pluralizable messages
479 :param string: the translated message string, or a 485 :param string: the translated message string, or a
480 ``(singular, plural)`` tuple for pluralizable messages 486 ``(singular, plural)`` tuple for pluralizable messages
481 :param locations: a sequence of ``(filenname, lineno)`` tuples 487 :param locations: a sequence of ``(filenname, lineno)`` tuples
482 :param flags: a set or sequence of flags 488 :param flags: a set or sequence of flags
483 :param auto_comments: a sequence of automatic comments 489 :param auto_comments: a sequence of automatic comments
484 :param user_comments: a sequence of user comments 490 :param user_comments: a sequence of user comments
485 """ 491 """
486 self[id] = Message(id, string, list(locations), flags, auto_comments, 492 self[id] = Message(id, string, list(locations), flags, auto_comments,
487 user_comments) 493 user_comments, old_message)
488 494
489 def update(self, template, fuzzy_matching=True): 495 def update(self, template, no_fuzzy_matching=False,
496 include_old_msgid=False):
490 """Update the catalog based on the given template catalog. 497 """Update the catalog based on the given template catalog.
491 498
492 >>> from babel.messages import Catalog 499 >>> from babel.messages import Catalog
493 >>> template = Catalog() 500 >>> template = Catalog()
494 >>> template.add('green', locations=[('main.py', 99)]) 501 >>> template.add('green', locations=[('main.py', 99)])
495 >>> template.add('blue', locations=[('main.py', 100)]) 502 >>> template.add('blue', locations=[('main.py', 100)])
496 >>> template.add(('salad', 'salads'), locations=[('util.py', 42)]) 503 >>> template.add(('salad', 'salads'), locations=[('util.py', 42)])
497 >>> catalog = Catalog(locale='de_DE') 504 >>> catalog = Catalog(locale='de_DE')
498 >>> catalog.add('blue', u'blau', locations=[('main.py', 98)]) 505 >>> catalog.add('blue', u'blau', locations=[('main.py', 98)])
499 >>> catalog.add('head', u'Kopf', locations=[('util.py', 33)]) 506 >>> catalog.add('head', u'Kopf', locations=[('util.py', 33)])
500 >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'), 507 >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'),
501 ... locations=[('util.py', 38)]) 508 ... locations=[('util.py', 38)])
502 509
503 >>> catalog.update(template) 510 >>> catalog.update(template)
504 >>> len(catalog) 511 >>> len(catalog)
505 3 512 3
506 513
507 >>> msg1 = catalog['green'] 514 >>> msg1 = catalog['green']
508 >>> msg1.string 515 >>> msg1.string
509 >>> msg1.locations 516 >>> msg1.locations
510 [('main.py', 99)] 517 [('main.py', 99)]
511 518
512 >>> msg2 = catalog['blue'] 519 >>> msg2 = catalog['blue']
513 >>> msg2.string 520 >>> msg2.string
514 u'blau' 521 u'blau'
515 >>> msg2.locations 522 >>> msg2.locations
516 [('main.py', 100)] 523 [('main.py', 100)]
517 524
518 >>> msg3 = catalog['salad'] 525 >>> msg3 = catalog['salad']
519 >>> msg3.string 526 >>> msg3.string
520 (u'Salat', u'Salate') 527 (u'Salat', u'Salate')
521 >>> msg3.locations 528 >>> msg3.locations
522 [('util.py', 42)] 529 [('util.py', 42)]
523 530
524 Messages that are in the catalog but not in the template are removed 531 Messages that are in the catalog but not in the template are removed
525 from the main collection, but can still be accessed via the `obsolete` 532 from the main collection, but can still be accessed via the `obsolete`
526 member: 533 member:
527 534
528 >>> 'head' in catalog 535 >>> 'head' in catalog
529 False 536 False
530 >>> catalog.obsolete.values() 537 >>> catalog.obsolete.values()
531 [<Message 'head' (flags: [])>] 538 [<Message 'head' (flags: [])>]
532 539
533 :param template: the reference catalog, usually read from a POT file 540 :param template: the reference catalog, usually read from a POT file
534 :param fuzzy_matching: whether to use fuzzy matching of message IDs 541 :param no_fuzzy_matching: whether to use fuzzy matching of message IDs
542 :param include_old_msgid: include the old msgid as a comment when
543 updating the catalog
535 """ 544 """
536 messages = self._messages 545 messages = self._messages
537 self._messages = odict() 546 self._messages = odict()
538 547
539 for message in template: 548 for message in template:
544 message.string = oldmsg.string 553 message.string = oldmsg.string
545 message.flags |= oldmsg.flags 554 message.flags |= oldmsg.flags
546 self[message.id] = message 555 self[message.id] = message
547 556
548 else: 557 else:
549 if fuzzy_matching: 558 if no_fuzzy_matching is False:
550 # do some fuzzy matching with difflib 559 # do some fuzzy matching with difflib
551 matches = get_close_matches(key.lower().strip(), 560 matches = get_close_matches(key.lower().strip(),
552 [self._key_for(msgid) for msgid in messages], 1) 561 [self._key_for(msgid) for msgid in messages], 1)
553 if matches: 562 if matches:
554 oldmsg = messages.pop(matches[0]) 563 oldmsg = messages.pop(matches[0])
555 message.string = oldmsg.string 564 message.string = oldmsg.string
556 message.flags |= oldmsg.flags | set([u'fuzzy']) 565 message.flags |= oldmsg.flags | set([u'fuzzy'])
566 if include_old_msgid:
567 if isinstance(oldmsg.id, basestring):
568 message.old_msgid = [oldmsg.id]
569 else:
570 message.old_msgid = list(oldmsg.id)
557 self[message.id] = message 571 self[message.id] = message
558 continue 572 continue
559 573
560 self[message.id] = message 574 self[message.id] = message
561 575
Copyright (C) 2012-2017 Edgewall Software