summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Vasiliev <dima@hlabs.spb.ru>2006-01-05 12:27:06 +0000
committerDmitry Vasiliev <dima@hlabs.spb.ru>2006-01-05 12:27:06 +0000
commitce84d044e648494ae03f30c0d7a956c254a26513 (patch)
tree35cd4b8a2c04eda3838d5500f413e165a5f24f4a
parent136811e67a31fdb4923041cd46fb61f8abe01045 (diff)
downloadzope-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__.py114
-rw-r--r--tests/test_interpolate.py28
-rw-r--r--translationdomain.py132
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()
+