PageRenderTime 76ms CodeModel.GetById 42ms RepoModel.GetById 0ms app.codeStats 0ms

/distribution/libraries/Babel-1.0dev-py3.2/babel/messages/catalog.py

https://github.com/tictactatic/Superdesk
Python | 847 lines | 806 code | 12 blank | 29 comment | 6 complexity | 1bb19ac1cce5b4d73c3c89e3771a3970 MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-3.0, GPL-2.0
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2007-2011 Edgewall Software
  4. # All rights reserved.
  5. #
  6. # This software is licensed as described in the file COPYING, which
  7. # you should have received as part of this distribution. The terms
  8. # are also available at http://babel.edgewall.org/wiki/License.
  9. #
  10. # This software consists of voluntary contributions made by many
  11. # individuals. For the exact contribution history, see the revision
  12. # history and logs, available at http://babel.edgewall.org/log/.
  13. """Data structures for message catalogs."""
  14. from cgi import parse_header
  15. from datetime import datetime
  16. from difflib import get_close_matches
  17. from email import message_from_string
  18. from copy import copy
  19. import re
  20. import sys
  21. import time
  22. from babel import __version__ as VERSION
  23. from babel.compat import u, string_types, PY3
  24. from babel.core import Locale
  25. from babel.dates import format_datetime
  26. from babel.messages.plurals import get_plural
  27. from babel.util import odict, distinct, LOCALTZ, UTC, FixedOffsetTimezone
  28. __all__ = ['Message', 'Catalog', 'TranslationError']
  29. __docformat__ = 'restructuredtext en'
  30. PYTHON_FORMAT = re.compile(r'''(?x)
  31. \%
  32. (?:\(([\w]*)\))?
  33. (
  34. [-#0\ +]?(?:\*|[\d]+)?
  35. (?:\.(?:\*|[\d]+))?
  36. [hlL]?
  37. )
  38. ([diouxXeEfFgGcrs%])
  39. ''')
  40. class Message(object):
  41. """Representation of a single message in a catalog."""
  42. def __init__(self, id, string=u(''), locations=(), flags=(), auto_comments=(),
  43. user_comments=(), previous_id=(), lineno=None, context=None):
  44. """Create the message object.
  45. :param id: the message ID, or a ``(singular, plural)`` tuple for
  46. pluralizable messages
  47. :param string: the translated message string, or a
  48. ``(singular, plural)`` tuple for pluralizable messages
  49. :param locations: a sequence of ``(filenname, lineno)`` tuples
  50. :param flags: a set or sequence of flags
  51. :param auto_comments: a sequence of automatic comments for the message
  52. :param user_comments: a sequence of user comments for the message
  53. :param previous_id: the previous message ID, or a ``(singular, plural)``
  54. tuple for pluralizable messages
  55. :param lineno: the line number on which the msgid line was found in the
  56. PO file, if any
  57. :param context: the message context
  58. """
  59. self.id = id #: The message ID
  60. if not string and self.pluralizable:
  61. string = (u(''), u(''))
  62. self.string = string #: The message translation
  63. self.locations = list(distinct(locations))
  64. self.flags = set(flags)
  65. if id and self.python_format:
  66. self.flags.add('python-format')
  67. else:
  68. self.flags.discard('python-format')
  69. self.auto_comments = list(distinct(auto_comments))
  70. self.user_comments = list(distinct(user_comments))
  71. if isinstance(previous_id, string_types):
  72. self.previous_id = [previous_id]
  73. else:
  74. self.previous_id = list(previous_id)
  75. self.lineno = lineno
  76. self.context = context
  77. def __repr__(self):
  78. return '<%s %s (flags: %r)>' % (type(self).__name__, self.id,
  79. list(self.flags))
  80. def __cmp__(self, obj):
  81. """Compare Messages, taking into account plural ids"""
  82. def cmp(a, b):
  83. return ((a > b) - (a < b))
  84. if isinstance(obj, Message):
  85. plural = self.pluralizable
  86. obj_plural = obj.pluralizable
  87. if plural and obj_plural:
  88. return cmp(self.id[0], obj.id[0])
  89. elif plural:
  90. return cmp(self.id[0], obj.id)
  91. elif obj_plural:
  92. return cmp(self.id, obj.id[0])
  93. return cmp(self.id, obj.id)
  94. def __gt__(self, other):
  95. return self.__cmp__(other) > 0
  96. def __lt__(self, other):
  97. return self.__cmp__(other) < 0
  98. def __ge__(self, other):
  99. return self.__cmp__(other) >= 0
  100. def __le__(self, other):
  101. return self.__cmp__(other) <= 0
  102. def __eq__(self, other):
  103. return self.__cmp__(other) == 0
  104. def __ne__(self, other):
  105. return self.__cmp__(other) != 0
  106. def clone(self):
  107. return Message(*map(copy, (self.id, self.string, self.locations,
  108. self.flags, self.auto_comments,
  109. self.user_comments, self.previous_id,
  110. self.lineno, self.context)))
  111. def check(self, catalog=None):
  112. """Run various validation checks on the message. Some validations
  113. are only performed if the catalog is provided. This method returns
  114. a sequence of `TranslationError` objects.
  115. :rtype: ``iterator``
  116. :param catalog: A catalog instance that is passed to the checkers
  117. :see: `Catalog.check` for a way to perform checks for all messages
  118. in a catalog.
  119. """
  120. from babel.messages.checkers import checkers
  121. errors = []
  122. for checker in checkers:
  123. try:
  124. checker(catalog, self)
  125. except TranslationError:
  126. errors.append(sys.exc_info()[1])
  127. return errors
  128. def fuzzy(self):
  129. return 'fuzzy' in self.flags
  130. fuzzy = property(fuzzy, doc="""\
  131. Whether the translation is fuzzy.
  132. >>> Message('foo').fuzzy
  133. False
  134. >>> msg = Message('foo', 'foo', flags=['fuzzy'])
  135. >>> msg.fuzzy
  136. True
  137. >>> msg
  138. <Message foo (flags: ['fuzzy'])>
  139. :type: `bool`
  140. """)
  141. def pluralizable(self):
  142. return isinstance(self.id, (list, tuple))
  143. pluralizable = property(pluralizable, doc="""\
  144. Whether the message is plurizable.
  145. >>> Message('foo').pluralizable
  146. False
  147. >>> Message(('foo', 'bar')).pluralizable
  148. True
  149. :type: `bool`
  150. """)
  151. def python_format(self):
  152. ids = self.id
  153. if not isinstance(ids, (list, tuple)):
  154. ids = [ids]
  155. return bool([_f for _f in [PYTHON_FORMAT.search(id) for id in ids] if _f])
  156. python_format = property(python_format, doc="""\
  157. Whether the message contains Python-style parameters.
  158. >>> Message('foo %(name)s bar').python_format
  159. True
  160. >>> Message(('foo %(name)s', 'foo %(name)s')).python_format
  161. True
  162. :type: `bool`
  163. """)
  164. class TranslationError(Exception):
  165. """Exception thrown by translation checkers when invalid message
  166. translations are encountered."""
  167. DEFAULT_HEADER = u("""\
  168. # Translations template for PROJECT.
  169. # Copyright (C) YEAR ORGANIZATION
  170. # This file is distributed under the same license as the PROJECT project.
  171. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
  172. #""")
  173. class Catalog(object):
  174. """Representation of a message catalog."""
  175. def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER,
  176. project=None, version=None, copyright_holder=None,
  177. msgid_bugs_address=None, creation_date=None,
  178. revision_date=None, last_translator=None, language_team=None,
  179. charset='utf-8', fuzzy=True):
  180. """Initialize the catalog object.
  181. :param locale: the locale identifier or `Locale` object, or `None`
  182. if the catalog is not bound to a locale (which basically
  183. means it's a template)
  184. :param domain: the message domain
  185. :param header_comment: the header comment as string, or `None` for the
  186. default header
  187. :param project: the project's name
  188. :param version: the project's version
  189. :param copyright_holder: the copyright holder of the catalog
  190. :param msgid_bugs_address: the email address or URL to submit bug
  191. reports to
  192. :param creation_date: the date the catalog was created
  193. :param revision_date: the date the catalog was revised
  194. :param last_translator: the name and email of the last translator
  195. :param language_team: the name and email of the language team
  196. :param charset: the encoding to use in the output
  197. :param fuzzy: the fuzzy bit on the catalog header
  198. """
  199. self.domain = domain #: The message domain
  200. if locale:
  201. locale = Locale.parse(locale)
  202. self.locale = locale #: The locale or `None`
  203. self._header_comment = header_comment
  204. self._messages = odict()
  205. self.project = project or 'PROJECT' #: The project name
  206. self.version = version or 'VERSION' #: The project version
  207. self.copyright_holder = copyright_holder or 'ORGANIZATION'
  208. self.msgid_bugs_address = msgid_bugs_address or 'EMAIL@ADDRESS'
  209. self.last_translator = last_translator or 'FULL NAME <EMAIL@ADDRESS>'
  210. """Name and email address of the last translator."""
  211. self.language_team = language_team or 'LANGUAGE <LL@li.org>'
  212. """Name and email address of the language team."""
  213. self.charset = charset or 'utf-8'
  214. if creation_date is None:
  215. creation_date = datetime.now(LOCALTZ)
  216. elif isinstance(creation_date, datetime) and not creation_date.tzinfo:
  217. creation_date = creation_date.replace(tzinfo=LOCALTZ)
  218. self.creation_date = creation_date #: Creation date of the template
  219. if revision_date is None:
  220. revision_date = datetime.now(LOCALTZ)
  221. elif isinstance(revision_date, datetime) and not revision_date.tzinfo:
  222. revision_date = revision_date.replace(tzinfo=LOCALTZ)
  223. self.revision_date = revision_date #: Last revision date of the catalog
  224. self.fuzzy = fuzzy #: Catalog header fuzzy bit (`True` or `False`)
  225. self.obsolete = odict() #: Dictionary of obsolete messages
  226. self._num_plurals = None
  227. self._plural_expr = None
  228. def _get_header_comment(self):
  229. comment = self._header_comment
  230. comment = comment.replace('PROJECT', self.project) \
  231. .replace('VERSION', self.version) \
  232. .replace('YEAR', self.revision_date.strftime('%Y')) \
  233. .replace('ORGANIZATION', self.copyright_holder)
  234. if self.locale:
  235. comment = comment.replace('Translations template', '%s translations'
  236. % self.locale.english_name)
  237. return comment
  238. def _set_header_comment(self, string):
  239. self._header_comment = string
  240. header_comment = property(_get_header_comment, _set_header_comment, doc="""\
  241. The header comment for the catalog.
  242. >>> catalog = Catalog(project='Foobar', version='1.0',
  243. ... copyright_holder='Foo Company')
  244. >>> print(catalog.header_comment) #doctest: +ELLIPSIS
  245. # Translations template for Foobar.
  246. # Copyright (C) ... Foo Company
  247. # This file is distributed under the same license as the Foobar project.
  248. # FIRST AUTHOR <EMAIL@ADDRESS>, ....
  249. #
  250. The header can also be set from a string. Any known upper-case variables
  251. will be replaced when the header is retrieved again:
  252. >>> catalog = Catalog(project='Foobar', version='1.0',
  253. ... copyright_holder='Foo Company')
  254. >>> catalog.header_comment = '''\\
  255. ... # The POT for my really cool PROJECT project.
  256. ... # Copyright (C) 1990-2003 ORGANIZATION
  257. ... # This file is distributed under the same license as the PROJECT
  258. ... # project.
  259. ... #'''
  260. >>> print(catalog.header_comment)
  261. # The POT for my really cool Foobar project.
  262. # Copyright (C) 1990-2003 Foo Company
  263. # This file is distributed under the same license as the Foobar
  264. # project.
  265. #
  266. :type: `unicode`
  267. """)
  268. def _get_mime_headers(self):
  269. headers = []
  270. headers.append(('Project-Id-Version',
  271. '%s %s' % (self.project, self.version)))
  272. headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address))
  273. headers.append(('POT-Creation-Date',
  274. format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ',
  275. locale='en')))
  276. if self.locale is None:
  277. headers.append(('PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE'))
  278. headers.append(('Last-Translator', 'FULL NAME <EMAIL@ADDRESS>'))
  279. headers.append(('Language-Team', 'LANGUAGE <LL@li.org>'))
  280. else:
  281. headers.append(('PO-Revision-Date',
  282. format_datetime(self.revision_date,
  283. 'yyyy-MM-dd HH:mmZ', locale='en')))
  284. headers.append(('Last-Translator', self.last_translator))
  285. headers.append(('Language-Team',
  286. self.language_team.replace('LANGUAGE',
  287. str(self.locale))))
  288. headers.append(('Plural-Forms', self.plural_forms))
  289. headers.append(('MIME-Version', '1.0'))
  290. headers.append(('Content-Type',
  291. 'text/plain; charset=%s' % self.charset))
  292. headers.append(('Content-Transfer-Encoding', '8bit'))
  293. headers.append(('Generated-By', 'Babel %s\n' % VERSION))
  294. return headers
  295. def _set_mime_headers(self, headers):
  296. for name, value in headers:
  297. name = name.lower()
  298. if name == 'project-id-version':
  299. parts = value.split(' ')
  300. self.project = u(' ').join(parts[:-1])
  301. self.version = parts[-1]
  302. elif name == 'report-msgid-bugs-to':
  303. self.msgid_bugs_address = value
  304. elif name == 'last-translator':
  305. self.last_translator = value
  306. elif name == 'language-team':
  307. self.language_team = value
  308. elif name == 'content-type':
  309. mimetype, params = parse_header(value)
  310. if 'charset' in params:
  311. self.charset = params['charset'].lower()
  312. elif name == 'plural-forms':
  313. _, params = parse_header(' ;' + value)
  314. self._num_plurals = int(params.get('nplurals', 2))
  315. self._plural_expr = params.get('plural', '(n != 1)')
  316. elif name == 'pot-creation-date':
  317. # FIXME: this should use dates.parse_datetime as soon as that
  318. # is ready
  319. value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1)
  320. tt = time.strptime(value, '%Y-%m-%d %H:%M')
  321. ts = time.mktime(tt)
  322. # Separate the offset into a sign component, hours, and minutes
  323. plus_minus_s, rest = tzoffset[0], tzoffset[1:]
  324. hours_offset_s, mins_offset_s = rest[:2], rest[2:]
  325. # Make them all integers
  326. plus_minus = int(plus_minus_s + '1')
  327. hours_offset = int(hours_offset_s)
  328. mins_offset = int(mins_offset_s)
  329. # Calculate net offset
  330. net_mins_offset = hours_offset * 60
  331. net_mins_offset += mins_offset
  332. net_mins_offset *= plus_minus
  333. # Create an offset object
  334. tzoffset = FixedOffsetTimezone(net_mins_offset)
  335. # Store the offset in a datetime object
  336. dt = datetime.fromtimestamp(ts)
  337. self.creation_date = dt.replace(tzinfo=tzoffset)
  338. elif name == 'po-revision-date':
  339. # Keep the value if it's not the default one
  340. if 'YEAR' not in value:
  341. # FIXME: this should use dates.parse_datetime as soon as
  342. # that is ready
  343. value, tzoffset, _ = re.split('([+-]\d{4})$', value, 1)
  344. tt = time.strptime(value, '%Y-%m-%d %H:%M')
  345. ts = time.mktime(tt)
  346. # Separate the offset into a sign component, hours, and
  347. # minutes
  348. plus_minus_s, rest = tzoffset[0], tzoffset[1:]
  349. hours_offset_s, mins_offset_s = rest[:2], rest[2:]
  350. # Make them all integers
  351. plus_minus = int(plus_minus_s + '1')
  352. hours_offset = int(hours_offset_s)
  353. mins_offset = int(mins_offset_s)
  354. # Calculate net offset
  355. net_mins_offset = hours_offset * 60
  356. net_mins_offset += mins_offset
  357. net_mins_offset *= plus_minus
  358. # Create an offset object
  359. tzoffset = FixedOffsetTimezone(net_mins_offset)
  360. # Store the offset in a datetime object
  361. dt = datetime.fromtimestamp(ts)
  362. self.revision_date = dt.replace(tzinfo=tzoffset)
  363. mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\
  364. The MIME headers of the catalog, used for the special ``msgid ""`` entry.
  365. The behavior of this property changes slightly depending on whether a locale
  366. is set or not, the latter indicating that the catalog is actually a template
  367. for actual translations.
  368. Here's an example of the output for such a catalog template:
  369. >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC)
  370. >>> catalog = Catalog(project='Foobar', version='1.0',
  371. ... creation_date=created)
  372. >>> for name, value in catalog.mime_headers:
  373. ... print('%s: %s' % (name, value))
  374. Project-Id-Version: Foobar 1.0
  375. Report-Msgid-Bugs-To: EMAIL@ADDRESS
  376. POT-Creation-Date: 1990-04-01 15:30+0000
  377. PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
  378. Last-Translator: FULL NAME <EMAIL@ADDRESS>
  379. Language-Team: LANGUAGE <LL@li.org>
  380. MIME-Version: 1.0
  381. Content-Type: text/plain; charset=utf-8
  382. Content-Transfer-Encoding: 8bit
  383. Generated-By: Babel ...
  384. And here's an example of the output when the locale is set:
  385. >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC)
  386. >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0',
  387. ... creation_date=created, revision_date=revised,
  388. ... last_translator='John Doe <jd@example.com>',
  389. ... language_team='de_DE <de@example.com>')
  390. >>> for name, value in catalog.mime_headers:
  391. ... print('%s: %s' % (name, value))
  392. Project-Id-Version: Foobar 1.0
  393. Report-Msgid-Bugs-To: EMAIL@ADDRESS
  394. POT-Creation-Date: 1990-04-01 15:30+0000
  395. PO-Revision-Date: 1990-08-03 12:00+0000
  396. Last-Translator: John Doe <jd@example.com>
  397. Language-Team: de_DE <de@example.com>
  398. Plural-Forms: nplurals=2; plural=(n != 1)
  399. MIME-Version: 1.0
  400. Content-Type: text/plain; charset=utf-8
  401. Content-Transfer-Encoding: 8bit
  402. Generated-By: Babel ...
  403. :type: `list`
  404. """)
  405. def num_plurals(self):
  406. if self._num_plurals is None:
  407. num = 2
  408. if self.locale:
  409. num = get_plural(self.locale)[0]
  410. self._num_plurals = num
  411. return self._num_plurals
  412. num_plurals = property(num_plurals, doc="""\
  413. The number of plurals used by the catalog or locale.
  414. >>> Catalog(locale='en').num_plurals
  415. 2
  416. >>> Catalog(locale='ga').num_plurals
  417. 3
  418. :type: `int`
  419. """)
  420. def plural_expr(self):
  421. if self._plural_expr is None:
  422. expr = '(n != 1)'
  423. if self.locale:
  424. expr = get_plural(self.locale)[1]
  425. self._plural_expr = expr
  426. return self._plural_expr
  427. plural_expr = property(plural_expr, doc="""\
  428. The plural expression used by the catalog or locale.
  429. >>> Catalog(locale='en').plural_expr
  430. '(n != 1)'
  431. >>> Catalog(locale='ga').plural_expr
  432. '(n==1 ? 0 : n==2 ? 1 : 2)'
  433. :type: `basestring`
  434. """)
  435. def plural_forms(self):
  436. return 'nplurals=%s; plural=%s' % (self.num_plurals, self.plural_expr)
  437. plural_forms = property(plural_forms, doc="""\
  438. Return the plural forms declaration for the locale.
  439. >>> Catalog(locale='en').plural_forms
  440. 'nplurals=2; plural=(n != 1)'
  441. >>> Catalog(locale='pt_BR').plural_forms
  442. 'nplurals=2; plural=(n > 1)'
  443. :type: `str`
  444. """)
  445. def __contains__(self, id):
  446. """Return whether the catalog has a message with the specified ID."""
  447. return self._key_for(id) in self._messages
  448. def __len__(self):
  449. """The number of messages in the catalog.
  450. This does not include the special ``msgid ""`` entry.
  451. """
  452. return len(self._messages)
  453. def __iter__(self):
  454. """Iterates through all the entries in the catalog, in the order they
  455. were added, yielding a `Message` object for every entry.
  456. :rtype: ``iterator``
  457. """
  458. buf = []
  459. for name, value in self.mime_headers:
  460. buf.append('%s: %s' % (name, value))
  461. flags = set()
  462. if self.fuzzy:
  463. flags |= set(['fuzzy'])
  464. yield Message(u(''), '\n'.join(buf), flags=flags)
  465. for key in self._messages:
  466. yield self._messages[key]
  467. def __repr__(self):
  468. locale = ''
  469. if self.locale:
  470. locale = ' %s' % self.locale
  471. return '<%s %r%s>' % (type(self).__name__, self.domain, locale)
  472. def __delitem__(self, id):
  473. """Delete the message with the specified ID."""
  474. self.delete(id)
  475. def __getitem__(self, id):
  476. """Return the message with the specified ID.
  477. :param id: the message ID
  478. :return: the message with the specified ID, or `None` if no such
  479. message is in the catalog
  480. :rtype: `Message`
  481. """
  482. return self.get(id)
  483. def __setitem__(self, id, message):
  484. """Add or update the message with the specified ID.
  485. >>> catalog = Catalog()
  486. >>> catalog[u('foo')] = Message(u('foo'))
  487. >>> catalog[u('foo')]
  488. <Message foo (flags: [])>
  489. If a message with that ID is already in the catalog, it is updated
  490. to include the locations and flags of the new message.
  491. >>> catalog = Catalog()
  492. >>> catalog[u('foo')] = Message(u('foo'), locations=[('main.py', 1)])
  493. >>> catalog[u('foo')].locations
  494. [('main.py', 1)]
  495. >>> catalog[u('foo')] = Message(u('foo'), locations=[('utils.py', 5)])
  496. >>> catalog[u('foo')].locations
  497. [('main.py', 1), ('utils.py', 5)]
  498. :param id: the message ID
  499. :param message: the `Message` object
  500. """
  501. assert isinstance(message, Message), 'expected a Message object'
  502. key = self._key_for(id, message.context)
  503. current = self._messages.get(key)
  504. if current:
  505. if message.pluralizable and not current.pluralizable:
  506. # The new message adds pluralization
  507. current.id = message.id
  508. current.string = message.string
  509. current.locations = list(distinct(current.locations +
  510. message.locations))
  511. current.auto_comments = list(distinct(current.auto_comments +
  512. message.auto_comments))
  513. current.user_comments = list(distinct(current.user_comments +
  514. message.user_comments))
  515. current.flags |= message.flags
  516. message = current
  517. elif id == '':
  518. # special treatment for the header message
  519. def _parse_header(header_string):
  520. # message_from_string only works for str, not for unicode
  521. if not PY3:
  522. header_string = header_string.encode('utf8')
  523. headers = message_from_string(header_string)
  524. decoded_headers = {}
  525. for name, value in headers.items():
  526. if not PY3:
  527. name, value = name.decode('utf8'), value.decode('utf8')
  528. decoded_headers[name] = value
  529. return decoded_headers
  530. self.mime_headers = list(_parse_header(message.string).items())
  531. self.header_comment = '\n'.join(['# %s' % comment for comment
  532. in message.user_comments])
  533. self.fuzzy = message.fuzzy
  534. else:
  535. if isinstance(id, (list, tuple)):
  536. assert isinstance(message.string, (list, tuple)), \
  537. 'Expected sequence but got %s' % type(message.string)
  538. self._messages[key] = message
  539. def add(self, id, string=None, locations=(), flags=(), auto_comments=(),
  540. user_comments=(), previous_id=(), lineno=None, context=None):
  541. """Add or update the message with the specified ID.
  542. >>> catalog = Catalog()
  543. >>> catalog.add(u('foo'))
  544. <Message ...>
  545. >>> catalog[u('foo')]
  546. <Message foo (flags: [])>
  547. This method simply constructs a `Message` object with the given
  548. arguments and invokes `__setitem__` with that object.
  549. :param id: the message ID, or a ``(singular, plural)`` tuple for
  550. pluralizable messages
  551. :param string: the translated message string, or a
  552. ``(singular, plural)`` tuple for pluralizable messages
  553. :param locations: a sequence of ``(filenname, lineno)`` tuples
  554. :param flags: a set or sequence of flags
  555. :param auto_comments: a sequence of automatic comments
  556. :param user_comments: a sequence of user comments
  557. :param previous_id: the previous message ID, or a ``(singular, plural)``
  558. tuple for pluralizable messages
  559. :param lineno: the line number on which the msgid line was found in the
  560. PO file, if any
  561. :param context: the message context
  562. :return: the newly added message
  563. :rtype: `Message`
  564. """
  565. message = Message(id, string, list(locations), flags, auto_comments,
  566. user_comments, previous_id, lineno=lineno,
  567. context=context)
  568. self[id] = message
  569. return message
  570. def check(self):
  571. """Run various validation checks on the translations in the catalog.
  572. For every message which fails validation, this method yield a
  573. ``(message, errors)`` tuple, where ``message`` is the `Message` object
  574. and ``errors`` is a sequence of `TranslationError` objects.
  575. :rtype: ``iterator``
  576. """
  577. for message in self._messages.values():
  578. errors = message.check(catalog=self)
  579. if errors:
  580. yield message, errors
  581. def get(self, id, context=None):
  582. """Return the message with the specified ID and context.
  583. :param id: the message ID
  584. :param context: the message context, or ``None`` for no context
  585. :return: the message with the specified ID, or `None` if no such
  586. message is in the catalog
  587. :rtype: `Message`
  588. """
  589. return self._messages.get(self._key_for(id, context))
  590. def delete(self, id, context=None):
  591. """Delete the message with the specified ID and context.
  592. :param id: the message ID
  593. :param context: the message context, or ``None`` for no context
  594. """
  595. key = self._key_for(id, context)
  596. if key in self._messages:
  597. del self._messages[key]
  598. def update(self, template, no_fuzzy_matching=False):
  599. """Update the catalog based on the given template catalog.
  600. >>> from babel.messages import Catalog
  601. >>> template = Catalog()
  602. >>> template.add('green', locations=[('main.py', 99)])
  603. <Message ...>
  604. >>> template.add('blue', locations=[('main.py', 100)])
  605. <Message ...>
  606. >>> template.add(('salad', 'salads'), locations=[('util.py', 42)])
  607. <Message ...>
  608. >>> catalog = Catalog(locale='de_DE')
  609. >>> catalog.add('blue', u('blau'), locations=[('main.py', 98)])
  610. <Message ...>
  611. >>> catalog.add('head', u('Kopf'), locations=[('util.py', 33)])
  612. <Message ...>
  613. >>> catalog.add(('salad', 'salads'), (u('Salat'), u('Salate')),
  614. ... locations=[('util.py', 38)])
  615. <Message ...>
  616. >>> catalog.update(template)
  617. >>> len(catalog)
  618. 3
  619. >>> msg1 = catalog['green']
  620. >>> msg1.string
  621. >>> msg1.locations
  622. [('main.py', 99)]
  623. >>> msg2 = catalog['blue']
  624. >>> print(msg2.string)
  625. blau
  626. >>> msg2.locations
  627. [('main.py', 100)]
  628. >>> msg3 = catalog['salad']
  629. >>> print(msg3.string[0])
  630. Salat
  631. >>> print(msg3.string[1])
  632. Salate
  633. >>> msg3.locations
  634. [('util.py', 42)]
  635. Messages that are in the catalog but not in the template are removed
  636. from the main collection, but can still be accessed via the `obsolete`
  637. member:
  638. >>> 'head' in catalog
  639. False
  640. >>> for v in catalog.obsolete.values():
  641. ... print(v)
  642. <Message head (flags: [])>
  643. :param template: the reference catalog, usually read from a POT file
  644. :param no_fuzzy_matching: whether to use fuzzy matching of message IDs
  645. """
  646. messages = self._messages
  647. remaining = messages.copy()
  648. self._messages = odict()
  649. # Prepare for fuzzy matching
  650. fuzzy_candidates = []
  651. if not no_fuzzy_matching:
  652. fuzzy_candidates = dict([
  653. (self._key_for(msgid), messages[msgid].context)
  654. for msgid in messages if msgid and messages[msgid].string
  655. ])
  656. fuzzy_matches = set()
  657. def _merge(message, oldkey, newkey):
  658. message = message.clone()
  659. fuzzy = False
  660. if oldkey != newkey:
  661. fuzzy = True
  662. fuzzy_matches.add(oldkey)
  663. oldmsg = messages.get(oldkey)
  664. if isinstance(oldmsg.id, string_types):
  665. message.previous_id = [oldmsg.id]
  666. else:
  667. message.previous_id = list(oldmsg.id)
  668. else:
  669. oldmsg = remaining.pop(oldkey, None)
  670. message.string = oldmsg.string
  671. if isinstance(message.id, (list, tuple)):
  672. if not isinstance(message.string, (list, tuple)):
  673. fuzzy = True
  674. message.string = tuple(
  675. [message.string] + ([u('')] * (len(message.id) - 1))
  676. )
  677. elif len(message.string) != self.num_plurals:
  678. fuzzy = True
  679. message.string = tuple(message.string[:len(oldmsg.string)])
  680. elif isinstance(message.string, (list, tuple)):
  681. fuzzy = True
  682. message.string = message.string[0]
  683. message.flags |= oldmsg.flags
  684. if fuzzy:
  685. message.flags |= set([u('fuzzy')])
  686. self[message.id] = message
  687. for message in template:
  688. if message.id:
  689. key = self._key_for(message.id, message.context)
  690. if key in messages:
  691. _merge(message, key, key)
  692. else:
  693. if no_fuzzy_matching is False:
  694. # do some fuzzy matching with difflib
  695. if isinstance(key, tuple):
  696. matchkey = key[0] # just the msgid, no context
  697. else:
  698. matchkey = key
  699. matches = get_close_matches(matchkey.lower().strip(),
  700. fuzzy_candidates.keys(), 1)
  701. if matches:
  702. newkey = matches[0]
  703. newctxt = fuzzy_candidates[newkey]
  704. if newctxt is not None:
  705. newkey = newkey, newctxt
  706. _merge(message, newkey, key)
  707. continue
  708. self[message.id] = message
  709. self.obsolete = odict()
  710. for msgid in remaining:
  711. if no_fuzzy_matching or msgid not in fuzzy_matches:
  712. self.obsolete[msgid] = remaining[msgid]
  713. # Make updated catalog's POT-Creation-Date equal to the template
  714. # used to update the catalog
  715. self.creation_date = template.creation_date
  716. def _key_for(self, id, context=None):
  717. """The key for a message is just the singular ID even for pluralizable
  718. messages, but is a ``(msgid, msgctxt)`` tuple for context-specific
  719. messages.
  720. """
  721. key = id
  722. if isinstance(key, (list, tuple)):
  723. key = id[0]
  724. if context is not None:
  725. key = (key, context)
  726. return key