From 12cb6df7728c8321905a08b0864b3ff0386c62cf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 27 Apr 2010 16:44:52 +0000 Subject: Features -------- - A locale negotiator no longer needs to be registered explicitly. The default locale negotiator at ``repoze.bfg.i18n.default_locale_negotiator`` is now used unconditionally as... um, the default locale negotiator. - The default locale negotiator has become more complex. * First, the negotiator looks for the ``_LOCALE_`` attribute of the request object (possibly set by an :term:`event listener`). * Then it looks for the ``request.params['_LOCALE_']`` value. * Then it looks for the ``request.cookies['_LOCALE_']`` value. Backwards Incompatibilities --------------------------- - The default locale negotiator now looks for the parameter named ``_LOCALE_`` rather than a parameter named ``locale`` in ``request.params``. Behavior Changes ---------------- - A locale negotiator may now return ``None``, signifying that the default locale should be used. Documentation ------------- - Documentation concerning locale negotiation in the Internationalizationa and Localization chapter was updated. --- CHANGES.txt | 39 ++++++++ docs/narr/i18n.rst | 178 +++++++++++++++++++++------------ repoze/bfg/configuration.py | 10 +- repoze/bfg/i18n.py | 37 +++++-- repoze/bfg/tests/test_configuration.py | 2 + repoze/bfg/tests/test_i18n.py | 36 ++++--- 6 files changed, 210 insertions(+), 92 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index aeb018531..6a683f05d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,42 @@ +Next release +============ + +Features +-------- + +- A locale negotiator no longer needs to be registered explicitly. The + default locale negotiator at + ``repoze.bfg.i18n.default_locale_negotiator`` is now used + unconditionally as... um, the default locale negotiator. + +- The default locale negotiator has become more complex. + + * First, the negotiator looks for the ``_LOCALE_`` attribute of + the request object (possibly set by an :term:`event listener`). + + * Then it looks for the ``request.params['_LOCALE_']`` value. + + * Then it looks for the ``request.cookies['_LOCALE_']`` value. + +Backwards Incompatibilities +--------------------------- + +- The default locale negotiator now looks for the parameter named + ``_LOCALE_`` rather than a parameter named ``locale`` in + ``request.params``. + +Behavior Changes +---------------- + +- A locale negotiator may now return ``None``, signifying that the + default locale should be used. + +Documentation +------------- + +- Documentation concerning locale negotiation in the + Internationalizationa and Localization chapter was updated. + 1.3a1 (2010-04-26) ================== diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index d7fd86840..c5c77f971 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -479,10 +479,10 @@ You can obtain the locale name related to a request by using the locale_name = get_locale_name(request) This returns the locale name negotiated by the currently active -:term:`locale negotiator` or the default locale name (usually ``en``) -if no locale negotiator is configured. You can change the default -locale name by changing the ``default_locale_name`` setting; see -:ref:`default_locale_name_setting`. +:term:`locale negotiator` or the :term:`default locale name` (usually +``en``) if the locale negotiator returns ``None``. You can change the +default locale name by changing the ``default_locale_name`` setting; +see :ref:`default_locale_name_setting`. Once :func:`repoze.bfg.i18n.get_locale_name` has run once, the local name is stored on the request object it is passed. Subsequent calls @@ -617,8 +617,8 @@ Localization-Related Deployment Settings ---------------------------------------- A :mod:`repoze.bfg` application will have a ``default_locale_name`` -setting. This value represents the default locale name when no -:term:`locale negotiator` is registered. Pass it to the +setting. This value represents the :term:`default locale name` used +when the :term:`locale negotiator` returns ``None``. Pass it to the :mod:`repoze.bfg.configuration.Configurator` constructor at startup time: @@ -665,12 +665,14 @@ Activating Translation ---------------------- By default, a :mod:`repoze.bfg` application performs no translation. -To turn translation on, you must do both of these two things: +To turn translation on, you must: -- Add at least one :term:`translation directory` to your application. +- add at least one :term:`translation directory` to your application. -- Configure a :term:`locale negotiator` into your application's - configuration. +- ensure that your application sets the :term:`locale` correctly. + +Adding a Translation Directory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :term:`gettext` is the underlying machinery behind the :mod:`repoze.bfg` translation machinery. A translation directory is a @@ -681,23 +683,6 @@ which itself includes an ``LC_MESSAGES`` directory. Each Each ``.mo`` file represents a :term:`message catalog`, which is used to provide translations to your application. -A locale negotiator is a bit of code which accepts a request and which -returns a :term:`locale name`. It is consulted when -:meth:`repoze.bfg.i18n.Localizer.translate` or -:meth:`repoze.bfg.i18n.Localizer.pluralize` is invoked. It is also -consulted when :func:`repoze.bfg.i18n.get_locale_name` or -:func:`repoze.bfg.i18n.negotiate_locale_name` is invoked. - -At the time of this writing, only one (very weak) built-in locale -negotiator implementation named -:class:`repoze.bfg.i18n.default_locale_negotiator` ships as part of -the :mod:`repoze.bfg` software. This negotiator looks only at the the -``request.params['locale']`` value to determine the locale name. You -can provide your own locale negotiator function as required. - -Adding a Translation Directory -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Adding a :term:`translation directory` registers all of its constituent :term:`message catalog` files (all of the ``.mo`` files found within all ``LC_MESSAGES`` directories within each locale @@ -719,12 +704,10 @@ configuration using either imperative configuration or ZCML. :linenos: from repoze.bfg.configuration import Configurator - from repoze.bfg.i18n import default_locale_negotiator - config = Configurator( - locale_negotiator=default_locale_negotiator) config.begin() config.add_translation_dirs('my.application:locale/', 'another.application:locale/') + ... config.end() A message catalog in a translation directory added via @@ -749,16 +732,106 @@ configuration using either imperative configuration or ZCML. contain translations for the same locale and :term:`translation domain`. -.. _adding_a_locale_negotiator: +Setting the Locale +~~~~~~~~~~~~~~~~~~ -Adding a Locale Negotiator -~~~~~~~~~~~~~~~~~~~~~~~~~~ +When the *default locale negotiator* (see +:ref:`default_locale_negotiator`) is in use, you can inform +:mod:`repoze.bfg` of the current locale name by doing any of these +things before any translations need to be performed: + +- Set the ``_LOCALE_`` attribute of the request to a valid locale name + (usually directly within view code). E.g. ``request._LOCALE_ = + 'de'``. + +- Ensure that a valid locale name value is in the ``request.params`` + dictionary under the key named ``_LOCALE_``. This is usually the + result of passing a ``_LOCALE_`` value in the query string or in the + body of a form post associated with a request. For example, + visiting ``http://my.application?_LOCALE_=de``. + +- Ensure that a valid locale name value is in the ``request.cookies`` + dictionary under the key named ``_LOCALE_``. This is usually the + result of setting a ``_LOCALE_`` cookie in a prior response, + e.g. ``response.set_cookie('_LOCALE_', 'de')``. + +.. note:: + + If this locale negotiation scheme is inappropriate for a particular + application, you can configure a custom :term:`locale negotiator` + function into that application as required. See + :ref:`custom_locale_negotiator`. + +.. _locale_negotiators: + +Locale Negotiators +------------------ A :term:`locale negotiator` informs the operation of a :term:`localizer` by telling it what :term:`locale name` is related to -a particular request. See :ref:`creating_a_locale_negotiator`. +a particular request. A locale negotiator is a bit of code which +accepts a request and which returns a :term:`locale name`. It is +consulted when :meth:`repoze.bfg.i18n.Localizer.translate` or +:meth:`repoze.bfg.i18n.Localizer.pluralize` is invoked. It is also +consulted when :func:`repoze.bfg.i18n.get_locale_name` or +:func:`repoze.bfg.i18n.negotiate_locale_name` is invoked. + +.. _default_locale_negotiator: + +The Default Locale Negotiator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Most applications can make use of the default locale negotiator, which +requires no additional coding or configuration. + +The default locale negotiator implementation named +:class:`repoze.bfg.i18n.default_locale_negotiator` uses the following +set of steps to dermine the locale name. -You may add a locale directory to your application's +- First, the negotiator looks for the ``_LOCALE_`` attribute of + the request object (possibly set by an :term:`event listener`). + +- Then it looks for the ``request.params['_LOCALE_']`` value. + +- Then it looks for the ``request.cookies['_LOCALE_']`` value. + +- If no locale can be found via the request, it falls back to using + the :term:`default locale name` (see + :ref:`localization_deployment_settings`). + +- Finally, if the default locale name is not explicitly set, it uses + the locale name ``en``. + +.. _custom_locale_negotiator: + +Using a Custom Locale Negotiator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Locale negotiation is sometimes policy-laden and complex. If the +(simple) default locale negotiation scheme described in +:ref:`activating translation` is inappropriate for your application, +you may create and a special :term:`locale negotiator`. Subsequently +you may override the default locale negotiator by adding your newly +created locale negotiator to your application's configuration. + +A locale negotiator is simply a callable which +accepts a request and returns a single :term:`locale name` or ``None`` +if no locale can be determined. + +Here's an implementation of a simple locale negotiator: + +.. code-block:: python + :linenos: + + def my_locale_negotiator(request): + locale_name = request.params.get('my_locale') + return locale_name + +If a locale negotiator returns ``None``, it signifies to +:mod:`repoze.bfg` that the default application locale name should be +used. + +You may add your newly created locale negotiator to your application's configuration using either imperative configuration or ZCML. .. topic:: Using Imperative Configuration @@ -774,9 +847,7 @@ configuration using either imperative configuration or ZCML. :linenos: from repoze.bfg.configuration import Configurator - from repoze.bfg.i18n import default_locale_negotiator - config = Configurator( - locale_negotiator=default_locale_negotiator) + config = Configurator(locale_negotiator=my_locale_negotiator) Alternately, use the :meth:`repoze.bfg.configuration.Configurator.set_locale_negotiator` @@ -788,44 +859,19 @@ configuration using either imperative configuration or ZCML. :linenos: from repoze.bfg.configuration import Configurator - from repoze.bfg.i18n import default_locale_negotiator config = Configurator() config.begin() - config.set_locale_negotiator(default_locale_negotiator) + config.set_locale_negotiator(my_locale_negotiator) config.end() .. topic:: Using ZCML - You can add a translation directory via ZCML by using the + You can add a locale negotiator via ZCML by using the :ref:`localenegotiator_directive` ZCML directive: .. code-block:: xml :linenos: - -.. _creating_a_locale_negotiator: - -Creating a Locale Negotiator ----------------------------- - -A :term:`locale negotiator` is simply a callable which accepts a -request and returns a single :term:`locale name`. Here's an -implementation of a simple locale negotiator: - -.. code-block:: python - :linenos: - - def default_locale_negotiator(request): - locale_name = request.params.get('locale') - if locale_name is None: - settings = get_settings() or {} - locale_name = settings.get('default_locale_name', 'en') - return locale_name - -Locale negotiation can be complex. Your application may require a -policy-laden locale negotiator policy, so you can write your own and -supply it to an application configuration as per -:ref:`adding_a_locale_negotiator`. + negotiator="my_application.my_module.my_locale_negotiator"/> diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py index 9fa3fe8b2..546a6978b 100644 --- a/repoze/bfg/configuration.py +++ b/repoze/bfg/configuration.py @@ -145,6 +145,10 @@ class Configurator(object): class. The debug logger is used by :mod:`repoze.bfg` itself to log warnings and authorization debugging information. + If ``locale_negotiator`` is passed, it should be a + :term:`locale negotiator` implementation. See + :ref:`custom_locale_negotiator`. + """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -367,9 +371,9 @@ class Configurator(object): initialization. ``setup_registry`` configures settings, a root factory, - security policies, renderers, and a debug logger using the - configurator's current registry, as per the descriptions in - the Configurator constructor.""" + security policies, renderers, a debug logger, and a locale + negotiator using the configurator's current registry, as per + the descriptions in the Configurator constructor.""" self._fix_registry() self._set_settings(settings) self._set_root_factory(root_factory) diff --git a/repoze/bfg/i18n.py b/repoze/bfg/i18n.py index e3b276331..640ee030b 100644 --- a/repoze/bfg/i18n.py +++ b/repoze/bfg/i18n.py @@ -96,14 +96,27 @@ class Localizer(object): def default_locale_negotiator(request): - """ The default :term:`locale negotiator`. Returns a locale - name based on ``request.params.get('locale')`` or the - ``default_locale_name`` settings option; or ``en`` if all else - fails.""" - locale_name = request.params.get('locale') + """ The default :term:`locale negotiator`. Returns a locale name + or ``None``. + + - First, the negotiator looks for the ``_LOCALE_`` attribute of + the request object (possibly set by an :term:`event listener`). + + - Then it looks for the ``request.params['_LOCALE_']`` value. + + - Then it looks for the ``request.cookies['_LOCALE_']`` value. + + - Finally, the negotiator returns ``None`` if the locale could not + be determined via any of the previous checks (when a locale + negotiator returns ``None``, it signifies that the + :term:`default locale` should be used.) + """ + name = '_LOCALE_' + locale_name = getattr(request, name, None) if locale_name is None: - settings = get_settings() or {} - locale_name = settings.get('default_locale_name', 'en') + locale_name = request.params.get(name) + if locale_name is None: + locale_name = request.cookies.get(name) return locale_name def negotiate_locale_name(request): @@ -113,12 +126,14 @@ def negotiate_locale_name(request): registry = request.registry except AttributeError: registry = get_current_registry() - negotiator = registry.queryUtility(ILocaleNegotiator) - if negotiator is None: + negotiator = registry.queryUtility(ILocaleNegotiator, + default=default_locale_negotiator) + locale_name = negotiator(request) + + if locale_name is None: settings = get_settings() or {} locale_name = settings.get('default_locale_name', 'en') - else: - locale_name = negotiator(request) + return locale_name def get_locale_name(request): diff --git a/repoze/bfg/tests/test_configuration.py b/repoze/bfg/tests/test_configuration.py index 0a443720f..16b590cdd 100644 --- a/repoze/bfg/tests/test_configuration.py +++ b/repoze/bfg/tests/test_configuration.py @@ -3518,6 +3518,8 @@ class DummyRequest: subpath = () def __init__(self): self.environ = {'PATH_INFO':'/static'} + self.params = {} + self.cookies = {} def copy(self): return self def get_response(self, app): diff --git a/repoze/bfg/tests/test_i18n.py b/repoze/bfg/tests/test_i18n.py index fb79a802c..95017807f 100644 --- a/repoze/bfg/tests/test_i18n.py +++ b/repoze/bfg/tests/test_i18n.py @@ -94,6 +94,15 @@ class Test_negotiate_locale_name(unittest.TestCase): result = self._callFUT(request) self.assertEqual(result, 'settings') + def test_use_default_locale_negotiator(self): + from repoze.bfg.threadlocal import get_current_registry + registry = get_current_registry() + request = DummyRequest() + request.registry = registry + request._LOCALE_ = 'locale' + result = self._callFUT(request) + self.assertEqual(result, 'locale') + def test_default_default(self): request = DummyRequest() result = self._callFUT(request) @@ -219,24 +228,26 @@ class Test_default_locale_negotiator(unittest.TestCase): from repoze.bfg.i18n import default_locale_negotiator return default_locale_negotiator(request) - def test_from_settings(self): - from repoze.bfg.interfaces import ISettings - from repoze.bfg.threadlocal import get_current_registry - settings = {'default_locale_name':'dude'} - registry = get_current_registry() - registry.registerUtility(settings, ISettings) + def test_from_none(self): request = DummyRequest() result = self._callFUT(request) - self.assertEqual(result, 'dude') - - def test_settings_empty(self): + self.assertEqual(result, None) + + def test_from_request_attr(self): request = DummyRequest() + request._LOCALE_ = 'foo' result = self._callFUT(request) - self.assertEqual(result, 'en') - + self.assertEqual(result, 'foo') + def test_from_params(self): request = DummyRequest() - request.params['locale'] = 'foo' + request.params['_LOCALE_'] = 'foo' + result = self._callFUT(request) + self.assertEqual(result, 'foo') + + def test_from_cookies(self): + request = DummyRequest() + request.cookies['_LOCALE_'] = 'foo' result = self._callFUT(request) self.assertEqual(result, 'foo') @@ -370,6 +381,7 @@ class TestTranslations(unittest.TestCase): class DummyRequest(object): def __init__(self): self.params = {} + self.cookies = {} def dummy_negotiator(request): return 'bogus' -- cgit v1.2.3