diff options
Diffstat (limited to 'repoze/bfg')
28 files changed, 1172 insertions, 11 deletions
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 Binary files differnew file mode 100644 index 000000000..2924a5eb5 --- /dev/null +++ b/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.mo 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 Binary files differnew file mode 100644 index 000000000..2924a5eb5 --- /dev/null +++ b/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.mo 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 |
