summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2010-04-25 00:02:12 +0000
committerChris McDonough <chrism@agendaless.com>2010-04-25 00:02:12 +0000
commit7534bae0c5eeb34a0146e76a81a9312797f6ba5c (patch)
tree15d3fd388eb3c5538c50da20c3691a9f39ca6de3
parent51981a128c7ed05e51938a3f358c0970dcc33a6f (diff)
downloadpyramid-7534bae0c5eeb34a0146e76a81a9312797f6ba5c.tar.gz
pyramid-7534bae0c5eeb34a0146e76a81a9312797f6ba5c.tar.bz2
pyramid-7534bae0c5eeb34a0146e76a81a9312797f6ba5c.zip
Merge i18n branch via svn merge --ignore-ancestry -r9030:9150 $REPOZE_SVN/repoze.bfg/branches/i18n
No foreigners were harmed in the coding of this feature.
-rw-r--r--CHANGES.txt53
-rw-r--r--COPYRIGHT.txt2
-rw-r--r--LICENSE.txt32
-rw-r--r--TODO.txt2
-rw-r--r--docs/api.rst1
-rw-r--r--docs/api/configuration.rst4
-rw-r--r--docs/api/i18n.rst31
-rw-r--r--docs/glossary.rst73
-rw-r--r--docs/index.rst1
-rw-r--r--docs/latexindex.rst1
-rw-r--r--docs/narr/MyProject/MyProject.ini1
-rw-r--r--docs/narr/MyProject/setup.cfg27
-rw-r--r--docs/narr/environment.rst17
-rw-r--r--docs/narr/i18n.rst777
-rw-r--r--docs/narr/project.rst19
-rw-r--r--docs/narr/views.rst5
-rw-r--r--docs/zcml.rst2
-rw-r--r--docs/zcml/localenegotiator.rst41
-rw-r--r--docs/zcml/translationdir.rst66
-rw-r--r--repoze/bfg/chameleon_text.py8
-rw-r--r--repoze/bfg/chameleon_zpt.py8
-rw-r--r--repoze/bfg/configuration.py93
-rw-r--r--repoze/bfg/i18n.py306
-rw-r--r--repoze/bfg/includes/meta.zcml12
-rw-r--r--repoze/bfg/interfaces.py17
-rw-r--r--repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl1
-rw-r--r--repoze/bfg/paster_templates/alchemy/setup.cfg_tmpl27
-rw-r--r--repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl1
-rw-r--r--repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl21
-rw-r--r--repoze/bfg/paster_templates/starter/+project+.ini_tmpl1
-rw-r--r--repoze/bfg/paster_templates/starter/setup.cfg_tmpl27
-rw-r--r--repoze/bfg/paster_templates/zodb/+project+.ini_tmpl1
-rw-r--r--repoze/bfg/paster_templates/zodb/setup.cfg_tmpl27
-rw-r--r--repoze/bfg/settings.py4
-rw-r--r--repoze/bfg/tests/fixtures/__init__.py1
-rw-r--r--repoze/bfg/tests/fixtures/locale/GARBAGE1
-rw-r--r--repoze/bfg/tests/fixtures/locale/be/LC_MESSAGES1
-rw-r--r--repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.mobin0 -> 543 bytes
-rw-r--r--repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.po31
-rw-r--r--repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.mobin0 -> 543 bytes
-rw-r--r--repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.po31
-rw-r--r--repoze/bfg/tests/test_chameleon_text.py20
-rw-r--r--repoze/bfg/tests/test_chameleon_zpt.py20
-rw-r--r--repoze/bfg/tests/test_configuration.py57
-rw-r--r--repoze/bfg/tests/test_i18n.py382
-rw-r--r--repoze/bfg/tests/test_zcml.py50
-rw-r--r--repoze/bfg/zcml.py35
-rw-r--r--setup.py3
48 files changed, 2328 insertions, 13 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index fdc67b191..1d38ff462 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -22,7 +22,7 @@ Features
returned.
Other normal view predicates can also be used in combination with an
- exception view registration:
+ exception view registration::
from repoze.bfg.view import bfg_view
from repoze.bfg.exceptions import NotFound
@@ -58,6 +58,22 @@ Features
scanner. (Truth be told, Venusian is really just a generalization
of the BFG-internal decorator scanner).
+- Internationalization and localization features as documented in the
+ narrative documentation chapter entitled ``Internationalization and
+ Localization``.
+
+- A new setting named ``default_locale_name`` was added. If this
+ string is present as a Paster ``.ini`` file option, it will be
+ considered the default locale name. The default locale name is used
+ during locale-related operations such as language translation.
+
+ZCML
+----
+
+- Add a ``translationdir`` ZCML directive to support localization.
+
+- Add a ``localenegotiator`` ZCML directive to support localization.
+
Deprecations
------------
@@ -74,6 +90,12 @@ Dependencies
- A new install-time dependency on the ``venusian`` distribution was
added.
+- A new install-time dependency on the ``translationstring``
+ distribution was added.
+
+- Chameleon 1.2.3 or better is now required (internationalization and
+ per-template debug settings).
+
Internal
--------
@@ -96,3 +118,32 @@ Documentation
- Exception view documentation was added to the ``Hooks`` narrative
chapter.
+
+- A new narrative chapter entitled ``Internationalization and
+ Localization`` was added.
+
+- The "Environment Variables and ``ini`` File Settings" chapter was
+ changed: documentation about the ``default_locale_name`` setting was
+ added.
+
+- A new API chapter for the ``repoze.bfg.i18n`` module was added.
+
+- Documentation for the new ``translationdir`` and
+ ``locale_negotiator`` ZCML directives were added.
+
+Paster Templates
+----------------
+
+- All paster templates now create a ``setup.cfg`` which includes
+ commands related to nose testing and Babel message catalog
+ extraction/compilation.
+
+- A ``default_locale_name = en`` setting was added to each existing paster
+ template.
+
+Licensing
+---------
+
+- The Edgewall (BSD) license was added to the LICENSES.txt file, as
+ some code in the ``repoze.bfg.i18n`` derives from Babel source.
+
diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt
index 7b03cc535..aa870dc84 100644
--- a/COPYRIGHT.txt
+++ b/COPYRIGHT.txt
@@ -4,3 +4,5 @@ Copyright (c) 2008-2010 Agendaless Consulting and Contributors.
Portions (c) Django Project (http://djangoproject.com/).
Portions (c) Zope Foundation and contributors (http://www.zope.org/).
+
+Portions (c) Edgewall Software (http://edgewall.org)
diff --git a/LICENSE.txt b/LICENSE.txt
index 0f9145223..c2c18cdb5 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -156,3 +156,35 @@ The documentation portion of repoze.bfg (the rendered contents of the
under the Creative Commons Attribution-Noncommercial-Share Alike 3.0
United States License as described by
http://creativecommons.org/licenses/by-nc-sa/3.0/us/
+
+Internationalization Code in ``repoze.bfg.i18n`` is supplied under the
+following license:
+
+ Copyright (C) 2007 Edgewall Software
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ 3. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior
+ written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+ GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/TODO.txt b/TODO.txt
index b16d806b2..0d19e8273 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -17,3 +17,5 @@
- Provide a webob.Response class facade for forward compat.
+- Replace default_notfound_view and default_forbidden_view with better
+ exception view candidates.
diff --git a/docs/api.rst b/docs/api.rst
index 2bd5fca01..a97c79fa9 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -15,6 +15,7 @@ documentation is organized alphabetically by module name.
api/configuration
api/events
api/exceptions
+ api/i18n
api/interfaces
api/location
api/paster
diff --git a/docs/api/configuration.rst b/docs/api/configuration.rst
index e872a91b9..7b411b92d 100644
--- a/docs/api/configuration.rst
+++ b/docs/api/configuration.rst
@@ -32,6 +32,8 @@
.. automethod:: add_subscriber
+ .. automethod:: add_translation_dirs
+
.. automethod:: add_view
.. automethod:: load_zcml(spec)
@@ -46,6 +48,8 @@
.. automethod:: set_notfound_view(view=None, attr=None, renderer=None, wrapper=None)
+ .. automethod:: set_locale_negotiator
+
.. automethod:: testing_securitypolicy
.. automethod:: testing_models
diff --git a/docs/api/i18n.rst b/docs/api/i18n.rst
new file mode 100644
index 000000000..6562bb13b
--- /dev/null
+++ b/docs/api/i18n.rst
@@ -0,0 +1,31 @@
+.. _i18n_module:
+
+:mod:`repoze.bfg.i18n`
+----------------------
+
+.. automodule:: repoze.bfg.i18n
+
+ .. autoclass:: TranslationString
+
+ .. autoclass:: TranslationStringFactory
+
+ .. autoclass:: Localizer
+ :members:
+
+ .. attribute:: locale_name
+
+ The locale name for this localizer (e.g. ``en`` or ``en_US``).
+
+ .. autofunction:: get_localizer
+
+ .. autofunction:: negotiate_locale_name
+
+ .. autofunction:: get_locale_name
+
+ .. autofunction:: default_locale_negotiator
+
+See :ref:`i18n_chapter` for more information about using
+:mod:`repoze.bfg` internationalization and localization services
+within an application.
+
+
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 14bd4fc44..026db614a 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -667,3 +667,76 @@ Glossary
at import time, the action usually taken by the decorator is
deferred until a separate "scan" phase. :mod:`repoze.bfg` relies
on Venusian to provide a basis for its :term:`scan` feature.
+
+ Translation String
+ An instance of :class:`repoze.bfg.i18n.TranslationString`, which
+ is a class that behaves like a Unicode string, but has several
+ extra attributes such as ``domain``, ``msgid``, and ``mapping``
+ for use during translation. Translation strings are usually
+ created by hand within software, but are sometimes created on the
+ behalf of the system for automatic template translation. For
+ more information, see :ref:`i18n_chapter`.
+
+ Translation Domain
+ A string representing the "context" in which a translation was
+ made. For example the word "java" might be translated
+ differently if the translation domain is "programming-languages"
+ than would be if the translation domain was "coffee". A
+ translation domain is represnted by a collection of ``.mo`` files
+ within one or more :term:`translation directory` directories.
+
+ Translator
+ A callable which receives a :term:`translation string` and
+ returns a translated Unicode object for the purposes of
+ internationalization. A :term:`localizer` supplies a
+ translator to a :mod:`repoze.bfg` application accessible via its
+ ``translate`` method.
+
+ Translation Directory
+ A translation directory is a :term:`gettext` translation
+ directory. It contains language folders, which themselves
+ contain ``LC_MESSAGES`` folders, which contain ``.mo`` files.
+ Each ``.mo`` file represents a set of translations for a language
+ in a :term:`translation domain`. The name of the ``.mo`` file
+ (minus the .mo extension) is the translation domain name.
+
+ Localizer
+ An instance of the class :class:`repoze.bfg.i18n.Localizer` which
+ provides translation and pluralization services to an
+ application. It is retrieved via the
+ :func:`repoze.bfg.i18n.get_localizer` function.
+
+ Locale Name
+ A string like ``en``, ``en_US``, ``de``, or ``de_AT`` which
+ uniquely identifies a particular locale.
+
+ Locale Negotiator
+ An object supplying a policy determining which :term:`locale
+ name` best represents a given :term:`request`. It is used by the
+ :func:`repoze.bfg.i18n.get_locale_name`, and
+ :func:`repoze.bfg.i18n.negotiate_locale_name` functions, and
+ indirectly by :func:`repoze.bfg.i18n.get_localizer`. The
+ :func:`repoze.bfg.i18n.default_locale_negotiator` function
+ is an example of a locale negotiator.
+
+ Gettext
+ The GNU `gettext <http://www.gnu.org/software/gettext/>`_
+ library, used by the :mod:`repoze.bfg` translation machinery.
+
+ Babel
+ A `collection of tools <http://babel.edgewall.org/>`_ for
+ internationalizing Python applications. :mod:`repoze.bfg` does
+ not depend on Babel to operate, but if Babel is installed,
+ additional locale functionality becomes available to your
+ application.
+
+ Message Identifier
+ A string used as a translation lookup key during localization.
+ The ``msgid`` argument to a :term:`translation string` is a
+ message identifier. Message identifiers are also present in a
+ :term:`message catalog`.
+
+ Message Catalog
+ A :term:`gettext` ``.mo`` file containing translations.
+
+
diff --git a/docs/index.rst b/docs/index.rst
index ee32cf691..c1b185352 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -53,6 +53,7 @@ Narrative documentation in chapter form explaining how to use
narr/templates
narr/models
narr/security
+ narr/i18n
narr/vhosting
narr/events
narr/environment
diff --git a/docs/latexindex.rst b/docs/latexindex.rst
index 45438e939..5850ee06c 100644
--- a/docs/latexindex.rst
+++ b/docs/latexindex.rst
@@ -42,6 +42,7 @@ Narrative Documentation
narr/templates
narr/models
narr/security
+ narr/i18n
narr/vhosting
narr/events
narr/environment
diff --git a/docs/narr/MyProject/MyProject.ini b/docs/narr/MyProject/MyProject.ini
index f5d094dff..38cbf879c 100644
--- a/docs/narr/MyProject/MyProject.ini
+++ b/docs/narr/MyProject/MyProject.ini
@@ -6,6 +6,7 @@ use = egg:MyProject#app
reload_templates = true
debug_authorization = false
debug_notfound = false
+default_locale_name = en
[server:main]
use = egg:Paste#http
diff --git a/docs/narr/MyProject/setup.cfg b/docs/narr/MyProject/setup.cfg
new file mode 100644
index 000000000..de38b5616
--- /dev/null
+++ b/docs/narr/MyProject/setup.cfg
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package=myproject
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = myproject/locale
+domain = MyProject
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = myproject/locale/MyProject.pot
+width = 80
+
+[init_catalog]
+domain = MyProject
+input_file = myproject/locale/MyProject.pot
+output_dir = myproject/locale
+
+[update_catalog]
+domain = MyProject
+input_file = myproject/locale/MyProject.pot
+output_dir = myproject/locale
+previous = true
diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst
index 5af24bfac..a9bcbf70a 100644
--- a/docs/narr/environment.rst
+++ b/docs/narr/environment.rst
@@ -9,6 +9,7 @@
single: reload_all
single: debug settings
single: reload settings
+ single: default_locale_name
single: environment variables
single: ini file settings
single: PasteDeploy settings
@@ -121,6 +122,22 @@ Turns on all ``reload*`` settings.
| | |
+---------------------------------+-----------------------------+
+Default Locale Name
+--------------------
+
+The value supplied here is used as the default locale name when a
+:term:`locale negotiator` is not registered. See also
+:ref:`localization_deployment_settings`.
+
++---------------------------------+-----------------------------+
+| Environment Variable Name | Config File Setting Name |
++=================================+=============================+
+| ``BFG_DEFAULT_LOCALE_NAME`` | ``default_locale_name`` |
+| | |
+| | |
+| | |
++---------------------------------+-----------------------------+
+
Examples
--------
diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst
new file mode 100644
index 000000000..7ec6b2607
--- /dev/null
+++ b/docs/narr/i18n.rst
@@ -0,0 +1,777 @@
+.. index::
+ single: i18n
+ single: l10n
+ single: internationalization
+ single: localization
+
+.. _i18n_chapter:
+
+Internationalization and Localization
+=====================================
+
+:mod:`repoze.bfg` offers internationalization (i18n) and localization
+(l10n) subsystems that can be used to translate the text of buttons,
+the text of error messages and other software- and template-defined
+values into the native language of a user of your application.
+
+.. note: The APIs and functionality described in this chapter are new
+ as of :mod:`repoze.bfg` version 1.3.
+
+.. index::
+ single: translation string
+ pair: domain; translation
+ pair: msgid; translation
+ single: message identifier
+
+Creating a Translation String
+-----------------------------
+
+While you write your software, you can insert specialized markup into
+your Python code that makes it possible for the system to translate
+text values into the languages used by your application's users. This
+markup generates a :term:`translation string`. A translation string
+is an object that behave mostly like a normal Unicode object, except
+that it also carries around extra information related to its job as
+part of the :mod:`repoze.bfg` translation machinery.
+
+Using The ``TranslationString`` Class
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The most primitive way to create a translation string is to use the
+:class:`repoze.bfg.i18n.TranslationString` callable:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import TranslationString
+ ts = TranslationString('Add')
+
+This creates a Unicode-like object that is a TranslationString.
+
+.. note::
+
+ For people more familiar with :term:`Zope` i8n, a TranslationString
+ is a lot like a ``zope.i18nmessageid.Message`` object. It is not a
+ subclass, however. For people more familiar with :term:`Pylons` or
+ :term:`Django` i8n, using a TranslationString is a lot like using
+ "lazy" versions of related gettext APIs.
+
+The first argument to :class:`repoze.bfg.i18n.TranslationString` is
+the ``msgid``; it is required. It represents the key into the
+translation mappings provided by a particular localization. The
+``msgid`` argument must be a Unicode object or an ASCII string. The
+msgid may optionally contain *replacement markers*. For instance:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import TranslationString
+ ts = TranslationString('Add ${number}')
+
+Within the string above, ``${stuff}`` is a replacement marker. It
+will be replaced by whatever is in the *mapping* for a translation
+string. The mapping may be supplied at the same time as the
+replacement marker itself:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import TranslationString
+ ts = TranslationString('Add ${number}', mapping={'number':1})
+
+Any number of replacement markers can be present in the msgid value,
+any number of times. Only markers which can be replaced by the values
+in the *mapping* will be replaced at translation time. The others
+will not be interpolated and will be output literally.
+
+A translation string should also usually carry a *domain*. The domain
+represents a translation category to disambiguate it from other
+translations of the same msgid, in case they conflict.
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import TranslationString
+ ts = TranslationString('Add ${number}', mapping={'number':1},
+ domain='form')
+
+The above translation string named a domain of ``form``. A
+:term:`translator` function will often use the domain to locate the
+right translator file on the filesystem which contains translations
+for a given domain. In this case, if it were trying to translate to
+our msgid to German, it might try to find a translation from a
+:term:`gettext` file within a :term:`translation directory` like this
+one::
+
+ locale/de/LC_MESSAGES/form.mo
+
+In other words, it would want to take translations from the ``form.mo``
+translation file in the German language.
+
+Finally, the TranslationString constructor accepts a ``default``
+argument. If a ``default`` argument is supplied, it replaces usages
+of the ``msgid`` as the *default value* for the translation string.
+When ``default`` is ``None``, the ``msgid`` value passed to a
+TranslationString is used as an implicit message identifier. Message
+identifiers are matched with translations in translation files, so it
+is often useful to create translation strings with "opaque" message
+identifiers unrelated to their default text:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import TranslationString
+ ts = TranslationString('add-number', default='Add ${number}',
+ domain='form', mapping={'number':1})
+
+When default text is used, Default text objects may contain
+replacement values.
+
+.. index::
+ single: translation string factory
+
+Using the ``TranslationStringFactory`` Class
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Another way to generate a translation string is to use the
+:attr:`repoze.bfg.i18n.TranslationStringFactory` object. This object
+is a *translation string factory*. Basically a translation string
+factory presets the ``domain`` value of any :term:`translation string`
+generated by using it. For example:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import TranslationStringFactory
+ _ = TranslationStringFactory('bfg')
+ ts = _('Add ${number}', msgid='add-number', mapping={'number':1})
+
+.. note:: We assigned the translation string factory to the name
+ ``_``. This is a convention which will be supported by translation
+ file generation tools.
+
+After assigning ``_`` to the result of a
+:func:`repoze.bfg.i18n.TranslationStringFactory`, the subsequent
+result of calling ``_`` will be a
+:class:`repoze.bfg.i18n.TranslationString` instance. Even though a
+``domain`` value was not passed to ``_` (as would have been necessary
+if the :class:`repoze.bfg.i18n.TranslationString` constructor were
+used instead of a translation string factory), the ``domain``
+attribute of the resulting translation string will be ``bfg``. As a
+result, the previous code example is completely equivalent (except for
+spelling) to:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import TranslationString as _
+ ts = _('Add ${number}', msgid='add-number', mapping={'number':1},
+ domain='bfg')
+
+You can set up your own translation string factory much like the one
+provided above by using the
+:class:`repoze.bfg.i18n.TranslationStringFactory` class. For example,
+if you'd like to create a translation string factory which presets the
+``domain`` value of generated translation strings to ``form``, you'd
+do something like this:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import TranslationStringFactory
+ _ = TranslationStringFactory('form')
+ ts = _('Add ${number}', msgid='add-number', mapping={'number':1})
+
+Creating a unique domain for your application via a translation string
+factory is best practice. Using your own unique translation domain
+allows another person to reuse your application without needing to
+merge your translation files with his own. Instead, he can just
+include your package's :term:`translation directory` via the
+:meth:`repoze.bfg.configuration.Configurator.add_translation_dirs`
+method.
+
+.. note::
+
+ For people familiar with Zope internationalization, a
+ TranslationStringFactory is a lot like a
+ ``zope.i18nmessageid.MessageFactory`` object. It is not a
+ subclass, however.
+
+.. index::
+ single: gettext
+ single: translation directories
+
+Working With ``gettext`` Translation Files
+------------------------------------------
+
+Once your application source code files and templates are marked up
+with translation markers, you can work on translations.
+
+.. note::
+
+ The steps used to work with gettext translation files in
+ :mod:`repoze.bfg` are very similar to the steps supported by
+ `Pylons internationalization
+ <http://wiki.pylonshq.com/display/pylonsdocs/Internationalization+and+Localization>`_.
+
+.. index::
+ single: Babel
+
+.. _installing_babel:
+
+Installing Babel
+~~~~~~~~~~~~~~~~
+
+In order for the commands related to working with ``gettext``
+translation files to work properly, you will need to have
+:term:`Babel` installed into the same environment in which
+:mod:`repoze.bfg` is installed.
+
+Installation on UNIX
+++++++++++++++++++++
+
+If the :term:`virtualenv` into which you've installed your
+:mod:`repoze.bfg` application lives in ``/my/virtualenv``, you can
+install Babel like so:
+
+.. code-block:: bash
+
+ $ cd /my/virtualenv
+ $ bin/easy_install Babel
+
+Installation on Windows
++++++++++++++++++++++++
+
+If the :term:`virtualenv` into which you've installed your
+:mod:`repoze.bfg` application lives in ``C:\my\virtualenv``, you can
+install Babel like so:
+
+.. code-block:: bash
+
+ C> cd \my\virtualenv
+ C> bin\easy_install Babel
+
+.. index::
+ pair: extracting; messages
+
+Extracting Messages from Code and Templates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Once :term:`Babel` is installed, you may extract a message catalog
+template from the code and :term:`Chameleon` templates which reside in
+your application::
+
+ $ cd /place/where/myapplication/setup.py/lives
+ $ mkdir -p myapplication/locale
+ $ python setup.py extract_messages
+
+The message catalog template will end up in
+``myapplication/locale/myapplication.pot``.
+
+XXX finish, including hair about Chameleon template scraping
+
+.. index::
+ pair: initalizing; message catalog
+
+Initializaing a Message Catalog File
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Initialize a message catalog for a specific locale from a pregenerated
+``.pot`` template::
+
+ $ cd /place/where/myapplication/setup.py/lives
+ $ python setup.py init_catalog -l es
+
+The message catalog ``.po`` file will end up in
+``myapplication/locale/es/LC_MESSAGES/myapplication.po``.
+
+XXX finish
+
+.. index::
+ pair: updating; message catalog
+
+Updating a Catalog File
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Update ``.po`` files based on changes to the ``.pot`` file::
+
+ $ cd /place/where/myapplication/setup.py/lives
+ $ python setup.py update_catalog
+
+XXX finish
+
+.. index::
+ pair: compiling; message catalog
+
+Compiling a Message Catalog File
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Compile ``.po`` files to ``.mo`` files::
+
+ $ cd /place/where/myapplication/setup.py/lives
+ $ python setup.py compile_catalog
+
+XXX finish
+
+.. index::
+ single: localizer
+ single: get_localizer
+
+Using a Localizer
+-----------------
+
+A :term:`localizer` is an object that allows you to perform
+translation or pluralization "by hand" in an application. You may use
+the :func:`repoze.bfg.i18n.get_localizer` function to obtain a
+:term:`localizer`. :func:`repoze.bfg.i18n.get_localizer`. This
+function will return either the localizer object implied by the active
+:term:`locale negotiator` or a default localizer object if no explicit
+locale negotiator is registered.
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import get_localizer
+
+ def aview(request):
+ locale = get_localizer(request)
+
+.. index::
+ single: translating (i18n)
+
+.. _performing_a_translation:
+
+Performing a Translation
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+A :term:`localizer` has a ``translate`` method which accepts either a
+:term:`translation string` or a Unicode string and which returns a
+Unicode object representing the translation. So, generating a
+translation in a view component of an application might look like so:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import get_localizer
+ from repoze.bfg.i18n import TranslationString
+
+ ts = TranslationString('Add ${number}', mapping={'number':1}, domain='bfg')
+
+ def aview(request):
+ localizer = get_localizer(request)
+ translated = localizer.translate(ts) # translation string
+ # ... use translated ...
+
+The :func:`repoze.bfg.i18n.get_localizer` function will return a
+:class:`repoze.bfg.i18n.Localizer` object bound to the locale name
+represented by the request. The translation returned from its
+:meth:`repoze.bfg.i18n.Localizer.translate` method will depend on the
+``domain`` attribute of the provided translation string as well as the
+locale of the localizer.
+
+.. note:: If you're using :term:`Chameleon` templates, you don't need
+ to pre-translate translation strings this way. See
+ :ref:`chameleon_translation_strings`.
+
+.. index::
+ single: pluralizing (i18n)
+
+.. _performing_a_pluralization:
+
+Performing a Pluralization
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A :term:`localizer` has a ``pluralize`` method with the following
+signature:
+
+.. code-block:: python
+ :linenos:
+
+ def pluralize(singular, plural, n, domain=None, mapping=None):
+ ...
+
+The ``singular`` and ``plural`` arguments should each be a Unicode
+value representing a :term:`message identifier`. ``num`` should be an
+integer. ``domain`` should be a :term:`translation domain`, and
+``mapping`` should be a dictionary that is used for *replacement
+value* interpolation of the translated string. If ``num`` is plural
+for the current locale, ``pluralize`` will return a Unicode
+translation for the message id ``plural``, otherwise it will return a
+Unicode translation for the message id ``singular``.
+
+The arguments provided as ``singular`` and/or ``plural`` may also be
+:term:`translation string` objects, but the domain and mapping
+information attached to those object is ignored.
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import get_localizer
+
+ def aview(request):
+ localizer = get_localizer(request)
+ translated = localizer.pluralize('Item', 'Items', 1, 'mydomain')
+ # ... use translated ...
+
+.. index::
+ single: locale name
+ single: get_locale_name
+ single: negotiate_locale_name
+
+.. _obtaining_the_locale_name:
+
+Obtaining the Locale Name for a Request
+---------------------------------------
+
+You can obtain the locale name related to a request by using the
+:func:`repoze.bfg.i18n.get_locale_name` function.
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import get_locale_name
+
+ def aview(request):
+ 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.
+
+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
+to :func:`repoze.bfg.i18n.get_locale_name` will return the stored
+locale name without invoking the :term:`locale negotiator`. To
+avoid this caching, you can use the
+:func:`repoze.bfg.i18n.negotiate_locale_name` function:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.i18n import negotiate_locale_name
+
+ def aview(request):
+ locale_name = negotiate_locale_name(request)
+
+You can also obtain the locale name related to a request using the
+``locale_name`` attribute of a :term:`localizer`.
+
+ from repoze.bfg.i18n import get_localizer
+
+ def aview(request):
+ localizer = get_localizer(request)
+ locale_name = localizer.locale_name
+
+Obtaining the locale name as an attribute of a localizer is equivalent
+to obtaining a locale name by calling the
+:func:`repoze.bfg.i18n.get_locale_name` function.
+
+.. index::
+ single: date and currency formatting (i18n)
+ single: Babel
+
+Performing Date Formatting and Currency Formatting
+--------------------------------------------------
+
+:mod:`repoze.bfg` does not itself perform date and currency formatting
+for different locales. However, :term:`Babel` can help you do this
+via the :class:`babel.core.Locale` class. The `Babel documentation
+for this class
+<http://babel.edgewall.org/wiki/ApiDocs/babel.core#babel.core:Locale>`_
+provides minimal information about how to perform date and currency
+related locale operations. See :ref:`installing_babel` for
+information about how to install Babel.
+
+The :class:`babel.core.Locale` class requires a :term:`locale name` as
+an argument to its constructor. You can use :mod:`repoze.bfg` APIs to
+obtain the locale name for a request to pass to the
+:class:`babel.core.Locale` constructor; see
+:ref:`obtaining_the_locale_name`. For example:
+
+.. code-block:: python
+ :linenos:
+
+ from babel.core import Locale
+ from repoze.bfg.i18n import get_locale_name
+
+ def aview(request):
+ locale_name = get_locale_name(request)
+ locale = Locale(locale_name)
+
+.. index::
+ pair: translation strings; Chameleon
+
+.. _chameleon_translation_strings:
+
+Chameleon Template Support for Translation Strings
+--------------------------------------------------
+
+When a :term:`translation string` is used as the subject of textual
+rendering by a :term:`Chameleon` template renderer, it will
+automatically be translated to the requesting user's language if a
+suitable translation exists. This is true of both the ZPT and text
+variants of the Chameleon template renderers.
+
+For example, in a Chameleon ZPT template, the translation string
+represented by "some_translation_string" in each example below will go
+through translation before being rendered:
+
+.. code-block:: xml
+ :linenos:
+
+ <span tal:content="some_translation_string"/>
+
+.. code-block:: xml
+ :linenos:
+
+ <span tal:replace="some_translation_string"/>
+
+.. code-block:: xml
+ :linenos:
+
+ <span>${some_translation_string}</span>
+
+.. code-block:: xml
+ :linenos:
+
+ <a tal:attributes="href some_translation_string">Click here</a>
+ XXX this appears to not yet work as of Chameleon 1.2.3
+
+The features represented by attributes of the ``i18n`` namespace of
+Chameleon will also consult the :mod:`repoze.bfg` translations.
+See
+`http://chameleon.repoze.org/docs/latest/i18n.html#the-i18n-namespace
+<http://chameleon.repoze.org/docs/latest/i18n.html#the-i18n-namespace>`_.
+
+.. note::
+
+ Unlike when Chameleon is used outside of :mod:`repoze.bfg`, when it
+ is used *within* :mod:`repoze.bfg`, it does not support use of the
+ ``zope.i18n`` translation framework. Applications which use
+ :mod:`repoze.bfg` should use the features documented in this
+ chapter rather than ``zope.i18n``.
+
+Third party :mod:`repoze.bfg` template renderers might not provide
+this support out of the box and may need special code to do an
+equivalent. For those, you can always use the more manual translation
+facility described in :ref:`performing_a_translation`.
+
+.. index::
+ single: localization deployment settings
+ single: default_locale_name
+
+.. _localization_deployment_settings:
+
+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
+:mod:`repoze.bfg.configuration.Configurator` constructor at startup
+time:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.configuration import Configurator
+ config = Configurator(settings={'default_locale_name':'de'})
+
+You may alternately supply a ``default_locale_name`` via an
+application's Paster ``.ini`` file:
+
+.. code-block:: ini
+ :linenos:
+
+ [app:main]
+ use = egg:MyProject#app
+ reload_templates = true
+ debug_authorization = false
+ debug_notfound = false
+ default_locale_name = de
+
+If this value is not supplied via the Configurator constructor or via
+a Paste onfig file, it will default to ``en``.
+
+If this setting is supplied within the :mod:`repoze.bfg` application
+``.ini`` file, it will be available as a settings key:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.setttings import get_settings
+ settings = get_settings()
+ default_locale_name = settings['default_locale_name']
+
+.. index::
+ pair: translation; activating
+ pair: locale; negotiator
+ single: translation directory
+
+.. _activating_translation:
+
+Activating Translation
+----------------------
+
+By default, a :mod:`repoze.bfg` application performs no translation.
+To turn translation on, you must do both of these two things:
+
+- Add at least one :term:`translation directory` to your application.
+
+- Configure a :term:`locale negotiator` into your application's
+ configuration.
+
+:term:`gettext` is the underlying machinery behind the
+:mod:`repoze.bfg` translation machinery. A translation directory is a
+directory organized to be useful to :term:`gettext`. A translation
+directory usually includes a listing of language directories, each of
+which itself includes an ``LC_MESSAGES`` directory. Each
+``LC_MESSAGES`` directory should contain one or more ``.mo`` files.
+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`
+: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
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You may add a :term:`translation directory` to your application's
+configuration either imperatively or via ZCML. Adding a 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 directory in the translation directory)
+within your :mod:`repoze.bfg` application to be available to use for
+translation services.
+
+Imperative
+++++++++++
+
+You can add a translation directory imperatively by using the
+:meth:`repoze.bfg.configuration.Configurator.add_translation_dirs`
+during application startup.
+
+For example:
+
+.. code-block:: python
+ :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
+:meth:`repoze.bfg.configuration.Configurator.add_translation_dirs`
+will be merged into translations from an message catalog added earlier
+if both translation directories contain translations for the same
+locale and :term:`translation domain`.
+
+ZCML
+++++
+
+You can add a translation directory via ZCML by using the
+:ref:`translationdir_directive` ZCML directive:
+
+.. code-block:: xml
+ :linenos:
+
+ <translationdir dir="my.application:locale/"/>
+
+A message catalog in a translation directory added via
+:ref:`translationdir_directive` will be merged into translations from
+an message catalog added earlier if both translation directories
+contain translations for the same locale and :term:`translation
+domain`.
+
+.. _adding_a_locale_negotiator:
+
+Adding a Locale Negotiator
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You may add a :term:`locale negotiator` either imperatively or via
+ZCML. A 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`.
+
+Imperative
+++++++++++
+
+Pass an object which can act as the negotiator as the
+``locale_negotiator`` argument of the
+:class:`repoze.bfg.configuration.Configurator` instance during
+application startup.
+
+For example:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.configuration import Configurator
+ from repoze.bfg.i18n import default_locale_negotiator
+ config = Configurator(locale_negotiator=default_locale_negotiator)
+
+Alternately, use the
+:meth:`repoze.bfg.configuration.Configurator.set_locale_negotiator`
+method.
+
+For example:
+
+.. code-block:: python
+ :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.end()
+
+ZCML
+++++
+
+You can add a translation directory via ZCML by using the
+:ref:`localenegotiator_directive` ZCML directive:
+
+.. code-block:: xml
+ :linenos:
+
+ <localenegotiator negotiator="repoze.bfg.i18n.default_locale_negotiator"/>
+
+.. _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`.
diff --git a/docs/narr/project.rst b/docs/narr/project.rst
index 89bc5691b..dfac238b5 100644
--- a/docs/narr/project.rst
+++ b/docs/narr/project.rst
@@ -421,6 +421,7 @@ structure::
| |-- tests.py
| `-- views.py
|-- MyProject.ini
+ |-- setup.cfg
`-- setup.py
The ``MyProject`` :term:`Project`
@@ -441,6 +442,9 @@ describe, run, and test your application.
#. ``MyProject.ini`` is a :term:`PasteDeploy` configuration file that
can be used to execute your application.
+#. ``setup.cfg`` is a :term:`setuptools` configuration file used by
+ ``setup.py``.
+
#. ``setup.py`` is the file you'll use to test and distribute your
application. It is a standard :term:`setuptools` ``setup.py``
file.
@@ -633,6 +637,21 @@ tarball to other people who want to use your application.
you rerun ``setup.py sdist``, all files checked into the version
control system will be included in the tarball.
+``setup.cfg``
+~~~~~~~~~~~~~
+
+The ``setup.cfg`` file is a :term:`setuptools` configuration file. It
+contains various settings related to testing and internationalization:
+
+Our generated ``setup.cfg`` looks like this:
+
+.. literalinclude:: MyProject/setup.cfg
+ :linenos:
+
+The values in the default setup file allow various commonly-used
+internationalization commands and testing commands to work more
+smoothly.
+
.. index::
single: package
diff --git a/docs/narr/views.rst b/docs/narr/views.rst
index a24e4b7b5..eebaa63de 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -840,6 +840,11 @@ In all cases, the message provided to the exception constructor is
made available to the view which :mod:`repoze.bfg` invokes as
``request.exception.args[0]``.
+.. index::
+ single: exception views
+
+.. _exception_views:
+
Exception Views
~~~~~~~~~~~~~~~~
diff --git a/docs/zcml.rst b/docs/zcml.rst
index 0e45f7692..e1bfc4f4b 100644
--- a/docs/zcml.rst
+++ b/docs/zcml.rst
@@ -16,6 +16,7 @@ directive documentation is organized alphabetically by directive name.
zcml/configure
zcml/forbidden
zcml/include
+ zcml/localenegotiator
zcml/notfound
zcml/remoteuserauthenticationpolicy
zcml/renderer
@@ -25,5 +26,6 @@ directive documentation is organized alphabetically by directive name.
zcml/scan
zcml/static
zcml/subscriber
+ zcml/translationdir
zcml/utility
zcml/view
diff --git a/docs/zcml/localenegotiator.rst b/docs/zcml/localenegotiator.rst
new file mode 100644
index 000000000..59dcf32c2
--- /dev/null
+++ b/docs/zcml/localenegotiator.rst
@@ -0,0 +1,41 @@
+.. _localenegotiator_directive:
+
+``localenegotiator``
+--------------------
+
+Set the :term:`locale negotiator` for the current configurator to
+support localization of text.
+
+.. note: This ZCML directive is new as of :mod:`repoze.bfg` version 1.3.
+
+Attributes
+~~~~~~~~~~
+
+``negotiator``
+
+ The :term:`dotted Python name` to a :term:`locale negotiator`
+ implementation. This attribute is required. If it begins with a
+ dot (``.``), the name will be considered relative to the directory
+ in which the ZCML file which contains this directive lives.
+
+Example
+~~~~~~~
+
+.. code-block:: xml
+ :linenos:
+
+ <localenegotiator
+ negotiator="some.package.module.my_locale_negotiator"
+ />
+
+Alternatives
+~~~~~~~~~~~~
+
+Use :meth:`repoze.bfg.configuration.Configurator.set_locale_negotiator`
+method instance during initial application setup.
+
+See Also
+~~~~~~~~
+
+See also :ref:`activating_translation`.
+
diff --git a/docs/zcml/translationdir.rst b/docs/zcml/translationdir.rst
new file mode 100644
index 000000000..5c69cb737
--- /dev/null
+++ b/docs/zcml/translationdir.rst
@@ -0,0 +1,66 @@
+.. _translationdir_directive:
+
+``translationdir``
+------------------
+
+Add a :term:`gettext` :term:`translation directory` to the current
+configuration for use in localization of text.
+
+.. note: This ZCML directive is new as of :mod:`repoze.bfg` version 1.3.
+
+Attributes
+~~~~~~~~~~
+
+``dir``
+ The path to the translation directory. This path may either be 1)
+ absolute (e.g. ``/foo/bar/baz``) 2) Python-package-relative
+ (e.g. ``packagename:foo/bar/baz``) or 3) relative to the package
+ directory in which the ZCML file which contains the directive
+ (e.g. ``foo/bar/baz``).
+
+Example 1
+~~~~~~~~~
+
+.. code-block:: xml
+ :linenos:
+
+ <!-- relative to configure.zcml file -->
+
+ <translationdir
+ dir="locale"
+ />
+
+Example 2
+~~~~~~~~~
+
+.. code-block:: xml
+ :linenos:
+
+ <!-- relative to another package -->
+
+ <translationdir
+ dir="another.package:locale"
+ />
+
+Example 3
+~~~~~~~~~
+
+.. code-block:: xml
+ :linenos:
+
+ <!-- an absolute directory name -->
+
+ <translationdir
+ dir="/usr/share/locale"
+ />
+
+Alternatives
+~~~~~~~~~~~~
+
+Use :meth:`repoze.bfg.configuration.Configurator.add_translation_dirs`
+method instance during initial application setup.
+
+See Also
+~~~~~~~~
+
+See also :ref:`activating_translation`.
diff --git a/repoze/bfg/chameleon_text.py b/repoze/bfg/chameleon_text.py
index f1dc8e3aa..deb1cc43d 100644
--- a/repoze/bfg/chameleon_text.py
+++ b/repoze/bfg/chameleon_text.py
@@ -22,6 +22,7 @@ except ImportError: # pragma: no cover
from repoze.bfg.interfaces import IResponseFactory
from repoze.bfg.interfaces import ITemplateRenderer
+from repoze.bfg.interfaces import IChameleonTranslate
from repoze.bfg.decorator import reify
from repoze.bfg.renderers import template_renderer_factory
@@ -50,7 +51,12 @@ class TextTemplateRenderer(object):
def template(self):
settings = get_settings()
auto_reload = settings and settings['reload_templates']
- return TextTemplateFile(self.path, auto_reload=auto_reload)
+ reg = get_current_registry()
+ translate = None
+ if reg is not None:
+ translate = reg.queryUtility(IChameleonTranslate)
+ return TextTemplateFile(self.path, auto_reload=auto_reload,
+ translate=translate)
def implementation(self):
return self.template
diff --git a/repoze/bfg/chameleon_zpt.py b/repoze/bfg/chameleon_zpt.py
index f597ebd5f..687a11305 100644
--- a/repoze/bfg/chameleon_zpt.py
+++ b/repoze/bfg/chameleon_zpt.py
@@ -13,6 +13,7 @@ except ImportError: # pragma: no cover
def __init__(self, *arg, **kw):
raise ImportError, exc, tb
+from repoze.bfg.interfaces import IChameleonTranslate
from repoze.bfg.interfaces import IResponseFactory
from repoze.bfg.interfaces import ITemplateRenderer
@@ -33,7 +34,12 @@ class ZPTTemplateRenderer(object):
def template(self):
settings = get_settings()
auto_reload = settings and settings['reload_templates']
- return PageTemplateFile(self.path, auto_reload=auto_reload)
+ reg = get_current_registry()
+ translate = None
+ if reg is not None:
+ translate = reg.queryUtility(IChameleonTranslate)
+ return PageTemplateFile(self.path, auto_reload=auto_reload,
+ translate=translate)
def implementation(self):
return self.template
diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py
index 4673479da..9ebb62a01 100644
--- a/repoze/bfg/configuration.py
+++ b/repoze/bfg/configuration.py
@@ -8,6 +8,8 @@ import inspect
from webob import Response
import venusian
+from translationstring import ChameleonTranslate
+
from zope.configuration import xmlconfig
from zope.interface import Interface
@@ -17,9 +19,11 @@ from zope.interface import implements
from repoze.bfg.interfaces import IAuthenticationPolicy
from repoze.bfg.interfaces import IAuthorizationPolicy
+from repoze.bfg.interfaces import IChameleonTranslate
from repoze.bfg.interfaces import IDebugLogger
from repoze.bfg.interfaces import IDefaultRootFactory
from repoze.bfg.interfaces import IExceptionViewClassifier
+from repoze.bfg.interfaces import ILocaleNegotiator
from repoze.bfg.interfaces import IMultiView
from repoze.bfg.interfaces import IPackageOverrides
from repoze.bfg.interfaces import IRendererFactory
@@ -31,6 +35,7 @@ from repoze.bfg.interfaces import IRoutesMapper
from repoze.bfg.interfaces import ISecuredView
from repoze.bfg.interfaces import ISettings
from repoze.bfg.interfaces import ITemplateRenderer
+from repoze.bfg.interfaces import ITranslationDirectories
from repoze.bfg.interfaces import ITraverser
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IViewClassifier
@@ -45,8 +50,10 @@ from repoze.bfg.events import WSGIApplicationCreatedEvent
from repoze.bfg.exceptions import Forbidden
from repoze.bfg.exceptions import NotFound
from repoze.bfg.exceptions import ConfigurationError
+from repoze.bfg.i18n import get_localizer
from repoze.bfg.log import make_stream_logger
from repoze.bfg.path import caller_package
+from repoze.bfg.path import package_path
from repoze.bfg.registry import Registry
from repoze.bfg.request import route_request_iface
from repoze.bfg.resource import PackageOverrides
@@ -54,6 +61,7 @@ from repoze.bfg.resource import resolve_resource_spec
from repoze.bfg.settings import Settings
from repoze.bfg.static import StaticRootFactory
from repoze.bfg.threadlocal import get_current_registry
+from repoze.bfg.threadlocal import get_current_request
from repoze.bfg.threadlocal import manager
from repoze.bfg.traversal import traversal_path
from repoze.bfg.traversal import DefaultRootFactory
@@ -135,14 +143,16 @@ class Configurator(object):
logs to stderr will be used. If it is passed, it should be an
instance of the :class:`logging.Logger` (PEP 282) standard library
class. The debug logger is used by :mod:`repoze.bfg` itself to
- log warnings and authorization debugging information. """
-
+ log warnings and authorization debugging information.
+
+ """
manager = manager # for testing injection
venusian = venusian # for testing injection
def __init__(self, registry=None, package=None, settings=None,
root_factory=None, authentication_policy=None,
authorization_policy=None, renderers=DEFAULT_RENDERERS,
- debug_logger=None):
+ debug_logger=None,
+ locale_negotiator=None):
self.package = package or caller_package()
self.registry = registry
if registry is None:
@@ -154,7 +164,8 @@ class Configurator(object):
authentication_policy=authentication_policy,
authorization_policy=authorization_policy,
renderers=renderers,
- debug_logger=debug_logger)
+ debug_logger=debug_logger,
+ locale_negotiator=locale_negotiator)
def _set_settings(self, mapping):
settings = Settings(mapping or {})
@@ -270,7 +281,8 @@ class Configurator(object):
def setup_registry(self, settings=None, root_factory=None,
authentication_policy=None, authorization_policy=None,
- renderers=DEFAULT_RENDERERS, debug_logger=None):
+ renderers=DEFAULT_RENDERERS, debug_logger=None,
+ locale_negotiator=None):
""" When you pass a non-``None`` ``registry`` argument to the
:term:`Configurator` constructor, no initial 'setup' is
performed against the registry. This is because the registry
@@ -304,6 +316,8 @@ class Configurator(object):
self.add_renderer(name, renderer)
self.set_notfound_view(default_notfound_view)
self.set_forbidden_view(default_forbidden_view)
+ if locale_negotiator:
+ registry.registerUtility(locale_negotiator, ILocaleNegotiator)
# getSiteManager is a unit testing dep injection
def hook_zca(self, getSiteManager=None):
@@ -1225,10 +1239,6 @@ class Configurator(object):
found via context-finding or ``None`` if no context could
be found. The exception causing the registered view to be
called is however still available as ``request.exception``.
- .. warning:: This method has been deprecated in
- :mod:`repoze.bfg` 1.3. See
- :ref:`changing_the_forbidden_view` to see how a not found
- view should be registered in :mod:`repoze.bfg` 1.3+.
The ``view`` argument should be a :term:`view callable`.
@@ -1299,6 +1309,71 @@ class Configurator(object):
return self.add_view(bwcompat_view, context=NotFound,
wrapper=wrapper, _info=_info)
+ def set_locale_negotiator(self, negotiator):
+ """
+ Set the :term:`locale negotiator` for this application. The
+ :term:`locale negotiator` is a callable which accepts a
+ :term:`request` object and which returns a :term:`locale
+ name`. Later calls to this method override earlier calls;
+ there can be only one locale negotiator active at a time
+ within an application. See :ref:`activating_translation` for
+ more information.
+
+ .. note: This API is new as of :mod:`repoze.bfg` version 1.3.
+ """
+ self.registry.registerUtility(negotiator, ILocaleNegotiator)
+
+ def add_translation_dirs(self, *specs):
+ """ Add one or more :term:`translation directory` paths to the
+ current configuration state. The ``specs`` argument is a
+ sequence that may contain absolute directory paths
+ (e.g. ``/usr/share/locale``) or :term:`resource specification`
+ names naming a directory path (e.g. ``some.package:locale``)
+ or a combination of the two.
+
+ Example:
+
+ .. code-block:: python
+
+ add_translations_dirs('/usr/share/locale', 'some.package:locale')
+
+ .. note: This API is new as of :mod:`repoze.bfg` version 1.3.
+ """
+ for spec in specs:
+
+ package_name, filename = self._split_spec(spec)
+ if package_name is None: # absolute filename
+ directory = filename
+ else:
+ __import__(package_name)
+ package = sys.modules[package_name]
+ directory = os.path.join(package_path(package), filename)
+
+ if not os.path.isdir(os.path.realpath(directory)):
+ raise ConfigurationError('"%s" is not a directory' % directory)
+
+ tdirs = self.registry.queryUtility(ITranslationDirectories)
+ if tdirs is None:
+ tdirs = []
+ self.registry.registerUtility(tdirs, ITranslationDirectories)
+
+ tdirs.insert(0, directory)
+
+ if specs:
+
+ # We actually only need an IChameleonTranslate function
+ # utility to be registered zero or one times. We register the
+ # same function once for each added translation directory,
+ # which does too much work, but has the same effect.
+
+ def translator(msg):
+ request = get_current_request()
+ localizer = get_localizer(request)
+ return localizer.translate(msg)
+
+ ctranslate = ChameleonTranslate(translator)
+ self.registry.registerUtility(ctranslate, IChameleonTranslate)
+
def add_static_view(self, name, path, cache_max_age=3600, _info=u''):
""" Add a view used to render static resources to the current
configuration state.
diff --git a/repoze/bfg/i18n.py b/repoze/bfg/i18n.py
new file mode 100644
index 000000000..e3b276331
--- /dev/null
+++ b/repoze/bfg/i18n.py
@@ -0,0 +1,306 @@
+import gettext
+import os
+
+from translationstring import Translator
+from translationstring import Pluralizer
+from translationstring import TranslationString # API
+from translationstring import TranslationStringFactory # API
+
+TranslationString = TranslationString # PyFlakes
+TranslationStringFactory = TranslationStringFactory # PyFlakes
+
+from repoze.bfg.interfaces import ILocalizer
+from repoze.bfg.interfaces import ITranslationDirectories
+from repoze.bfg.interfaces import ILocaleNegotiator
+
+from repoze.bfg.settings import get_settings
+from repoze.bfg.threadlocal import get_current_registry
+
+class Localizer(object):
+ """
+ An object providing translation and pluralizations related to
+ the current request's locale name. A
+ :class:`repoze.bfg.i18n.Localizer` object is created using the
+ :func:`repoze.bfg.i18n.get_localizer` function.
+ """
+ def __init__(self, locale_name, translations):
+ self.locale_name = locale_name
+ self.translations = translations
+ self.pluralizer = None
+ self.translator = None
+
+ def translate(self, tstring, domain=None, mapping=None):
+ """
+ Translate a :term:`translation string` to the current language
+ and interpolate any *replacement markers* in the result. The
+ ``translate`` method accepts three arguments: ``tstring``
+ (required), ``domain`` (optional) and ``mapping`` (optional).
+ When called, it will translate the ``tstring`` translation
+ string to a ``unicode`` object using the current locale. If
+ the current locale could not be determined, the result of
+ interpolation of the default value is returned. The optional
+ ``domain`` argument can be used to specify or override the
+ domain of the ``tstring`` (useful when ``tstring`` is a normal
+ string rather than a translation string). The optional
+ ``mapping`` argument can specify or override the ``tstring``
+ interpolation mapping, useful when the ``tstring`` argument is
+ a simple string instead of a translation string.
+
+ Example::
+
+ from repoze.bfg.18n import TranslationString
+ ts = TranslationString('Add ${item}', domain='mypackage',
+ mapping={'item':'Item'})
+ translated = localizer.translate(ts)
+
+ Example::
+
+ translated = localizer.translate('Add ${item}', domain='mypackage',
+ mapping={'item':'Item'})
+
+ """
+ if self.translator is None:
+ self.translator = Translator(self.translations)
+ return self.translator(tstring, domain=domain, mapping=mapping)
+
+ def pluralize(self, singular, plural, n, domain=None, mapping=None):
+ """
+ Return a Unicode string translation by using two
+ :term:`message identifier` objects as a singular/plural pair
+ and an ``n`` value representing the number that appears in the
+ message using gettext plural forms support. The ``singular``
+ and ``plural`` objects passed may be translation strings or
+ unicode strings. ``n`` represents the number of elements.
+ ``domain`` is the translation domain to use to do the
+ pluralization, and ``mapping`` is the interpolation mapping
+ that should be used on the result. Note that if the objects
+ passed are translation strings, their domains and mappings are
+ ignored. The domain and mapping arguments must be used
+ instead. If the ``domain`` is not supplied, a default domain
+ is used (usually ``messages``).
+
+ Example::
+
+ num = 1
+ translated = localizer.pluralize('Add ${num} item',
+ 'Add ${num} items',
+ num,
+ mapping={'num':num})
+
+
+ """
+ if self.pluralizer is None:
+ self.pluralizer = Pluralizer(self.translations)
+ return self.pluralizer(singular, plural, n, domain=domain,
+ mapping=mapping)
+
+
+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')
+ if locale_name is None:
+ settings = get_settings() or {}
+ locale_name = settings.get('default_locale_name', 'en')
+ return locale_name
+
+def negotiate_locale_name(request):
+ """ Negotiate and return the :term:`locale name` associated with
+ the current request (never cached)."""
+ try:
+ registry = request.registry
+ except AttributeError:
+ registry = get_current_registry()
+ negotiator = registry.queryUtility(ILocaleNegotiator)
+ if negotiator 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):
+ """ Return the :term:`locale name` associated with the current
+ request (possibly cached)."""
+ locale_name = getattr(request, 'bfg_locale_name', None)
+ if locale_name is None:
+ locale_name = negotiate_locale_name(request)
+ request.bfg_locale_name = locale_name
+ return locale_name
+
+def get_localizer(request):
+ """ Retrieve a :class:`repoze.bfg.i18n.Localizer` object
+ corresponding to the current request's locale name. """
+ localizer = getattr(request, 'bfg_localizer', None)
+
+ if localizer is None:
+ # no locale object cached on request
+ try:
+ registry = request.registry
+ except AttributeError:
+ registry = get_current_registry()
+
+ current_locale_name = get_locale_name(request)
+ localizer = registry.queryUtility(ILocalizer, name=current_locale_name)
+
+ if localizer is None:
+ # no localizer utility registered yet
+ translations = Translations()
+ translations._catalog = {}
+ tdirs = registry.queryUtility(ITranslationDirectories, default=[])
+ for tdir in tdirs:
+ locale_dirs = [ (lname, os.path.join(tdir, lname)) for lname in
+ os.listdir(tdir) ]
+ for locale_name, locale_dir in locale_dirs:
+ if locale_name != current_locale_name:
+ continue
+ messages_dir = os.path.join(locale_dir, 'LC_MESSAGES')
+ if not os.path.isdir(os.path.realpath(messages_dir)):
+ continue
+ for mofile in os.listdir(messages_dir):
+ mopath = os.path.realpath(os.path.join(messages_dir,
+ mofile))
+ if mofile.endswith('.mo') and os.path.isfile(mopath):
+ mofp = open(mopath, 'rb')
+ domain = mofile[:-3]
+ dtrans = Translations(mofp, domain)
+ translations.add(dtrans)
+
+ localizer = Localizer(locale_name=current_locale_name,
+ translations=translations)
+ registry.registerUtility(localizer, ILocalizer,
+ name=current_locale_name)
+ request.bfg_localizer = localizer
+
+ return localizer
+
+class Translations(gettext.GNUTranslations, object):
+ """An extended translation catalog class (ripped off from Babel) """
+
+ DEFAULT_DOMAIN = 'messages'
+
+ def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN):
+ """Initialize the translations catalog.
+
+ :param fileobj: the file-like object the translation should be read
+ from
+ """
+ gettext.GNUTranslations.__init__(self, fp=fileobj)
+ self.files = filter(None, [getattr(fileobj, 'name', None)])
+ self.domain = domain
+ self._domains = {}
+
+ @classmethod
+ def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN):
+ """Load translations from the given directory.
+
+ :param dirname: the directory containing the ``MO`` files
+ :param locales: the list of locales in order of preference (items in
+ this list can be either `Locale` objects or locale
+ strings)
+ :param domain: the message domain
+ :return: the loaded catalog, or a ``NullTranslations`` instance if no
+ matching translations were found
+ :rtype: `Translations`
+ """
+ if locales is not None:
+ if not isinstance(locales, (list, tuple)):
+ locales = [locales]
+ locales = [str(l) for l in locales]
+ if not domain:
+ domain = cls.DEFAULT_DOMAIN
+ filename = gettext.find(domain, dirname, locales)
+ if not filename:
+ return gettext.NullTranslations()
+ return cls(fileobj=open(filename, 'rb'), domain=domain)
+
+ def __repr__(self):
+ return '<%s: "%s">' % (type(self).__name__,
+ self._info.get('project-id-version'))
+
+ def add(self, translations, merge=True):
+ """Add the given translations to the catalog.
+
+ If the domain of the translations is different than that of the
+ current catalog, they are added as a catalog that is only accessible
+ by the various ``d*gettext`` functions.
+
+ :param translations: the `Translations` instance with the messages to
+ add
+ :param merge: whether translations for message domains that have
+ already been added should be merged with the existing
+ translations
+ :return: the `Translations` instance (``self``) so that `merge` calls
+ can be easily chained
+ :rtype: `Translations`
+ """
+ domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
+ if merge and domain == self.domain:
+ return self.merge(translations)
+
+ existing = self._domains.get(domain)
+ if merge and existing is not None:
+ existing.merge(translations)
+ else:
+ translations.add_fallback(self)
+ self._domains[domain] = translations
+
+ return self
+
+ def merge(self, translations):
+ """Merge the given translations into the catalog.
+
+ Message translations in the specified catalog override any messages
+ with the same identifier in the existing catalog.
+
+ :param translations: the `Translations` instance with the messages to
+ merge
+ :return: the `Translations` instance (``self``) so that `merge` calls
+ can be easily chained
+ :rtype: `Translations`
+ """
+ if isinstance(translations, gettext.GNUTranslations):
+ self._catalog.update(translations._catalog)
+ if isinstance(translations, Translations):
+ self.files.extend(translations.files)
+
+ return self
+
+ def dgettext(self, domain, message):
+ """Like ``gettext()``, but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).gettext(message)
+
+ def ldgettext(self, domain, message):
+ """Like ``lgettext()``, but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).lgettext(message)
+
+ def dugettext(self, domain, message):
+ """Like ``ugettext()``, but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).ugettext(message)
+
+ def dngettext(self, domain, singular, plural, num):
+ """Like ``ngettext()``, but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).ngettext(singular, plural, num)
+
+ def ldngettext(self, domain, singular, plural, num):
+ """Like ``lngettext()``, but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).lngettext(singular, plural, num)
+
+ def dungettext(self, domain, singular, plural, num):
+ """Like ``ungettext()`` but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).ungettext(singular, plural, num)
+
diff --git a/repoze/bfg/includes/meta.zcml b/repoze/bfg/includes/meta.zcml
index 25894df71..5ecebd868 100644
--- a/repoze/bfg/includes/meta.zcml
+++ b/repoze/bfg/includes/meta.zcml
@@ -77,6 +77,18 @@
/>
<meta:directive
+ name="translationdir"
+ schema="repoze.bfg.zcml.ITranslationDirDirective"
+ handler="repoze.bfg.zcml.translationdir"
+ />
+
+ <meta:directive
+ name="localenegotiator"
+ schema="repoze.bfg.zcml.ILocaleNegotiatorDirective"
+ handler="repoze.bfg.zcml.localenegotiator"
+ />
+
+ <meta:directive
name="adapter"
schema="repoze.bfg.zcml.IAdapterDirective"
handler="repoze.bfg.zcml.adapter"
diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py
index 40d29348c..09b639525 100644
--- a/repoze/bfg/interfaces.py
+++ b/repoze/bfg/interfaces.py
@@ -229,3 +229,20 @@ class IPackageOverrides(Interface):
# VH_ROOT_KEY is an interface; its imported from other packages (e.g.
# traversalwrapper)
VH_ROOT_KEY = 'HTTP_X_VHM_ROOT'
+
+class IChameleonTranslate(Interface):
+ """ Internal interface representing a chameleon translate function """
+ def __call__(msgid, domain=None, mapping=None, context=None,
+ target_language=None, default=None):
+ """ Translate a mess of arguments to a Unicode object """
+
+class ILocalizer(Interface):
+ """ Localizer for a specific language """
+
+class ILocaleNegotiator(Interface):
+ def __call__(request):
+ """ Return a locale name """
+
+class ITranslationDirectories(Interface):
+ """ A list object representing all known translation directories
+ for an application"""
diff --git a/repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl b/repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl
index c3d79e08b..29fc7e806 100644
--- a/repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl
+++ b/repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl
@@ -6,6 +6,7 @@ use = egg:{{project}}#app
reload_templates = true
debug_authorization = false
debug_notfound = false
+default_locale_name = en
db_string = sqlite:///%(here)s/{{package}}.db
db_echo = false
diff --git a/repoze/bfg/paster_templates/alchemy/setup.cfg_tmpl b/repoze/bfg/paster_templates/alchemy/setup.cfg_tmpl
new file mode 100644
index 000000000..5bec29823
--- /dev/null
+++ b/repoze/bfg/paster_templates/alchemy/setup.cfg_tmpl
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package={{package}}
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = {{package}}/locale
+domain = {{project}}
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = {{package}}/locale/{{project}}.pot
+width = 80
+
+[init_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+
+[update_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+previous = true
diff --git a/repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl b/repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl
index 72c373fc2..c249172d3 100644
--- a/repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl
+++ b/repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl
@@ -6,6 +6,7 @@ use = egg:{{package}}#app
reload_templates = true
debug_authorization = false
debug_notfound = false
+default_locale_name = en
db_string = sqlite:///%(here)s/{{package}}.db
[pipeline:main]
diff --git a/repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl b/repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl
index 8bf5d22c1..5bec29823 100644
--- a/repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl
+++ b/repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl
@@ -4,3 +4,24 @@ nocapture=1
cover-package={{package}}
with-coverage=1
cover-erase=1
+
+[compile_catalog]
+directory = {{package}}/locale
+domain = {{project}}
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = {{package}}/locale/{{project}}.pot
+width = 80
+
+[init_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+
+[update_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+previous = true
diff --git a/repoze/bfg/paster_templates/starter/+project+.ini_tmpl b/repoze/bfg/paster_templates/starter/+project+.ini_tmpl
index cc1e2aa12..15be80cf4 100644
--- a/repoze/bfg/paster_templates/starter/+project+.ini_tmpl
+++ b/repoze/bfg/paster_templates/starter/+project+.ini_tmpl
@@ -6,6 +6,7 @@ use = egg:{{project}}#app
reload_templates = true
debug_authorization = false
debug_notfound = false
+default_locale_name = en
[server:main]
use = egg:Paste#http
diff --git a/repoze/bfg/paster_templates/starter/setup.cfg_tmpl b/repoze/bfg/paster_templates/starter/setup.cfg_tmpl
new file mode 100644
index 000000000..5bec29823
--- /dev/null
+++ b/repoze/bfg/paster_templates/starter/setup.cfg_tmpl
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package={{package}}
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = {{package}}/locale
+domain = {{project}}
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = {{package}}/locale/{{project}}.pot
+width = 80
+
+[init_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+
+[update_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+previous = true
diff --git a/repoze/bfg/paster_templates/zodb/+project+.ini_tmpl b/repoze/bfg/paster_templates/zodb/+project+.ini_tmpl
index ca6fe99a2..51b4e1ab8 100644
--- a/repoze/bfg/paster_templates/zodb/+project+.ini_tmpl
+++ b/repoze/bfg/paster_templates/zodb/+project+.ini_tmpl
@@ -6,6 +6,7 @@ use = egg:{{project}}#app
reload_templates = true
debug_authorization = false
debug_notfound = false
+default_locale_name = en
zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000
[pipeline:main]
diff --git a/repoze/bfg/paster_templates/zodb/setup.cfg_tmpl b/repoze/bfg/paster_templates/zodb/setup.cfg_tmpl
new file mode 100644
index 000000000..5bec29823
--- /dev/null
+++ b/repoze/bfg/paster_templates/zodb/setup.cfg_tmpl
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package={{package}}
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = {{package}}/locale
+domain = {{project}}
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = {{package}}/locale/{{project}}.pot
+width = 80
+
+[init_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+
+[update_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+previous = true
diff --git a/repoze/bfg/settings.py b/repoze/bfg/settings.py
index 069d25e6c..9cb2502a1 100644
--- a/repoze/bfg/settings.py
+++ b/repoze/bfg/settings.py
@@ -36,12 +36,16 @@ class Settings(dict):
config_reload_resources))
configure_zcml = self.get('configure_zcml', '')
eff_configure_zcml = eget('BFG_CONFIGURE_ZCML', configure_zcml)
+ locale_name = self.get('locale_name', 'en')
+ eff_locale_name = eget('BFG_DEFAULT_LOCALE_NAME', locale_name)
+
update = {
'debug_authorization': eff_debug_all or eff_debug_auth,
'debug_notfound': eff_debug_all or eff_debug_notfound,
'reload_templates': eff_reload_all or eff_reload_templates,
'reload_resources':eff_reload_all or eff_reload_resources,
'configure_zcml':eff_configure_zcml,
+ 'default_locale_name':eff_locale_name,
}
self.update(update)
diff --git a/repoze/bfg/tests/fixtures/__init__.py b/repoze/bfg/tests/fixtures/__init__.py
new file mode 100644
index 000000000..1a35cdb4a
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/__init__.py
@@ -0,0 +1 @@
+# a file
diff --git a/repoze/bfg/tests/fixtures/locale/GARBAGE b/repoze/bfg/tests/fixtures/locale/GARBAGE
new file mode 100644
index 000000000..032c55584
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/GARBAGE
@@ -0,0 +1 @@
+Garbage file.
diff --git a/repoze/bfg/tests/fixtures/locale/be/LC_MESSAGES b/repoze/bfg/tests/fixtures/locale/be/LC_MESSAGES
new file mode 100644
index 000000000..909cf6a3b
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/be/LC_MESSAGES
@@ -0,0 +1 @@
+busted.
diff --git a/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.mo b/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.mo
new file mode 100644
index 000000000..2924a5eb5
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.mo
Binary files differ
diff --git a/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.po b/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.po
new file mode 100644
index 000000000..17f87bc19
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.po
@@ -0,0 +1,31 @@
+# German translations for deformsite.
+# Copyright (C) 2010 ORGANIZATION
+# This file is distributed under the same license as the deformsite project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: deformsite 0.0\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2010-04-22 14:17+0400\n"
+"PO-Revision-Date: 2010-04-22 14:17-0400\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: de <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.5\n"
+
+#: deformsite/__init__.py:458
+msgid "Approve"
+msgstr "Genehmigen"
+
+#: deformsite/__init__.py:459
+msgid "Show approval"
+msgstr "Zeigen Genehmigung"
+
+#: deformsite/__init__.py:466
+msgid "Submit"
+msgstr "Beugen"
+
diff --git a/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.mo b/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.mo
new file mode 100644
index 000000000..2924a5eb5
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.mo
Binary files differ
diff --git a/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.po b/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.po
new file mode 100644
index 000000000..17f87bc19
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.po
@@ -0,0 +1,31 @@
+# German translations for deformsite.
+# Copyright (C) 2010 ORGANIZATION
+# This file is distributed under the same license as the deformsite project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: deformsite 0.0\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2010-04-22 14:17+0400\n"
+"PO-Revision-Date: 2010-04-22 14:17-0400\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: de <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.5\n"
+
+#: deformsite/__init__.py:458
+msgid "Approve"
+msgstr "Genehmigen"
+
+#: deformsite/__init__.py:459
+msgid "Show approval"
+msgstr "Zeigen Genehmigung"
+
+#: deformsite/__init__.py:466
+msgid "Submit"
+msgstr "Beugen"
+
diff --git a/repoze/bfg/tests/test_chameleon_text.py b/repoze/bfg/tests/test_chameleon_text.py
index 007c102e3..204805adb 100644
--- a/repoze/bfg/tests/test_chameleon_text.py
+++ b/repoze/bfg/tests/test_chameleon_text.py
@@ -28,6 +28,16 @@ class Base:
class TextTemplateRendererTests(Base, unittest.TestCase):
+ def setUp(self):
+ from repoze.bfg.configuration import Configurator
+ from repoze.bfg.registry import Registry
+ registry = Registry()
+ self.config = Configurator(registry=registry)
+ self.config.begin()
+
+ def tearDown(self):
+ self.config.end()
+
def _getTargetClass(self):
from repoze.bfg.chameleon_text import TextTemplateRenderer
return TextTemplateRenderer
@@ -54,6 +64,16 @@ class TextTemplateRendererTests(Base, unittest.TestCase):
template = instance.template
self.assertEqual(template, instance.__dict__['template'])
+ def test_template_with_ichameleon_translate(self):
+ from repoze.bfg.interfaces import IChameleonTranslate
+ def ct(): pass
+ self.config.registry.registerUtility(ct, IChameleonTranslate)
+ minimal = self._getTemplatePath('minimal.txt')
+ instance = self._makeOne(minimal)
+ self.failIf('template' in instance.__dict__)
+ template = instance.template
+ self.assertEqual(template.translate, ct)
+
def test_call(self):
minimal = self._getTemplatePath('minimal.txt')
instance = self._makeOne(minimal)
diff --git a/repoze/bfg/tests/test_chameleon_zpt.py b/repoze/bfg/tests/test_chameleon_zpt.py
index e4bf8f766..cbf9dd10b 100644
--- a/repoze/bfg/tests/test_chameleon_zpt.py
+++ b/repoze/bfg/tests/test_chameleon_zpt.py
@@ -21,6 +21,16 @@ class Base(object):
return reg
class ZPTTemplateRendererTests(Base, unittest.TestCase):
+ def setUp(self):
+ from repoze.bfg.configuration import Configurator
+ from repoze.bfg.registry import Registry
+ registry = Registry()
+ self.config = Configurator(registry=registry)
+ self.config.begin()
+
+ def tearDown(self):
+ self.config.end()
+
def _getTargetClass(self):
from repoze.bfg.chameleon_zpt import ZPTTemplateRenderer
return ZPTTemplateRenderer
@@ -55,6 +65,16 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase):
template = instance.template
self.assertEqual(template, instance.__dict__['template'])
+ def test_template_with_ichameleon_translate(self):
+ from repoze.bfg.interfaces import IChameleonTranslate
+ def ct(): pass
+ self.config.registry.registerUtility(ct, IChameleonTranslate)
+ minimal = self._getTemplatePath('minimal.pt')
+ instance = self._makeOne(minimal)
+ self.failIf('template' in instance.__dict__)
+ template = instance.template
+ self.assertEqual(template.translate, ct)
+
def test_call_with_nondict_value(self):
minimal = self._getTemplatePath('minimal.pt')
instance = self._makeOne(minimal)
diff --git a/repoze/bfg/tests/test_configuration.py b/repoze/bfg/tests/test_configuration.py
index 99d564b91..a1e753179 100644
--- a/repoze/bfg/tests/test_configuration.py
+++ b/repoze/bfg/tests/test_configuration.py
@@ -255,6 +255,15 @@ class ConfiguratorTests(unittest.TestCase):
config.setup_registry()
self.failUnless(reg.getUtility(IRootFactory))
+ def test_setup_registry_locale_negotiator(self):
+ from repoze.bfg.registry import Registry
+ from repoze.bfg.interfaces import ILocaleNegotiator
+ reg = Registry()
+ config = self._makeOne(reg)
+ config.setup_registry(locale_negotiator='abc')
+ utility = reg.getUtility(ILocaleNegotiator)
+ self.assertEqual(utility, 'abc')
+
def test_setup_registry_alternate_renderers(self):
from repoze.bfg.registry import Registry
from repoze.bfg.interfaces import IRendererFactory
@@ -1797,6 +1806,54 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(
config.registry.getUtility(IAuthorizationPolicy), policy)
+ def test_set_locale_negotiator(self):
+ from repoze.bfg.interfaces import ILocaleNegotiator
+ config = self._makeOne()
+ def negotiator(request): pass
+ config.set_locale_negotiator(negotiator)
+ self.assertEqual(config.registry.getUtility(ILocaleNegotiator),
+ negotiator)
+
+ def test_add_translation_dirs_missing_dir(self):
+ from repoze.bfg.exceptions import ConfigurationError
+ config = self._makeOne()
+ self.assertRaises(ConfigurationError,
+ config.add_translation_dirs,
+ '/wont/exist/on/my/system')
+
+ def test_add_translation_dirs_resource_spec(self):
+ import os
+ from repoze.bfg.interfaces import ITranslationDirectories
+ config = self._makeOne()
+ config.add_translation_dirs('repoze.bfg.tests.fixtures:locale')
+ here = os.path.dirname(__file__)
+ locale = os.path.join(here, 'fixtures', 'locale')
+ self.assertEqual(config.registry.getUtility(ITranslationDirectories),
+ [locale])
+
+ def test_add_translation_dirs_registers_chameleon_translate(self):
+ from repoze.bfg.interfaces import IChameleonTranslate
+ from repoze.bfg.threadlocal import manager
+ request = DummyRequest()
+ config = self._makeOne()
+ manager.push({'request':request, 'registry':config.registry})
+ try:
+ config.add_translation_dirs('repoze.bfg.tests.fixtures:locale')
+ translate = config.registry.getUtility(IChameleonTranslate)
+ self.assertEqual(translate('Approve'), u'Approve')
+ finally:
+ manager.pop()
+
+ def test_add_translation_dirs_abspath(self):
+ import os
+ from repoze.bfg.interfaces import ITranslationDirectories
+ config = self._makeOne()
+ here = os.path.dirname(__file__)
+ locale = os.path.join(here, 'fixtures', 'locale')
+ config.add_translation_dirs(locale)
+ self.assertEqual(config.registry.getUtility(ITranslationDirectories),
+ [locale])
+
def test__renderer_from_name_default_renderer(self):
from repoze.bfg.interfaces import IRendererFactory
config = self._makeOne()
diff --git a/repoze/bfg/tests/test_i18n.py b/repoze/bfg/tests/test_i18n.py
new file mode 100644
index 000000000..9c4b226e4
--- /dev/null
+++ b/repoze/bfg/tests/test_i18n.py
@@ -0,0 +1,382 @@
+# -*- coding: utf-8 -*-
+#
+
+import unittest
+from repoze.bfg.testing import cleanUp
+
+class TestTranslationString(unittest.TestCase):
+ def _makeOne(self, *arg, **kw):
+ from repoze.bfg.i18n import TranslationString
+ return TranslationString(*arg, **kw)
+
+ def test_it(self):
+ # this is part of the API, we don't actually need to test much more
+ # than that it's importable
+ ts = self._makeOne('a')
+ self.assertEqual(ts, 'a')
+
+class TestTranslationStringFactory(unittest.TestCase):
+ def _makeOne(self, *arg, **kw):
+ from repoze.bfg.i18n import TranslationStringFactory
+ return TranslationStringFactory(*arg, **kw)
+
+ def test_it(self):
+ # this is part of the API, we don't actually need to test much more
+ # than that it's importable
+ factory = self._makeOne('a')
+ self.assertEqual(factory('').domain, 'a')
+
+class TestLocalizer(unittest.TestCase):
+ def _makeOne(self, *arg, **kw):
+ from repoze.bfg.i18n import Localizer
+ return Localizer(*arg, **kw)
+
+ def test_ctor(self):
+ localizer = self._makeOne('en_US', None)
+ self.assertEqual(localizer.locale_name, 'en_US')
+ self.assertEqual(localizer.translations, None)
+
+ def test_translate(self):
+ translations = DummyTranslations()
+ localizer = self._makeOne(None, translations)
+ self.assertEqual(localizer.translate('123', domain='1',
+ mapping={}), '123')
+ self.failUnless(localizer.translator)
+
+ def test_pluralize(self):
+ translations = DummyTranslations()
+ localizer = self._makeOne(None, translations)
+ self.assertEqual(localizer.pluralize('singular', 'plural', 1,
+ domain='1', mapping={}),
+ 'singular')
+ self.failUnless(localizer.pluralizer)
+
+class Test_negotiate_locale_name(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _callFUT(self, request):
+ from repoze.bfg.i18n import negotiate_locale_name
+ return negotiate_locale_name(request)
+
+ def _registerImpl(self, impl):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ from repoze.bfg.interfaces import ILocaleNegotiator
+ registry.registerUtility(impl, ILocaleNegotiator)
+
+ def test_no_registry_on_request(self):
+ self._registerImpl(dummy_negotiator)
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'bogus')
+
+ def test_with_registry_on_request(self):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ self._registerImpl(dummy_negotiator)
+ request = DummyRequest()
+ request.registry = registry
+ result = self._callFUT(request)
+ self.assertEqual(result, 'bogus')
+
+ def test_default_from_settings(self):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ settings = {'default_locale_name':'settings'}
+ from repoze.bfg.interfaces import ISettings
+ registry.registerUtility(settings, ISettings)
+ request = DummyRequest()
+ request.registry = registry
+ result = self._callFUT(request)
+ self.assertEqual(result, 'settings')
+
+ def test_default_default(self):
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'en')
+
+class Test_get_locale_name(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _callFUT(self, request):
+ from repoze.bfg.i18n import get_locale_name
+ return get_locale_name(request)
+
+ def _registerImpl(self, impl):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ from repoze.bfg.interfaces import ILocaleNegotiator
+ registry.registerUtility(impl, ILocaleNegotiator)
+
+ def test_name_on_request(self):
+ request = DummyRequest()
+ request.bfg_locale_name = 'ie'
+ result = self._callFUT(request)
+ self.assertEqual(result, 'ie')
+
+ def test_name_not_on_request(self):
+ self._registerImpl(dummy_negotiator)
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'bogus')
+ self.assertEqual(request.bfg_locale_name, 'bogus')
+
+class Test_get_localizer(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _callFUT(self, request):
+ from repoze.bfg.i18n import get_localizer
+ return get_localizer(request)
+
+ def test_no_registry_on_request(self):
+ request = DummyRequest()
+ request.bfg_localizer = '123'
+ result = self._callFUT(request)
+ self.assertEqual(result, '123')
+
+ def test_with_registry_on_request(self):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ request = DummyRequest()
+ request.bfg_localizer = '123'
+ request.registry = registry
+ result = self._callFUT(request)
+ self.assertEqual(result, '123')
+
+ def test_locale_on_request(self):
+ request = DummyRequest()
+ request.bfg_localizer = 'abc'
+ result = self._callFUT(request)
+ self.assertEqual(result, 'abc')
+
+ def test_locale_from_registry(self):
+ from repoze.bfg.threadlocal import get_current_registry
+ from repoze.bfg.interfaces import ILocalizer
+ registry = get_current_registry()
+ locale = 'abc'
+ registry.registerUtility(locale, ILocalizer, name='en')
+ request = DummyRequest()
+ request.bfg_locale_name = 'en'
+ result = self._callFUT(request)
+ self.assertEqual(result, 'abc')
+
+ def test_locale_from_mo(self):
+ import os
+ from repoze.bfg.threadlocal import get_current_registry
+ from repoze.bfg.interfaces import ITranslationDirectories
+ from repoze.bfg.i18n import Localizer
+ registry = get_current_registry()
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ localedirs = [localedir]
+ registry.registerUtility(localedirs, ITranslationDirectories)
+ request = DummyRequest()
+ request.bfg_locale_name = 'de'
+ result = self._callFUT(request)
+ self.assertEqual(result.__class__, Localizer)
+ self.assertEqual(result.translate('Approve', 'deformsite'),
+ 'Genehmigen')
+ self.assertEqual(result.translate('Approve'), 'Approve')
+ self.failUnless(hasattr(result, 'pluralize'))
+
+ def test_locale_from_mo_bad_mo(self):
+ import os
+ from repoze.bfg.threadlocal import get_current_registry
+ from repoze.bfg.interfaces import ITranslationDirectories
+ from repoze.bfg.i18n import Localizer
+ registry = get_current_registry()
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ localedirs = [localedir]
+ registry.registerUtility(localedirs, ITranslationDirectories)
+ request = DummyRequest()
+ request.bfg_locale_name = 'be'
+ result = self._callFUT(request)
+ self.assertEqual(result.__class__, Localizer)
+ self.assertEqual(result.translate('Approve', 'deformsite'),
+ 'Approve')
+
+class Test_default_locale_negotiator(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _callFUT(self, request):
+ 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)
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'dude')
+
+ def test_settings_empty(self):
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'en')
+
+ def test_from_params(self):
+ request = DummyRequest()
+ request.params['locale'] = 'foo'
+ result = self._callFUT(request)
+ self.assertEqual(result, 'foo')
+
+class TestTranslations(unittest.TestCase):
+ def _getTargetClass(self):
+ from repoze.bfg.i18n import Translations
+ return Translations
+
+ def _makeOne(self):
+ messages1 = [
+ ('foo', 'Voh'),
+ (('foo1', 1), 'Voh1'),
+ ]
+ messages2 = [
+ ('foo', 'VohD'),
+ (('foo1', 1), 'VohD1'),
+ ]
+
+ klass = self._getTargetClass()
+
+ translations1 = klass(None, domain='messages')
+ translations1._catalog = dict(messages1)
+ translations1.plural = lambda *arg: 1
+ translations2 = klass(None, domain='messages1')
+ translations2._catalog = dict(messages2)
+ translations2.plural = lambda *arg: 1
+ translations = translations1.add(translations2, merge=False)
+ return translations
+
+ def test_load_domain_None(self):
+ import gettext
+ import os
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ locales = ['de', 'en']
+ klass = self._getTargetClass()
+ result = klass.load(localedir, locales, domain=None)
+ self.assertEqual(result.__class__, gettext.NullTranslations)
+
+ def test_load_found_locale_and_domain(self):
+ import os
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ locales = ['de', 'en']
+ klass = self._getTargetClass()
+ result = klass.load(localedir, locales, domain='deformsite')
+ self.assertEqual(result.__class__, klass)
+
+ def test_load_found_locale_and_domain_locale_is_string(self):
+ import os
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ locales = 'de'
+ klass = self._getTargetClass()
+ result = klass.load(localedir, locales, domain='deformsite')
+ self.assertEqual(result.__class__, klass)
+
+ def test___repr__(self):
+ inst = self._makeOne()
+ result = repr(inst)
+ self.assertEqual(result, '<Translations: "None">')
+
+ def test_merge_not_gnutranslations(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.merge(None), inst)
+
+ def test_merge_gnutranslations(self):
+ inst = self._makeOne()
+ inst2 = self._makeOne()
+ inst2._catalog['a'] = 'b'
+ inst.merge(inst2)
+ self.assertEqual(inst._catalog['a'], 'b')
+
+ def test_add_different_domain_merge_true_notexisting(self):
+ inst = self._makeOne()
+ inst2 = self._makeOne()
+ inst2.domain = 'domain2'
+ inst.add(inst2)
+ self.assertEqual(inst._domains['domain2'], inst2)
+
+ def test_add_different_domain_merge_true_existing(self):
+ inst = self._makeOne()
+ inst2 = self._makeOne()
+ inst3 = self._makeOne()
+ inst2.domain = 'domain2'
+ inst2._catalog['a'] = 'b'
+ inst3.domain = 'domain2'
+ inst._domains['domain2'] = inst3
+ inst.add(inst2)
+ self.assertEqual(inst._domains['domain2'], inst3)
+ self.assertEqual(inst3._catalog['a'], 'b')
+
+ def test_add_same_domain_merge_true(self):
+ inst = self._makeOne()
+ inst2 = self._makeOne()
+ inst2._catalog['a'] = 'b'
+ inst.add(inst2)
+ self.assertEqual(inst._catalog['a'], 'b')
+
+ def test_dgettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.dgettext('messages', 'foo'), 'Voh')
+ self.assertEqual(t.dgettext('messages1', 'foo'), 'VohD')
+
+ def test_ldgettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.ldgettext('messages', 'foo'), 'Voh')
+ self.assertEqual(t.ldgettext('messages1', 'foo'), 'VohD')
+
+ def test_dugettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.dugettext('messages', 'foo'), 'Voh')
+ self.assertEqual(t.dugettext('messages1', 'foo'), 'VohD')
+
+ def test_dngettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.dngettext('messages', 'foo1', 'foos1', 1), 'Voh1')
+ self.assertEqual(t.dngettext('messages1', 'foo1', 'foos1', 1), 'VohD1')
+
+ def test_ldngettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.ldngettext('messages', 'foo1', 'foos1', 1), 'Voh1')
+ self.assertEqual(t.ldngettext('messages1', 'foo1', 'foos1', 1), 'VohD1')
+
+ def test_dungettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.dungettext('messages', 'foo1', 'foos1', 1), 'Voh1')
+ self.assertEqual(t.dungettext('messages1', 'foo1', 'foos1', 1), 'VohD1')
+
+
+class DummyRequest(object):
+ def __init__(self):
+ self.params = {}
+
+def dummy_negotiator(request):
+ return 'bogus'
+
+class DummyTranslations(object):
+ def ugettext(self, text):
+ return text
+
+ def ungettext(self, singular, plural, n):
+ return singular
diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py
index 7f42e9f09..5fdf25f33 100644
--- a/repoze/bfg/tests/test_zcml.py
+++ b/repoze/bfg/tests/test_zcml.py
@@ -1041,6 +1041,56 @@ class TestUtilityDirective(unittest.TestCase):
self.assertEqual(utility['args'], (component, IFactory, '', None))
self.assertEqual(utility['kw'], {})
+class TestTranslationDirDirective(unittest.TestCase):
+ def setUp(self):
+ testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, *arg, **kw):
+ from repoze.bfg.zcml import translationdir
+ return translationdir(*arg, **kw)
+
+ def test_it(self):
+ from repoze.bfg.configuration import Configurator
+ context = DummyContext()
+ tdir = 'repoze.bfg.tests.fixtures:locale'
+ self._callFUT(context, tdir)
+ actions = context.actions
+ self.assertEqual(len(actions), 1)
+ action = context.actions[0]
+ self.assertEqual(action['discriminator'], ('tdir', tdir))
+ self.assertEqual(action['callable'].im_func,
+ Configurator.add_translation_dirs.im_func)
+ self.assertEqual(action['args'], (tdir,))
+ action['callable'](*action['args']) # doesn't blow up
+
+class TestLocaleNegotiatorDirective(unittest.TestCase):
+ def setUp(self):
+ testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, *arg, **kw):
+ from repoze.bfg.zcml import localenegotiator
+ return localenegotiator(*arg, **kw)
+
+ def test_it(self):
+ from repoze.bfg.configuration import Configurator
+ context = DummyContext()
+ dummy_negotiator = object()
+ self._callFUT(context, dummy_negotiator)
+ actions = context.actions
+ self.assertEqual(len(actions), 1)
+ action = context.actions[0]
+ self.assertEqual(action['discriminator'], 'lnegotiator')
+ self.assertEqual(action['callable'].im_func,
+ Configurator.set_locale_negotiator.im_func)
+ self.assertEqual(action['args'], (dummy_negotiator,))
+ action['callable'](*action['args']) # doesn't blow up
+
class TestLoadZCML(unittest.TestCase):
def setUp(self):
testing.setUp()
diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py
index 7ba82bfe7..3e8bb0c50 100644
--- a/repoze/bfg/zcml.py
+++ b/repoze/bfg/zcml.py
@@ -585,6 +585,41 @@ def scan(_context, package):
args=(package, None, _context.info)
)
+class ITranslationDirDirective(Interface):
+ dir = TextLine(
+ title=u"Add a translation directory",
+ description=(u"Add a translation directory"),
+ required=True,
+ )
+
+def translationdir(_context, dir):
+ path = path_spec(_context, dir)
+ reg = get_current_registry()
+ config = Configurator(reg, package=_context.package)
+
+ _context.action(
+ discriminator = ('tdir', path),
+ callable=config.add_translation_dirs,
+ args = (dir,),
+ )
+
+class ILocaleNegotiatorDirective(Interface):
+ negotiator = GlobalObject(
+ title=u"Configure a locale negotiator",
+ description=(u'Configure a locale negotiator'),
+ required=True,
+ )
+
+def localenegotiator(_context, negotiator):
+ reg = get_current_registry()
+ config = Configurator(reg, package=_context.package)
+
+ _context.action(
+ discriminator = 'lnegotiator',
+ callable=config.set_locale_negotiator,
+ args = (negotiator,)
+ )
+
class IAdapterDirective(Interface):
"""
Register an adapter
diff --git a/setup.py b/setup.py
index 3dbed2c2a..e5c5b9bfb 100644
--- a/setup.py
+++ b/setup.py
@@ -27,7 +27,7 @@ except IOError:
README = CHANGES = ''
install_requires=[
- 'Chameleon',
+ 'Chameleon >= 1.2.3',
'Paste > 1.7', # temp version pin to prevent PyPi install failure :-(
'PasteDeploy',
'PasteScript',
@@ -39,6 +39,7 @@ install_requires=[
'zope.deprecation',
'zope.interface >= 3.5.1', # 3.5.0 comment: "allow to bootstrap on jython"
'venusian >= 0.2',
+ 'translationstring',
]
if sys.version_info[:2] < (2, 6):