diff options
author | Dmitry Vasiliev <dima@hlabs.spb.ru> | 2006-01-05 12:27:06 +0000 |
---|---|---|
committer | Dmitry Vasiliev <dima@hlabs.spb.ru> | 2006-01-05 12:27:06 +0000 |
commit | ce84d044e648494ae03f30c0d7a956c254a26513 (patch) | |
tree | 35cd4b8a2c04eda3838d5500f413e165a5f24f4a | |
parent | 136811e67a31fdb4923041cd46fb61f8abe01045 (diff) | |
download | zope-i18n-monolithic-zope3-3.2.tar.gz |
Merged revision 41144 from the trunk:monolithic-zope3-3.2
Fixed zope.i18n.interpolate:
- now if the variable wasn't found in the mapping no substitution
will happens.
- fixed interpolation in case "$name $$name", only the first variable
will be substituted.
-rw-r--r-- | __init__.py | 114 | ||||
-rw-r--r-- | tests/test_interpolate.py | 28 | ||||
-rw-r--r-- | translationdomain.py | 132 |
3 files changed, 274 insertions, 0 deletions
diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..ee8c399 --- /dev/null +++ b/__init__.py @@ -0,0 +1,114 @@ +############################################################################## +# +# Copyright (c) 2003 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""i18n support. + +$Id$ +""" +import re +import warnings + +# BBB 2005/10/10 -- MessageIDs are to be removed for Zope 3.3 +import zope.deprecation +zope.deprecation.__show__.off() +from zope.i18nmessageid import MessageIDFactory, MessageID +zope.deprecation.__show__.on() + +from zope.i18nmessageid import MessageFactory, Message +from zope.i18n.interfaces import ITranslationDomain +from zope.i18n.interfaces import IFallbackTranslationDomainFactory +from zope.component import queryUtility + + +# Set up regular expressions for finding interpolation variables in text. +# NAME_RE must exactly match the expression of the same name in the +# zope.tal.taldefs module: +NAME_RE = r"[a-zA-Z][-a-zA-Z0-9_]*" + +_interp_regex = re.compile(r'(?<!\$)(\$(?:(%(n)s)|{(%(n)s)}))' + % ({'n': NAME_RE})) + +def _translate(msgid, domain=None, mapping=None, context=None, + target_language=None, default=None): + + if isinstance(msgid, (MessageID, Message)): + domain = msgid.domain + default = msgid.default + mapping = msgid.mapping + + if default is None: + default = msgid + + if domain: + util = queryUtility(ITranslationDomain, domain) + if util is None: + util = queryUtility(IFallbackTranslationDomainFactory) + if util is not None: + util = util(domain) + else: + util = queryUtility(IFallbackTranslationDomainFactory) + if util is not None: + util = util() + + if util is None: + return interpolate(default, mapping) + + return util.translate(msgid, mapping, context, target_language, default) + +# BBB Backward compat +def translate(*args, **kw): + if args and not isinstance(args[0], basestring): + warnings.warn( + "translate no longer takes a location argument. " + "The argument was ignored.", + DeprecationWarning, 2) + args = args[1:] + return _translate(*args, **kw) + +def interpolate(text, mapping=None): + """Insert the data passed from mapping into the text. + + First setup a test mapping: + + >>> mapping = {"name": "Zope", "version": 3} + + In the text we can use substitution slots like $varname or ${varname}: + + >>> interpolate(u"This is $name version ${version}.", mapping) + u'This is Zope version 3.' + + Interpolation variables can be used more than once in the text: + + >>> interpolate(u"This is $name version ${version}. ${name} $version!", + ... mapping) + u'This is Zope version 3. Zope 3!' + + In case if the variable wasn't found in the mapping or '$$' form + was used no substitution will happens: + + >>> interpolate(u"This is $name $version. $unknown $$name $${version}.", + ... mapping) + u'This is Zope 3. $unknown $$name $${version}.' + + >>> interpolate(u"This is ${name}") + u'This is ${name}' + """ + + def replace(match): + whole, param1, param2 = match.groups() + return unicode(mapping.get(param1 or param2, whole)) + + if not text or not mapping: + return text + + return _interp_regex.sub(replace, text) diff --git a/tests/test_interpolate.py b/tests/test_interpolate.py new file mode 100644 index 0000000..9fb3cd5 --- /dev/null +++ b/tests/test_interpolate.py @@ -0,0 +1,28 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Tests for zope.i18n.interpolate. + +$Id$ +""" +import unittest + +from zope.testing import doctest + + +def test_suite(): + return doctest.DocTestSuite("zope.i18n") + + +if __name__=='__main__': + unittest.TextTestRunner().run(test_suite()) diff --git a/translationdomain.py b/translationdomain.py new file mode 100644 index 0000000..9069659 --- /dev/null +++ b/translationdomain.py @@ -0,0 +1,132 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Global Translation Service for providing I18n to file-based code. + +$Id$ +""" +# BBB 2005/10/10 -- MessageIDs are to be removed for Zope 3.3 +import zope.deprecation +zope.deprecation.__show__.off() +from zope.i18nmessageid import MessageID, Message +zope.deprecation.__show__.on() + +from zope.i18n import interpolate +from zope.i18n.simpletranslationdomain import SimpleTranslationDomain +from zope.component import getUtility +from zope.i18n.interfaces import ITranslationDomain +from zope.i18n.interfaces import INegotiator + +# The configuration should specify a list of fallback languages for the +# site. If a particular catalog for a negotiated language is not available, +# then the zcml specified order should be tried. If that fails, then as a +# last resort the languages in the following list are tried. If these fail +# too, then the msgid is returned. +# +# Note that these fallbacks are used only to find a catalog. If a particular +# message in a catalog is not translated, tough luck, you get the msgid. +LANGUAGE_FALLBACKS = ['en'] + + +class TranslationDomain(SimpleTranslationDomain): + + def __init__(self, domain, fallbacks=None): + self.domain = domain + # _catalogs maps (language, domain) to IMessageCatalog instances + self._catalogs = {} + # _data maps IMessageCatalog.getIdentifier() to IMessageCatalog + self._data = {} + # What languages to fallback to, if there is no catalog for the + # requested language (no fallback on individual messages) + if fallbacks is None: + fallbacks = LANGUAGE_FALLBACKS + self._fallbacks = fallbacks + + def _registerMessageCatalog(self, language, catalog_name): + key = language + mc = self._catalogs.setdefault(key, []) + mc.append(catalog_name) + + def addCatalog(self, catalog): + self._data[catalog.getIdentifier()] = catalog + self._registerMessageCatalog(catalog.language, + catalog.getIdentifier()) + + def setLanguageFallbacks(self, fallbacks=None): + if fallbacks is None: + fallbacks = LANGUAGE_FALLBACKS + self._fallbacks = fallbacks + + def translate(self, msgid, mapping=None, context=None, + target_language=None, default=None): + """See zope.i18n.interfaces.ITranslationDomain""" + + # if the msgid is empty, let's save a lot of calculations and return + # an empty string. + if msgid == u'': + return u'' + + if target_language is None and context is not None: + langs = self._catalogs.keys() + # invoke local or global unnamed 'INegotiator' utilities + negotiator = getUtility(INegotiator) + # try to determine target language from negotiator utility + target_language = negotiator.getLanguage(langs, context) + + # MessageID attributes override arguments + if isinstance(msgid, (Message, MessageID)): + if msgid.domain != self.domain: + util = getUtility(ITranslationDomain, msgid.domain) + mapping = msgid.mapping + default = msgid.default + + if default is None: + default = msgid + + # Get the translation. Use the specified fallbacks if this fails + catalog_names = self._catalogs.get(target_language) + if catalog_names is None: + for language in self._fallbacks: + catalog_names = self._catalogs.get(language) + if catalog_names is not None: + break + + text = default + if catalog_names: + if len(catalog_names) == 1: + # this is a slight optimization for the case when there is a + # single catalog. More importantly, it is extremely helpful + # when testing and the test language is used, because it + # allows the test language to get the default. + text = self._data[catalog_names[0]].queryMessage( + msgid, default) + else: + for name in catalog_names: + catalog = self._data[name] + s = catalog.queryMessage(msgid) + if s is not None: + text = s + break + + # Now we need to do the interpolation + if text and mapping: + text = interpolate(text, mapping) + return text + + def getCatalogsInfo(self): + return self._catalogs + + def reloadCatalogs(self, catalogNames): + for catalogName in catalogNames: + self._data[catalogName].reload() + |