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@67: from datetime import datetime 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@67: from babel.messages.plurals import PLURALS cmlenz@67: from babel.util import odict, UTC cmlenz@56: cmlenz@56: __all__ = ['Message', 'Catalog'] cmlenz@56: __docformat__ = 'restructuredtext en' cmlenz@56: cmlenz@56: PYTHON_FORMAT = re.compile(r'\%(\([\w]+\))?[diouxXeEfFgGcrs]').search cmlenz@56: cmlenz@56: cmlenz@56: class Message(object): cmlenz@56: """Representation of a single message in a catalog.""" cmlenz@56: cmlenz@84: def __init__(self, id, string='', locations=(), flags=(), comments=()): cmlenz@56: """Create the message object. cmlenz@56: 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@84: :param comments: a sequence of translator comments for the message cmlenz@56: """ cmlenz@56: self.id = id cmlenz@68: if not string and self.pluralizable: cmlenz@68: string = (u'', u'') cmlenz@56: self.string = string cmlenz@70: self.locations = list(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@84: self.comments = list(comments) cmlenz@56: cmlenz@56: def __repr__(self): cmlenz@56: return '<%s %r>' % (type(self).__name__, self.id) cmlenz@56: 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. cmlenz@67: cmlenz@67: >>> Message('foo').fuzzy cmlenz@67: False cmlenz@67: >>> Message('foo', 'foo', flags=['fuzzy']).fuzzy cmlenz@67: True cmlenz@67: 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. cmlenz@56: cmlenz@56: >>> Message('foo').pluralizable cmlenz@56: False cmlenz@56: >>> Message(('foo', 'bar')).pluralizable cmlenz@56: True cmlenz@56: 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@56: return bool(filter(None, [PYTHON_FORMAT(id) for id in ids])) cmlenz@56: python_format = property(python_format, doc="""\ cmlenz@56: Whether the message contains Python-style parameters. cmlenz@56: 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 cmlenz@56: cmlenz@61: :type: `bool` cmlenz@56: """) cmlenz@56: cmlenz@56: cmlenz@56: class Catalog(object): palgarvio@78: """Representation of a message catalog.""" cmlenz@56: cmlenz@67: def __init__(self, locale=None, domain=None, project=None, version=None, palgarvio@78: msgid_bugs_address=None, creation_date=None, palgarvio@78: revision_date=None, last_translator=None, charset='utf-8'): cmlenz@64: """Initialize the catalog object. cmlenz@64: 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 palgarvio@78: :param project: the project's name palgarvio@78: :param version: the project's version palgarvio@78: :param msgid_bugs_address: the address to report bugs about the catalog 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@64: """ cmlenz@64: self.domain = domain #: the message domain cmlenz@64: if locale: cmlenz@64: locale = Locale.parse(locale) cmlenz@64: self.locale = locale #: the locale or `None` cmlenz@67: self._messages = odict() cmlenz@67: cmlenz@67: self.project = project or 'PROJECT' #: the project name cmlenz@84: self.version = version or 'VERSION' #: the project version palgarvio@78: self.msgid_bugs_address = msgid_bugs_address or 'EMAIL@ADDRESS' cmlenz@84: cmlenz@67: if creation_date is None: cmlenz@67: creation_date = time.localtime() cmlenz@67: elif isinstance(creation_date, datetime): cmlenz@67: if creation_date.tzinfo is None: cmlenz@67: creation_date = creation_date.replace(tzinfo=UTC) cmlenz@67: creation_date = creation_date.timetuple() cmlenz@67: self.creation_date = creation_date #: creation date of the template cmlenz@67: if revision_date is None: cmlenz@67: revision_date = time.localtime() cmlenz@67: elif isinstance(revision_date, datetime): cmlenz@67: if revision_date.tzinfo is None: cmlenz@67: revision_date = revision_date.replace(tzinfo=UTC) cmlenz@67: revision_date = revision_date.timetuple() cmlenz@67: self.revision_date = revision_date #: last revision date of the catalog cmlenz@67: self.last_translator = last_translator #: last translator name + email cmlenz@68: self.charset = charset or 'utf-8' cmlenz@67: cmlenz@67: def 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@67: time.strftime('%Y-%m-%d %H:%M%z', self.creation_date))) 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@67: time.strftime('%Y-%m-%d %H:%M%z', self.revision_date))) cmlenz@67: headers.append(('Last-Translator', self.last_translator)) cmlenz@67: headers.append(('Language-Team', '%s ' % 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')) cmlenz@67: headers.append(('Generated-By', 'Babel %s' % VERSION)) cmlenz@67: return headers cmlenz@67: headers = property(headers, doc="""\ cmlenz@67: The MIME headers of the catalog, used for the special ``msgid ""`` entry. cmlenz@67: 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. cmlenz@67: cmlenz@67: Here's an example of the output for such a catalog template: cmlenz@67: cmlenz@67: >>> catalog = Catalog(project='Foobar', version='1.0', cmlenz@67: ... creation_date=datetime(1990, 4, 1, 15, 30)) cmlenz@67: >>> for name, value in catalog.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 ... cmlenz@67: cmlenz@67: And here's an example of the output when the locale is set: cmlenz@67: cmlenz@67: >>> catalog = Catalog(locale='de_DE', project='Foobar', version='1.0', cmlenz@67: ... creation_date=datetime(1990, 4, 1, 15, 30), cmlenz@67: ... revision_date=datetime(1990, 8, 3, 12, 0), cmlenz@67: ... last_translator='John Doe ') cmlenz@67: >>> for name, value in catalog.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@67: 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 ... cmlenz@67: 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. 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. cmlenz@67: cmlenz@67: >>> Catalog(locale='en_US').plural_forms cmlenz@67: 'nplurals=2; plural=(n != 1)' cmlenz@67: >>> Catalog(locale='pt_BR').plural_forms cmlenz@67: 'nplurals=2; plural=(n > 1)' cmlenz@67: 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. cmlenz@84: 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. cmlenz@64: cmlenz@64: :rtype: ``iterator`` cmlenz@64: """ cmlenz@67: buf = [] cmlenz@67: for name, value in self.headers: cmlenz@67: buf.append('%s: %s' % (name, value)) cmlenz@67: yield Message('', '\n'.join(buf), flags=set(['fuzzy'])) 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. cmlenz@64: 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. cmlenz@64: cmlenz@64: >>> catalog = Catalog() cmlenz@64: >>> catalog[u'foo'] = Message(u'foo') cmlenz@64: >>> catalog[u'foo'] cmlenz@64: cmlenz@64: 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. cmlenz@64: 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)] cmlenz@64: 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@56: current.locations.extend(message.locations) cmlenz@56: current.flags |= message.flags cmlenz@56: message = current cmlenz@56: else: cmlenz@56: if isinstance(id, (list, tuple)): cmlenz@68: assert isinstance(message.string, (list, tuple)) cmlenz@69: self._messages[key] = message cmlenz@56: cmlenz@84: def add(self, id, string=None, locations=(), flags=(), comments=()): cmlenz@64: """Add or update the message with the specified ID. cmlenz@64: cmlenz@64: >>> catalog = Catalog() cmlenz@64: >>> catalog.add(u'foo') cmlenz@64: >>> catalog[u'foo'] cmlenz@64: cmlenz@64: cmlenz@64: This method simply constructs a `Message` object with the given cmlenz@64: arguments and invokes `__setitem__` with that object. cmlenz@64: 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@84: :param comments: a list of translator comments cmlenz@64: """ palgarvio@80: self[id] = Message(id, string, list(locations), flags, comments) cmlenz@69: 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