summaryrefslogtreecommitdiffstats
path: root/ipalib
diff options
context:
space:
mode:
Diffstat (limited to 'ipalib')
-rw-r--r--ipalib/__init__.py2
-rw-r--r--ipalib/parameters.py6
-rw-r--r--ipalib/request.py5
-rw-r--r--ipalib/text.py447
4 files changed, 434 insertions, 26 deletions
diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index 51b63c9fc..6545bf718 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -881,7 +881,7 @@ from crud import Create, Retrieve, Update, Delete, Search
from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, Password,List
from parameters import BytesEnum, StrEnum, AccessTime, File
from errors import SkipPluginModule
-from text import _, gettext, ngettext
+from text import _, ngettext, GettextFactory, NGettextFactory
# We can't import the python uuid since it includes ctypes which makes
# httpd throw up when run in in mod_python due to SELinux issues
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index a598690ed..606a57483 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -73,14 +73,14 @@ web-UI. The *label* should start with an initial capital. For example:
... label=_('Last name'),
... )
>>> sn.label
-Gettext('Last name')
+Gettext('Last name', domain='ipa', localedir=None)
The *doc* is a longer description of the parameter. It's used on the CLI when
displaying the help information for a command, and as extra instruction for a
form input on the web-UI. By default the *doc* is the same as the *label*:
>>> sn.doc
-Gettext('Last name')
+Gettext('Last name', domain='ipa', localedir=None)
But you can override this with the *doc* kwarg. Like the *label*, the *doc*
should also start with an initial capital and should not end with any
@@ -92,7 +92,7 @@ punctuation. For example:
... doc=_("The user's last name"),
... )
>>> sn.doc
-Gettext("The user's last name")
+Gettext("The user's last name", domain='ipa', localedir=None)
Demonstration aside, you should always provide at least the *label* so the
various UIs are translatable. Only provide the *doc* if the parameter needs
diff --git a/ipalib/request.py b/ipalib/request.py
index f21ac03c7..86b643383 100644
--- a/ipalib/request.py
+++ b/ipalib/request.py
@@ -52,10 +52,11 @@ def destroy_context():
"""
Delete all attributes on thread-local `request.context`.
"""
- # need to use .items(), 'cos value.disconnect modifies the dict
- for (name, value) in context.__dict__.items():
+ # need to use .values(), 'cos value.disconnect modifies the dict
+ for value in context.__dict__.values():
if isinstance(value, Connection):
value.disconnect()
+ context.__dict__.clear()
def ugettext(message):
diff --git a/ipalib/text.py b/ipalib/text.py
index cabca438e..96770ad9d 100644
--- a/ipalib/text.py
+++ b/ipalib/text.py
@@ -18,73 +18,480 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
-Thread-local lazy gettext service.
+Defers gettext translation till request time.
-TODO: This aren't hooked up into gettext yet, they currently just provide
-placeholders for the rest of the code.
+IPA presents some tricky gettext challenges. On the one hand, most translatable
+message are defined as class attributes on the plugins, which means these get
+evaluated at module-load time. But on the other hand, each request to the
+server can be in a different locale, so the actual translation must not occur
+till request time.
+
+The `text` module provides a mechanism for for deferred gettext translation. It
+was designed to:
+
+ 1. Allow translatable strings to be marked with the usual ``_()`` and
+ ``ngettext()`` functions so that standard tools like xgettext can still
+ be used
+
+ 2. Allow programmers to mark strings in a natural way without burdening them
+ with details of the deferred translation mechanism
+
+A typical plugin will use the deferred translation like this:
+
+>>> from ipalib import Command, _, ngettext
+>>> class my_plugin(Command):
+... my_string = _('Hello, %(name)s.')
+... my_plural = ngettext('%(count)d goose', '%(count)d geese', 0)
+...
+
+With normal gettext usage, the *my_string* and *my_plural* message would be
+translated at module-load-time when your ``my_plugin`` class is defined. This
+would mean that all message are translated in the locale of the server rather
+than the locale of the request.
+
+However, the ``_()`` function above is actually a `GettextFactory` instance,
+which when called returns a `Gettext` instance. A `Gettext` instance stores the
+message to be translated, and the gettext domain and localedir, but it doesn't
+perform the translation till `Gettext.__unicode__()` is called. For example:
+
+>>> my_plugin.my_string
+Gettext('Hello, %(name)s.', domain='ipa', localedir=None)
+>>> unicode(my_plugin.my_string)
+u'Hello, %(name)s.'
+
+Translation can also be performed via the `Gettext.__mod__()` convenience
+method. For example, these two are equivalent:
+
+>>> my_plugin.my_string % dict(name='Joe')
+u'Hello, Joe.'
+>>> unicode(my_plugin.my_string) % dict(name='Joe') # Long form
+u'Hello, Joe.'
+
+Similar to ``_()``, the ``ngettext()`` function above is actually an
+`NGettextFactory` instance, which when called returns an `NGettext` instance.
+An `NGettext` instance stores the singular and plural messages, and the gettext
+domain and localedir, but it doesn't perform the translation till
+`NGettext.__call__()` is called. For example:
+
+>>> my_plugin.my_plural
+NGettext('%(count)d goose', '%(count)d geese', domain='ipa', localedir=None)
+>>> my_plugin.my_plural(1)
+u'%(count)d goose'
+>>> my_plugin.my_plural(2)
+u'%(count)d geese'
+
+Translation can also be performed via the `NGettext.__mod__()` convenience
+method. For example, these two are equivalent:
+
+>>> my_plugin.my_plural % dict(count=1)
+u'1 goose'
+>>> my_plugin.my_plural(1) % dict(count=1) # Long form
+u'1 goose'
+
+Lastly, 3rd-party plugins can create factories bound to a different gettext
+domain. The default domain is ``'ipa'``, which is also the domain of the
+standard ``ipalib._()`` and ``ipalib.ngettext()`` factories. But 3rd-party
+plugins can create their own factories like this:
+
+>>> from ipalib import GettextFactory, NGettextFactory
+>>> _ = GettextFactory(domain='ipa_foo')
+>>> ngettext = NGettextFactory(domain='ipa_foo')
+>>> class foo(Command):
+... msg1 = _('Foo!')
+... msg2 = ngettext('%(count)d bar', '%(count)d bars', 0)
+...
+
+Notice that these messages are bound to the ``'ipa_foo'`` domain:
+
+>>> foo.msg1
+Gettext('Foo!', domain='ipa_foo', localedir=None)
+>>> foo.msg2
+NGettext('%(count)d bar', '%(count)d bars', domain='ipa_foo', localedir=None)
+
+For additional details, see `GettextFactory` and `Gettext`, and for plural
+forms, see `NGettextFactory` and `NGettext`.
"""
+import threading
+import locale
+import gettext
+from request import context
+
+
+def create_translation(key):
+ assert key not in context.__dict__
+ (domain, localedir) = key
+ translation = gettext.translation(domain,
+ localedir=localedir,
+ languages=getattr(context, 'languages', None),
+ fallback=True,
+ )
+ context.__dict__[key] = translation
+ return translation
+
class LazyText(object):
+ """
+ Base class for deferred translation.
+
+ This class is not used directly. See the `Gettext` and `NGettext`
+ subclasses.
+ """
+
+ __slots__ = ('domain', 'localedir', 'key')
+
def __init__(self, domain=None, localedir=None):
+ """
+ Initialize.
+
+ :param domain: The gettext domain in which this message will be
+ translated, e.g. ``'ipa'`` or ``'ipa_3rd_party'``; default is
+ ``None``
+ :param localedir: The directory containing the gettext translations,
+ e.g. ``'/usr/share/locale/'``; default is ``None``, in which case
+ gettext will use the default system locale directory.
+ """
self.domain = domain
self.localedir = localedir
+ self.key = (domain, localedir)
- def __mod__(self, kw):
- return self.__unicode__() % kw
+ def __eq__(self, other):
+ """
+ Return ``True`` if this instances is equal to *other*.
+
+ Note that this method cannot be used on the `LazyText` base class itself
+ as subclasses must define an *args* instance attribute.
+ """
+ if type(other) is not self.__class__:
+ return False
+ return self.args == other.args
+
+ def __ne__(self, other):
+ """
+ Return ``True`` if this instances is not equal to *other*.
+
+ Note that this method cannot be used on the `LazyText` base class itself
+ as subclasses must define an *args* instance attribute.
+ """
+ return not self.__eq__(other)
class Gettext(LazyText):
+ """
+ Deferred translation using ``gettext.ugettext()``.
+
+ Normally the `Gettext` class isn't used directly and instead is created via
+ a `GettextFactory` instance. However, for illustration, we can create one
+ like this:
+
+ >>> msg = Gettext('Hello, %(name)s.')
+
+ When you create a `Gettext` instance, the message is stored on the *msg*
+ attribute:
+
+ >>> msg.msg
+ 'Hello, %(name)s.'
+
+ No translation is performed till `Gettext.__unicode__()` is called. This
+ will translate *msg* using ``gettext.ugettext()``, which will return the
+ translated string as a Python ``unicode`` instance. For example:
+
+ >>> unicode(msg)
+ u'Hello, %(name)s.'
+
+ `Gettext.__unicode__()` should be called at request time, which in a
+ nutshell means it should be called from within your plugin's
+ ``Command.execute()`` method. `Gettext.__unicode__()` will perform the
+ translation based on the locale of the current request.
+
+ `Gettext.__mod__()` is a convenience method for Python "percent" string
+ formatting. It will translate your message using `Gettext.__unicode__()`
+ and then perform the string substitution on the translated message. For
+ example, these two are equivalent:
+
+ >>> msg % dict(name='Joe')
+ u'Hello, Joe.'
+ >>> unicode(msg) % dict(name='Joe') # Long form
+ u'Hello, Joe.'
+
+ See `GettextFactory` for additional details. If you need to pick between
+ singular and plural form, use `NGettext` instances via the
+ `NGettextFactory`.
+ """
+
+ __slots__ = ('msg', 'args')
+
def __init__(self, msg, domain=None, localedir=None):
- self.msg = msg
super(Gettext, self).__init__(domain, localedir)
-
- def __unicode__(self):
- return self.msg.decode('utf-8')
+ self.msg = msg
+ self.args = (msg, domain, localedir)
def __repr__(self):
- return '%s(%r)' % (self.__class__.__name__, self.msg)
+ return '%s(%r, domain=%r, localedir=%r)' % (self.__class__.__name__,
+ self.msg, self.domain, self.localedir)
+
+ def __unicode__(self):
+ """
+ Translate this message and return as a ``unicode`` instance.
+ """
+ if self.key in context.__dict__:
+ g = context.__dict__[self.key].ugettext
+ else:
+ g = create_translation(self.key).ugettext
+ return g(self.msg)
def __json__(self):
return self.__unicode__()
+ def __mod__(self, kw):
+ return self.__unicode__() % kw
+
class FixMe(Gettext):
+ """
+ Non-translated place-holder for UI labels.
+
+ `FixMe` is a subclass of `Gettext` and is used for automatically created
+ place-holder labels. It generally behaves exactly like `Gettext` except no
+ translation is ever performed.
+
+ `FixMe` allows programmers to get plugins working without first filling in
+ all the labels that will ultimately be required, while at the same time it
+ creates conspicuous looking UI labels that remind the programmer to
+ "fix-me!". For example, the typical usage would be something like this:
+
+ >>> class Plugin(object):
+ ... label = None
+ ... def __init__(self):
+ ... self.name = self.__class__.__name__
+ ... if self.label is None:
+ ... self.label = FixMe(self.name + '.label')
+ ... assert isinstance(self.label, Gettext)
+ ...
+ >>> class user(Plugin):
+ ... pass # Oops, we didn't set user.label yet
+ ...
+ >>> u = user()
+ >>> u.label
+ FixMe('user.label')
+
+ Note that as `FixMe` is a subclass of `Gettext`, is passes the above type
+ check using ``isinstance()``.
+
+ Calling `FixMe.__unicode__()` performs no translation, but instead returns
+ said conspicuous looking label:
+
+ >>> unicode(u.label)
+ u'<user.label>'
+
+ For more examples of how `FixMe` is used, see `ipalib.parameters`.
+ """
+
+ __slots__ = tuple()
+
+ def __repr__(self):
+ return '%s(%r)' % (self.__class__.__name__, self.msg)
+
def __unicode__(self):
return u'<%s>' % self.msg
class NGettext(LazyText):
- def __init__(self, singular, plural, domain, localedir):
+ """
+ Deferred translation for plural forms using ``gettext.ungettext()``.
+
+ Normally the `NGettext` class isn't used directly and instead is created via
+ a `NGettextFactory` instance. However, for illustration, we can create one
+ like this:
+
+ >>> msg = NGettext('%(count)d goose', '%(count)d geese')
+
+ When you create an `NGettext` instance, the singular and plural forms of
+ your message are stored on the *singular* and *plural* instance attributes:
+
+ >>> msg.singular
+ '%(count)d goose'
+ >>> msg.plural
+ '%(count)d geese'
+
+ The translation and number selection isn't performed till
+ `NGettext.__call__()` is called. This will translate and pick the correct
+ number using ``gettext.ungettext()``. As a callable, an `NGettext` instance
+ takes a single argument, an integer specifying the count. For example:
+
+ >>> msg(0)
+ u'%(count)d geese'
+ >>> msg(1)
+ u'%(count)d goose'
+ >>> msg(2)
+ u'%(count)d geese'
+
+ `NGettext.__mod__()` is a convenience method for Python "percent" string
+ formatting. It can only be used if your substitution ``dict`` contains the
+ count in a ``'count'`` item. For example:
+
+ >>> msg % dict(count=0)
+ u'0 geese'
+ >>> msg % dict(count=1)
+ u'1 goose'
+ >>> msg % dict(count=2)
+ u'2 geese'
+
+ Alternatively, these longer forms have the same effect as the three examples
+ above:
+
+ >>> msg(0) % dict(count=0)
+ u'0 geese'
+ >>> msg(1) % dict(count=1)
+ u'1 goose'
+ >>> msg(2) % dict(count=2)
+ u'2 geese'
+
+ A ``KeyError`` is raised if your substitution ``dict`` doesn't have a
+ ``'count'`` item. For example:
+
+ >>> msg2 = NGettext('%(num)d goose', '%(num)d geese')
+ >>> msg2 % dict(num=0)
+ Traceback (most recent call last):
+ ...
+ KeyError: 'count'
+
+ However, in this case you can still use the longer, explicit form for string
+ substitution:
+
+ >>> msg2(0) % dict(num=0)
+ u'0 geese'
+
+ See `NGettextFactory` for additional details.
+ """
+
+ __slots__ = ('singular', 'plural', 'args')
+
+ def __init__(self, singular, plural, domain=None, localedir=None):
+ super(NGettext, self).__init__(domain, localedir)
self.singular = singular
self.plural = plural
- super(NGettext, self).__init__(domain, localedir)
+ self.args = (singular, plural, domain, localedir)
+
+ def __repr__(self):
+ return '%s(%r, %r, domain=%r, localedir=%r)' % (self.__class__.__name__,
+ self.singular, self.plural, self.domain, self.localedir)
def __mod__(self, kw):
count = kw['count']
return self(count) % kw
def __call__(self, count):
- if count == 1:
- return self.singular.decode('utf-8')
- return self.plural.decode('utf-8')
+ if self.key in context.__dict__:
+ ng = context.__dict__[self.key].ungettext
+ else:
+ ng = create_translation(self.key).ungettext
+ return ng(self.singular, self.plural, count)
+
+
+class GettextFactory(object):
+ """
+ Factory for creating ``_()`` functions.
+
+ A `GettextFactory` allows you to mark translatable messages that are
+ evaluated at initialization time, but deferred their actual translation till
+ request time.
+
+ When you create a `GettextFactory` you can provide a specific gettext
+ *domain* and *localedir*. By default the *domain* will be ``'ipa'`` and
+ the *localedir* will be ``None``. Both are available via instance
+ attributes of the same name. For example:
+
+ >>> _ = GettextFactory()
+ >>> _.domain
+ 'ipa'
+ >>> _.localedir is None
+ True
+
+ When the *localedir* is ``None``, gettext will use the default system
+ localedir (typically ``'/usr/share/locale/'``). In general, you should
+ **not** provide a *localedir*... it is intended only to support in-tree
+ testing.
+
+ Third party plugins will most likely want to use a different gettext
+ *domain*. For example:
+
+ >>> _ = GettextFactory(domain='ipa_3rd_party')
+ >>> _.domain
+ 'ipa_3rd_party'
+ When you call your `GettextFactory` instance, it will return a `Gettext`
+ instance associated with the same *domain* and *localedir*. For example:
+
+ >>> my_msg = _('Hello world')
+ >>> my_msg.domain
+ 'ipa_3rd_party'
+ >>> my_msg.localedir is None
+ True
+
+ The message isn't translated till `Gettext.__unicode__()` is called, which
+ should be done during each request. See the `Gettext` class for additional
+ details.
+ """
-class gettext_factory(object):
def __init__(self, domain='ipa', localedir=None):
+ """
+ Initialize.
+
+ :param domain: The gettext domain in which this message will be
+ translated, e.g. ``'ipa'`` or ``'ipa_3rd_party'``; default is
+ ``'ipa'``
+ :param localedir: The directory containing the gettext translations,
+ e.g. ``'/usr/share/locale/'``; default is ``None``, in which case
+ gettext will use the default system locale directory.
+ """
self.domain = domain
self.localedir = localedir
+ def __repr__(self):
+ return '%s(domain=%r, localedir=%r)' % (self.__class__.__name__,
+ self.domain, self.localedir)
+
def __call__(self, msg):
return Gettext(msg, self.domain, self.localedir)
-class ngettext_factory(gettext_factory):
+class NGettextFactory(GettextFactory):
+ """
+ Factory for creating ``ngettext()`` functions.
+
+ `NGettextFactory` is similar to `GettextFactory`, except `NGettextFactory`
+ is for plural forms.
+
+ So that standard tools like xgettext can find your plural forms, you should
+ reference your `NGettextFactory` instance using a variable named
+ *ngettext*. For example:
+
+ >>> ngettext = NGettextFactory()
+ >>> ngettext
+ NGettextFactory(domain='ipa', localedir=None)
+
+ When you call your `NGettextFactory` instance to create a deferred
+ translation, you provide the *singular* message, the *plural* message, and
+ a dummy *count*. An `NGettext` instance will be returned. For example:
+
+ >>> my_msg = ngettext('%(count)d goose', '%(count)d geese', 0)
+ >>> my_msg
+ NGettext('%(count)d goose', '%(count)d geese', domain='ipa', localedir=None)
+
+ The *count* is ignored (because the translation is deferred), but you should
+ still provide it so parsing tools aren't confused. For consistency, it is
+ recommended to always provide ``0`` for the *count*.
+
+ See `NGettext` for details on how the deferred translation is later
+ performed. See `GettextFactory` for details on setting a different gettext
+ *domain* (likely needed for 3rd-party plugins).
+ """
+
def __call__(self, singular, plural, count=0):
return NGettext(singular, plural, self.domain, self.localedir)
# Process wide factories:
-gettext = gettext_factory()
-_ = gettext
-ngettext = ngettext_factory()
+_ = GettextFactory()
+ngettext = NGettextFactory()