cmlenz@56: # -*- coding: utf-8 -*- cmlenz@56: # cmlenz@56: # Copyright (C) 2007 Edgewall Software cmlenz@56: # All rights reserved. cmlenz@56: # cmlenz@56: # This software is licensed as described in the file COPYING, which cmlenz@56: # you should have received as part of this distribution. The terms cmlenz@56: # are also available at http://babel.edgewall.org/wiki/License. cmlenz@56: # cmlenz@56: # This software consists of voluntary contributions made by many cmlenz@56: # individuals. For the exact contribution history, see the revision cmlenz@56: # history and logs, available at http://babel.edgewall.org/log/. cmlenz@56: cmlenz@56: """Data structures for message catalogs.""" cmlenz@56: cmlenz@149: from cgi import parse_header cmlenz@67: from datetime import datetime cmlenz@165: from difflib import get_close_matches cmlenz@106: from email import message_from_string cmlenz@56: import re cmlenz@56: try: cmlenz@56: set cmlenz@56: except NameError: cmlenz@56: from sets import Set as set cmlenz@67: import time cmlenz@56: cmlenz@67: from babel import __version__ as VERSION cmlenz@64: from babel.core import Locale cmlenz@131: from babel.dates import format_datetime cmlenz@67: from babel.messages.plurals import PLURALS cmlenz@227: from babel.util import odict, distinct, LOCALTZ, UTC, FixedOffsetTimezone cmlenz@56: cmlenz@220: __all__ = ['Message', 'Catalog', 'TranslationError'] cmlenz@56: __docformat__ = 'restructuredtext en' cmlenz@56: cmlenz@229: PYTHON_FORMAT = re.compile(r'\%(\([\w]+\))?([-#0\ +])?(\*|[\d]+)?' cmlenz@229: r'(\.(\*|[\d]+))?([hlL])?[diouxXeEfFgGcrs]') cmlenz@56: cmlenz@56: cmlenz@56: class Message(object): cmlenz@56: """Representation of a single message in a catalog.""" cmlenz@56: cmlenz@149: def __init__(self, id, string=u'', locations=(), flags=(), auto_comments=(), cmlenz@220: user_comments=(), previous_id=(), lineno=None): cmlenz@56: """Create the message object. palgarvio@200: cmlenz@56: :param id: the message ID, or a ``(singular, plural)`` tuple for cmlenz@56: pluralizable messages cmlenz@56: :param string: the translated message string, or a cmlenz@56: ``(singular, plural)`` tuple for pluralizable messages cmlenz@56: :param locations: a sequence of ``(filenname, lineno)`` tuples cmlenz@56: :param flags: a set or sequence of flags cmlenz@106: :param auto_comments: a sequence of automatic comments for the message cmlenz@106: :param user_comments: a sequence of user comments for the message cmlenz@203: :param previous_id: the previous message ID, or a ``(singular, plural)`` cmlenz@203: tuple for pluralizable messages cmlenz@220: :param lineno: the line number on which the msgid line was found in the cmlenz@220: PO file, if any cmlenz@56: """ cmlenz@107: self.id = id #: The message ID cmlenz@68: if not string and self.pluralizable: cmlenz@68: string = (u'', u'') cmlenz@107: self.string = string #: The message translation cmlenz@229: self.locations = list(distinct(locations)) cmlenz@56: self.flags = set(flags) cmlenz@67: if id and self.python_format: cmlenz@56: self.flags.add('python-format') cmlenz@56: else: cmlenz@56: self.flags.discard('python-format') cmlenz@227: self.auto_comments = list(distinct(auto_comments)) cmlenz@227: self.user_comments = list(distinct(user_comments)) cmlenz@203: if isinstance(previous_id, basestring): cmlenz@203: self.previous_id = [previous_id] palgarvio@200: else: cmlenz@203: self.previous_id = list(previous_id) cmlenz@220: self.lineno = lineno cmlenz@56: cmlenz@56: def __repr__(self): cmlenz@196: return '<%s %r (flags: %r)>' % (type(self).__name__, self.id, cmlenz@196: list(self.flags)) cmlenz@56: pjenvey@248: def __cmp__(self, obj): pjenvey@248: """Compare Messages, taking into account plural ids""" pjenvey@248: if isinstance(obj, Message): pjenvey@248: plural = self.pluralizable pjenvey@248: obj_plural = obj.pluralizable pjenvey@248: if plural and obj_plural: pjenvey@248: return cmp(self.id[0], obj.id[0]) pjenvey@248: elif plural: pjenvey@248: return cmp(self.id[0], obj.id) pjenvey@248: elif obj_plural: pjenvey@248: return cmp(self.id, obj.id[0]) pjenvey@248: return cmp(self.id, obj.id) pjenvey@248: cmlenz@67: def fuzzy(self): cmlenz@67: return 'fuzzy' in self.flags cmlenz@67: fuzzy = property(fuzzy, doc="""\ cmlenz@67: Whether the translation is fuzzy. palgarvio@200: cmlenz@67: >>> Message('foo').fuzzy cmlenz@67: False palgarvio@175: >>> msg = Message('foo', 'foo', flags=['fuzzy']) palgarvio@175: >>> msg.fuzzy cmlenz@67: True palgarvio@175: >>> msg cmlenz@196: palgarvio@200: cmlenz@67: :type: `bool` cmlenz@67: """) cmlenz@67: cmlenz@56: def pluralizable(self): cmlenz@56: return isinstance(self.id, (list, tuple)) cmlenz@56: pluralizable = property(pluralizable, doc="""\ cmlenz@56: Whether the message is plurizable. palgarvio@200: cmlenz@56: >>> Message('foo').pluralizable cmlenz@56: False cmlenz@56: >>> Message(('foo', 'bar')).pluralizable cmlenz@56: True palgarvio@200: cmlenz@61: :type: `bool` cmlenz@56: """) cmlenz@56: cmlenz@56: def python_format(self): cmlenz@56: ids = self.id cmlenz@56: if not isinstance(ids, (list, tuple)): cmlenz@56: ids = [ids] cmlenz@220: return bool(filter(None, [PYTHON_FORMAT.search(id) for id in ids])) cmlenz@56: python_format = property(python_format, doc="""\ cmlenz@56: Whether the message contains Python-style parameters. palgarvio@200: cmlenz@56: >>> Message('foo %(name)s bar').python_format cmlenz@56: True cmlenz@56: >>> Message(('foo %(name)s', 'foo %(name)s')).python_format cmlenz@56: True palgarvio@200: cmlenz@61: :type: `bool` cmlenz@56: """) cmlenz@56: palgarvio@105: cmlenz@220: class TranslationError(Exception): cmlenz@220: """Exception thrown by translation checkers when invalid message cmlenz@220: translations are encountered.""" cmlenz@220: cmlenz@220: cmlenz@104: DEFAULT_HEADER = u"""\ cmlenz@104: # Translations template for PROJECT. cmlenz@120: # Copyright (C) YEAR ORGANIZATION cmlenz@104: # This file is distributed under the same license as the PROJECT project. cmlenz@104: # FIRST AUTHOR , YEAR. cmlenz@104: #""" cmlenz@56: cmlenz@196: cmlenz@56: class Catalog(object): palgarvio@78: """Representation of a message catalog.""" cmlenz@56: cmlenz@104: def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER, cmlenz@104: project=None, version=None, copyright_holder=None, palgarvio@78: msgid_bugs_address=None, creation_date=None, cmlenz@206: revision_date=None, last_translator=None, language_team=None, cmlenz@206: charset='utf-8', fuzzy=True): cmlenz@64: """Initialize the catalog object. palgarvio@200: cmlenz@64: :param locale: the locale identifier or `Locale` object, or `None` cmlenz@64: if the catalog is not bound to a locale (which basically cmlenz@64: means it's a template) palgarvio@78: :param domain: the message domain cmlenz@104: :param header_comment: the header comment as string, or `None` for the cmlenz@104: default header palgarvio@78: :param project: the project's name palgarvio@78: :param version: the project's version cmlenz@104: :param copyright_holder: the copyright holder of the catalog cmlenz@104: :param msgid_bugs_address: the email address or URL to submit bug cmlenz@104: reports to palgarvio@78: :param creation_date: the date the catalog was created palgarvio@78: :param revision_date: the date the catalog was revised palgarvio@78: :param last_translator: the name and email of the last translator cmlenz@206: :param language_team: the name and email of the language team cmlenz@104: :param charset: the encoding to use in the output palgarvio@175: :param fuzzy: the fuzzy bit on the catalog header cmlenz@64: """ cmlenz@107: self.domain = domain #: The message domain cmlenz@64: if locale: cmlenz@64: locale = Locale.parse(locale) cmlenz@107: self.locale = locale #: The locale or `None` cmlenz@104: self._header_comment = header_comment cmlenz@67: self._messages = odict() cmlenz@67: cmlenz@107: self.project = project or 'PROJECT' #: The project name cmlenz@107: self.version = version or 'VERSION' #: The project version cmlenz@104: self.copyright_holder = copyright_holder or 'ORGANIZATION' palgarvio@78: self.msgid_bugs_address = msgid_bugs_address or 'EMAIL@ADDRESS' cmlenz@106: cmlenz@106: self.last_translator = last_translator or 'FULL NAME ' cmlenz@106: """Name and email address of the last translator.""" cmlenz@206: self.language_team = language_team or 'LANGUAGE ' cmlenz@206: """Name and email address of the language team.""" cmlenz@106: cmlenz@95: self.charset = charset or 'utf-8' cmlenz@84: cmlenz@67: if creation_date is None: cmlenz@97: creation_date = datetime.now(LOCALTZ) cmlenz@95: elif isinstance(creation_date, datetime) and not creation_date.tzinfo: cmlenz@97: creation_date = creation_date.replace(tzinfo=LOCALTZ) cmlenz@107: self.creation_date = creation_date #: Creation date of the template cmlenz@67: if revision_date is None: cmlenz@97: revision_date = datetime.now(LOCALTZ) cmlenz@95: elif isinstance(revision_date, datetime) and not revision_date.tzinfo: cmlenz@97: revision_date = revision_date.replace(tzinfo=LOCALTZ) cmlenz@107: self.revision_date = revision_date #: Last revision date of the catalog cmlenz@181: self.fuzzy = fuzzy #: Catalog header fuzzy bit (`True` or `False`) cmlenz@181: cmlenz@181: self.obsolete = odict() #: Dictionary of obsolete messages cmlenz@67: cmlenz@107: def _get_header_comment(self): cmlenz@104: comment = self._header_comment cmlenz@104: comment = comment.replace('PROJECT', self.project) \ cmlenz@104: .replace('VERSION', self.version) \ cmlenz@104: .replace('YEAR', self.revision_date.strftime('%Y')) \ cmlenz@120: .replace('ORGANIZATION', self.copyright_holder) cmlenz@104: if self.locale: cmlenz@107: comment = comment.replace('Translations template', '%s translations' cmlenz@107: % self.locale.english_name) cmlenz@104: return comment cmlenz@120: cmlenz@107: def _set_header_comment(self, string): cmlenz@104: self._header_comment = string cmlenz@107: cmlenz@107: header_comment = property(_get_header_comment, _set_header_comment, doc="""\ cmlenz@104: The header comment for the catalog. palgarvio@200: cmlenz@104: >>> catalog = Catalog(project='Foobar', version='1.0', cmlenz@104: ... copyright_holder='Foo Company') cmlenz@104: >>> print catalog.header_comment cmlenz@104: # Translations template for Foobar. cmlenz@104: # Copyright (C) 2007 Foo Company cmlenz@104: # This file is distributed under the same license as the Foobar project. cmlenz@104: # FIRST AUTHOR , 2007. cmlenz@104: # palgarvio@200: cmlenz@120: The header can also be set from a string. Any known upper-case variables cmlenz@120: will be replaced when the header is retrieved again: palgarvio@200: cmlenz@120: >>> catalog = Catalog(project='Foobar', version='1.0', cmlenz@120: ... copyright_holder='Foo Company') cmlenz@120: >>> catalog.header_comment = '''\\ cmlenz@120: ... # The POT for my really cool PROJECT project. cmlenz@120: ... # Copyright (C) 1990-2003 ORGANIZATION cmlenz@120: ... # This file is distributed under the same license as the PROJECT cmlenz@120: ... # project. cmlenz@120: ... #''' cmlenz@120: >>> print catalog.header_comment cmlenz@120: # The POT for my really cool Foobar project. cmlenz@120: # Copyright (C) 1990-2003 Foo Company cmlenz@120: # This file is distributed under the same license as the Foobar cmlenz@120: # project. cmlenz@120: # cmlenz@120: cmlenz@104: :type: `unicode` cmlenz@104: """) cmlenz@104: cmlenz@106: def _get_mime_headers(self): cmlenz@67: headers = [] cmlenz@67: headers.append(('Project-Id-Version', cmlenz@67: '%s %s' % (self.project, self.version))) palgarvio@78: headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address)) cmlenz@67: headers.append(('POT-Creation-Date', cmlenz@131: format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ', cmlenz@131: locale='en'))) cmlenz@67: if self.locale is None: cmlenz@67: headers.append(('PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE')) cmlenz@67: headers.append(('Last-Translator', 'FULL NAME ')) cmlenz@67: headers.append(('Language-Team', 'LANGUAGE ')) cmlenz@67: else: cmlenz@67: headers.append(('PO-Revision-Date', cmlenz@131: format_datetime(self.revision_date, cmlenz@131: 'yyyy-MM-dd HH:mmZ', locale='en'))) cmlenz@67: headers.append(('Last-Translator', self.last_translator)) cmlenz@206: headers.append(('Language-Team', cmlenz@206: self.language_team.replace('LANGUAGE', cmlenz@206: str(self.locale)))) cmlenz@84: headers.append(('Plural-Forms', self.plural_forms)) cmlenz@67: headers.append(('MIME-Version', '1.0')) cmlenz@68: headers.append(('Content-Type', cmlenz@68: 'text/plain; charset=%s' % self.charset)) cmlenz@67: headers.append(('Content-Transfer-Encoding', '8bit')) palgarvio@105: headers.append(('Generated-By', 'Babel %s\n' % VERSION)) cmlenz@67: return headers cmlenz@106: cmlenz@106: def _set_mime_headers(self, headers): cmlenz@106: for name, value in headers: pjenvey@293: if name.lower() == 'content-type': cmlenz@210: mimetype, params = parse_header(value) cmlenz@210: if 'charset' in params: cmlenz@210: self.charset = params['charset'].lower() cmlenz@210: break cmlenz@210: for name, value in headers: cmlenz@210: name = name.lower().decode(self.charset) cmlenz@210: value = value.decode(self.charset) cmlenz@106: if name == 'project-id-version': cmlenz@106: parts = value.split(' ') cmlenz@210: self.project = u' '.join(parts[:-1]) cmlenz@106: self.version = parts[-1] cmlenz@106: elif name == 'report-msgid-bugs-to': cmlenz@106: self.msgid_bugs_address = value cmlenz@106: elif name == 'last-translator': cmlenz@106: self.last_translator = value cmlenz@206: elif name == 'language-team': cmlenz@206: self.language_team = value cmlenz@106: elif name == 'pot-creation-date': cmlenz@106: # FIXME: this should use dates.parse_datetime as soon as that cmlenz@106: # is ready cmlenz@106: value, tzoffset, _ = re.split('[+-](\d{4})$', value, 1) cmlenz@106: tt = time.strptime(value, '%Y-%m-%d %H:%M') cmlenz@106: ts = time.mktime(tt) cmlenz@120: tzoffset = FixedOffsetTimezone(int(tzoffset[:2]) * 60 + cmlenz@120: int(tzoffset[2:])) cmlenz@121: dt = datetime.fromtimestamp(ts) cmlenz@121: self.creation_date = dt.replace(tzinfo=tzoffset) cmlenz@106: cmlenz@106: mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\ cmlenz@67: The MIME headers of the catalog, used for the special ``msgid ""`` entry. palgarvio@200: cmlenz@67: The behavior of this property changes slightly depending on whether a locale cmlenz@67: is set or not, the latter indicating that the catalog is actually a template cmlenz@67: for actual translations. palgarvio@200: cmlenz@67: Here's an example of the output for such a catalog template: palgarvio@200: cmlenz@95: >>> created = datetime(1990, 4, 1, 15, 30, tzinfo=UTC) cmlenz@67: >>> catalog = Catalog(project='Foobar', version='1.0', cmlenz@95: ... creation_date=created) cmlenz@104: >>> for name, value in catalog.mime_headers: cmlenz@67: ... print '%s: %s' % (name, value) cmlenz@67: Project-Id-Version: Foobar 1.0 palgarvio@78: Report-Msgid-Bugs-To: EMAIL@ADDRESS cmlenz@67: POT-Creation-Date: 1990-04-01 15:30+0000 cmlenz@67: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE cmlenz@67: Last-Translator: FULL NAME cmlenz@67: Language-Team: LANGUAGE cmlenz@67: MIME-Version: 1.0 cmlenz@67: Content-Type: text/plain; charset=utf-8 cmlenz@67: Content-Transfer-Encoding: 8bit cmlenz@67: Generated-By: Babel ... palgarvio@200: cmlenz@67: And here's an example of the output when the locale is set: palgarvio@200: cmlenz@95: >>> revised = datetime(1990, 8, 3, 12, 0, tzinfo=UTC) cmlenz@67: >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0', cmlenz@95: ... creation_date=created, revision_date=revised, cmlenz@206: ... last_translator='John Doe ', cmlenz@206: ... language_team='de_DE ') cmlenz@104: >>> for name, value in catalog.mime_headers: cmlenz@67: ... print '%s: %s' % (name, value) cmlenz@67: Project-Id-Version: Foobar 1.0 palgarvio@78: Report-Msgid-Bugs-To: EMAIL@ADDRESS cmlenz@67: POT-Creation-Date: 1990-04-01 15:30+0000 cmlenz@67: PO-Revision-Date: 1990-08-03 12:00+0000 cmlenz@67: Last-Translator: John Doe cmlenz@206: Language-Team: de_DE cmlenz@84: Plural-Forms: nplurals=2; plural=(n != 1) cmlenz@67: MIME-Version: 1.0 cmlenz@67: Content-Type: text/plain; charset=utf-8 cmlenz@67: Content-Transfer-Encoding: 8bit cmlenz@67: Generated-By: Babel ... palgarvio@200: cmlenz@67: :type: `list` cmlenz@67: """) cmlenz@67: cmlenz@68: def num_plurals(self): cmlenz@68: num = 2 cmlenz@68: if self.locale: cmlenz@68: if str(self.locale) in PLURALS: cmlenz@68: num = PLURALS[str(self.locale)][0] cmlenz@68: elif self.locale.language in PLURALS: cmlenz@68: num = PLURALS[self.locale.language][0] cmlenz@68: return num cmlenz@84: num_plurals = property(num_plurals, doc="""\ cmlenz@84: The number of plurals used by the locale. palgarvio@200: cmlenz@103: >>> Catalog(locale='en').num_plurals cmlenz@103: 2 cmlenz@103: >>> Catalog(locale='cs_CZ').num_plurals cmlenz@103: 3 palgarvio@200: cmlenz@103: :type: `int` cmlenz@84: """) cmlenz@68: cmlenz@67: def plural_forms(self): cmlenz@67: num, expr = ('INTEGER', 'EXPRESSION') cmlenz@67: if self.locale: cmlenz@67: if str(self.locale) in PLURALS: cmlenz@67: num, expr = PLURALS[str(self.locale)] cmlenz@67: elif self.locale.language in PLURALS: cmlenz@67: num, expr = PLURALS[self.locale.language] cmlenz@67: return 'nplurals=%s; plural=%s' % (num, expr) cmlenz@67: plural_forms = property(plural_forms, doc="""\ cmlenz@67: Return the plural forms declaration for the locale. palgarvio@200: cmlenz@103: >>> Catalog(locale='en').plural_forms cmlenz@67: 'nplurals=2; plural=(n != 1)' cmlenz@67: >>> Catalog(locale='pt_BR').plural_forms cmlenz@67: 'nplurals=2; plural=(n > 1)' palgarvio@200: cmlenz@67: :type: `str` cmlenz@67: """) cmlenz@67: cmlenz@67: def __contains__(self, id): cmlenz@67: """Return whether the catalog has a message with the specified ID.""" cmlenz@69: return self._key_for(id) in self._messages cmlenz@69: cmlenz@69: def __len__(self): cmlenz@84: """The number of messages in the catalog. palgarvio@200: cmlenz@84: This does not include the special ``msgid ""`` entry. cmlenz@84: """ cmlenz@69: return len(self._messages) cmlenz@56: cmlenz@56: def __iter__(self): cmlenz@64: """Iterates through all the entries in the catalog, in the order they cmlenz@64: were added, yielding a `Message` object for every entry. palgarvio@200: cmlenz@64: :rtype: ``iterator`` cmlenz@64: """ cmlenz@67: buf = [] cmlenz@104: for name, value in self.mime_headers: cmlenz@67: buf.append('%s: %s' % (name, value)) cmlenz@198: flags = set() palgarvio@175: if self.fuzzy: cmlenz@198: flags |= set(['fuzzy']) cmlenz@210: yield Message(u'', '\n'.join(buf), flags=flags) cmlenz@69: for key in self._messages: cmlenz@69: yield self._messages[key] cmlenz@56: cmlenz@56: def __repr__(self): cmlenz@64: locale = '' cmlenz@64: if self.locale: cmlenz@64: locale = ' %s' % self.locale cmlenz@64: return '<%s %r%s>' % (type(self).__name__, self.domain, locale) cmlenz@56: cmlenz@56: def __delitem__(self, id): cmlenz@64: """Delete the message with the specified ID.""" cmlenz@69: key = self._key_for(id) cmlenz@69: if key in self._messages: cmlenz@69: del self._messages[key] cmlenz@56: cmlenz@56: def __getitem__(self, id): cmlenz@64: """Return the message with the specified ID. palgarvio@200: cmlenz@64: :param id: the message ID cmlenz@64: :return: the message with the specified ID, or `None` if no such message cmlenz@64: is in the catalog cmlenz@67: :rtype: `Message` cmlenz@64: """ cmlenz@69: return self._messages.get(self._key_for(id)) cmlenz@56: cmlenz@56: def __setitem__(self, id, message): cmlenz@64: """Add or update the message with the specified ID. palgarvio@200: cmlenz@64: >>> catalog = Catalog() cmlenz@64: >>> catalog[u'foo'] = Message(u'foo') cmlenz@64: >>> catalog[u'foo'] cmlenz@196: palgarvio@200: cmlenz@64: If a message with that ID is already in the catalog, it is updated cmlenz@64: to include the locations and flags of the new message. palgarvio@200: cmlenz@64: >>> catalog = Catalog() cmlenz@64: >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)]) cmlenz@64: >>> catalog[u'foo'].locations cmlenz@64: [('main.py', 1)] cmlenz@64: >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)]) cmlenz@64: >>> catalog[u'foo'].locations cmlenz@64: [('main.py', 1), ('utils.py', 5)] palgarvio@200: cmlenz@64: :param id: the message ID cmlenz@64: :param message: the `Message` object cmlenz@64: """ cmlenz@56: assert isinstance(message, Message), 'expected a Message object' cmlenz@69: key = self._key_for(id) cmlenz@69: current = self._messages.get(key) cmlenz@56: if current: cmlenz@69: if message.pluralizable and not current.pluralizable: cmlenz@69: # The new message adds pluralization cmlenz@69: current.id = message.id cmlenz@70: current.string = message.string cmlenz@229: current.locations = list(distinct(current.locations + cmlenz@229: message.locations)) cmlenz@228: current.auto_comments = list(distinct(current.auto_comments + cmlenz@228: message.auto_comments)) cmlenz@228: current.user_comments = list(distinct(current.user_comments + cmlenz@228: message.user_comments)) cmlenz@56: current.flags |= message.flags cmlenz@56: message = current cmlenz@106: elif id == '': cmlenz@106: # special treatment for the header message cmlenz@106: headers = message_from_string(message.string.encode(self.charset)) cmlenz@106: self.mime_headers = headers.items() cmlenz@120: self.header_comment = '\n'.join(['# %s' % comment for comment cmlenz@120: in message.user_comments]) cmlenz@196: self.fuzzy = message.fuzzy cmlenz@56: else: cmlenz@56: if isinstance(id, (list, tuple)): cmlenz@278: assert isinstance(message.string, (list, tuple)), \ cmlenz@278: 'Expected sequence but got %s' % type(message.string) cmlenz@69: self._messages[key] = message cmlenz@56: palgarvio@105: def add(self, id, string=None, locations=(), flags=(), auto_comments=(), cmlenz@220: user_comments=(), previous_id=(), lineno=None): cmlenz@64: """Add or update the message with the specified ID. palgarvio@200: cmlenz@64: >>> catalog = Catalog() cmlenz@64: >>> catalog.add(u'foo') cmlenz@64: >>> catalog[u'foo'] cmlenz@196: palgarvio@200: cmlenz@64: This method simply constructs a `Message` object with the given cmlenz@64: arguments and invokes `__setitem__` with that object. palgarvio@200: cmlenz@64: :param id: the message ID, or a ``(singular, plural)`` tuple for cmlenz@64: pluralizable messages cmlenz@64: :param string: the translated message string, or a cmlenz@64: ``(singular, plural)`` tuple for pluralizable messages cmlenz@64: :param locations: a sequence of ``(filenname, lineno)`` tuples cmlenz@64: :param flags: a set or sequence of flags cmlenz@106: :param auto_comments: a sequence of automatic comments cmlenz@106: :param user_comments: a sequence of user comments cmlenz@203: :param previous_id: the previous message ID, or a ``(singular, plural)`` cmlenz@203: tuple for pluralizable messages cmlenz@220: :param lineno: the line number on which the msgid line was found in the cmlenz@220: PO file, if any cmlenz@64: """ palgarvio@105: self[id] = Message(id, string, list(locations), flags, auto_comments, cmlenz@220: user_comments, previous_id, lineno=lineno) cmlenz@220: cmlenz@220: def check(self): cmlenz@220: """Run various validation checks on the translations in the catalog. palgarvio@226: cmlenz@220: For every message which fails validation, this method yield a cmlenz@220: ``(message, errors)`` tuple, where ``message`` is the `Message` object cmlenz@220: and ``errors`` is a sequence of `TranslationError` objects. palgarvio@226: cmlenz@250: :note: this feature requires ``setuptools``/``pkg_resources`` to be cmlenz@250: installed; if it is not, this method will simply return an empty cmlenz@250: iterator cmlenz@220: :rtype: ``iterator`` cmlenz@220: """ cmlenz@220: checkers = [] cmlenz@250: try: cmlenz@250: from pkg_resources import working_set cmlenz@250: except ImportError: cmlenz@250: return cmlenz@250: else: cmlenz@250: for entry_point in working_set.iter_entry_points('babel.checkers'): cmlenz@250: checkers.append(entry_point.load()) cmlenz@250: for message in self._messages.values(): cmlenz@250: errors = [] cmlenz@250: for checker in checkers: cmlenz@250: try: cmlenz@250: checker(self, message) cmlenz@250: except TranslationError, e: cmlenz@250: errors.append(e) cmlenz@250: if errors: cmlenz@250: yield message, errors cmlenz@69: cmlenz@203: def update(self, template, no_fuzzy_matching=False): cmlenz@163: """Update the catalog based on the given template catalog. palgarvio@200: cmlenz@163: >>> from babel.messages import Catalog cmlenz@163: >>> template = Catalog() cmlenz@188: >>> template.add('green', locations=[('main.py', 99)]) cmlenz@163: >>> template.add('blue', locations=[('main.py', 100)]) cmlenz@163: >>> template.add(('salad', 'salads'), locations=[('util.py', 42)]) cmlenz@163: >>> catalog = Catalog(locale='de_DE') cmlenz@163: >>> catalog.add('blue', u'blau', locations=[('main.py', 98)]) cmlenz@163: >>> catalog.add('head', u'Kopf', locations=[('util.py', 33)]) cmlenz@163: >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'), cmlenz@163: ... locations=[('util.py', 38)]) palgarvio@200: cmlenz@181: >>> catalog.update(template) cmlenz@163: >>> len(catalog) cmlenz@188: 3 palgarvio@200: cmlenz@188: >>> msg1 = catalog['green'] cmlenz@163: >>> msg1.string cmlenz@188: >>> msg1.locations cmlenz@188: [('main.py', 99)] palgarvio@200: cmlenz@188: >>> msg2 = catalog['blue'] cmlenz@188: >>> msg2.string cmlenz@163: u'blau' cmlenz@188: >>> msg2.locations cmlenz@163: [('main.py', 100)] palgarvio@200: cmlenz@188: >>> msg3 = catalog['salad'] cmlenz@188: >>> msg3.string cmlenz@163: (u'Salat', u'Salate') cmlenz@188: >>> msg3.locations cmlenz@163: [('util.py', 42)] palgarvio@200: cmlenz@181: Messages that are in the catalog but not in the template are removed cmlenz@181: from the main collection, but can still be accessed via the `obsolete` cmlenz@181: member: palgarvio@200: cmlenz@163: >>> 'head' in catalog cmlenz@163: False cmlenz@181: >>> catalog.obsolete.values() cmlenz@196: [] palgarvio@200: cmlenz@163: :param template: the reference catalog, usually read from a POT file palgarvio@200: :param no_fuzzy_matching: whether to use fuzzy matching of message IDs cmlenz@163: """ cmlenz@163: messages = self._messages cmlenz@163: self._messages = odict() cmlenz@163: cmlenz@278: def _merge(message, oldkey, newkey): cmlenz@278: fuzzy = False cmlenz@278: oldmsg = messages.pop(oldkey) cmlenz@278: if oldkey != newkey: cmlenz@278: fuzzy = True cmlenz@278: if isinstance(oldmsg.id, basestring): cmlenz@278: message.previous_id = [oldmsg.id] cmlenz@278: else: cmlenz@278: message.previous_id = list(oldmsg.id) cmlenz@278: message.string = oldmsg.string cmlenz@278: if isinstance(message.id, (list, tuple)): cmlenz@278: if not isinstance(message.string, (list, tuple)): cmlenz@278: fuzzy = True cmlenz@278: message.string = tuple( cmlenz@278: [message.string] + ([u''] * (len(message.id) - 1)) cmlenz@278: ) cmlenz@278: elif len(message.string) != len(message.id): cmlenz@278: fuzzy = True cmlenz@278: message.string = tuple(message.string[:len(oldmsg.string)]) cmlenz@278: elif isinstance(message.string, (list, tuple)): cmlenz@278: fuzzy = True cmlenz@278: message.string = message.string[0] cmlenz@278: message.flags |= oldmsg.flags cmlenz@278: if fuzzy: cmlenz@278: message.flags |= set([u'fuzzy']) cmlenz@278: self[message.id] = message cmlenz@278: cmlenz@163: for message in template: cmlenz@163: if message.id: cmlenz@163: key = self._key_for(message.id) cmlenz@163: if key in messages: cmlenz@278: _merge(message, key, key) cmlenz@163: else: palgarvio@200: if no_fuzzy_matching is False: cmlenz@165: # do some fuzzy matching with difflib cmlenz@165: matches = get_close_matches(key.lower().strip(), cmlenz@165: [self._key_for(msgid) for msgid in messages], 1) cmlenz@165: if matches: cmlenz@278: _merge(message, matches[0], key) cmlenz@188: continue cmlenz@163: cmlenz@165: self[message.id] = message cmlenz@165: cmlenz@181: self.obsolete = messages cmlenz@163: cmlenz@69: def _key_for(self, id): cmlenz@69: """The key for a message is just the singular ID even for pluralizable cmlenz@69: messages. cmlenz@69: """ cmlenz@69: key = id cmlenz@69: if isinstance(key, (list, tuple)): cmlenz@69: key = id[0] cmlenz@69: return key