diff options
Diffstat (limited to 'src/zope/i18n')
-rw-r--r-- | src/zope/i18n/__init__.py | 21 | ||||
-rw-r--r-- | src/zope/i18n/format.py | 8 | ||||
-rw-r--r-- | src/zope/i18n/gettextmessagecatalog.py | 55 | ||||
-rw-r--r-- | src/zope/i18n/interfaces/__init__.py | 32 | ||||
-rw-r--r-- | src/zope/i18n/negotiator.py | 4 | ||||
-rw-r--r-- | src/zope/i18n/simpletranslationdomain.py | 8 | ||||
-rw-r--r-- | src/zope/i18n/tests/de-default.mo | bin | 347 -> 483 bytes | |||
-rw-r--r-- | src/zope/i18n/tests/de-default.po | 6 | ||||
-rw-r--r-- | src/zope/i18n/tests/en-alt.mo | bin | 318 -> 361 bytes | |||
-rw-r--r-- | src/zope/i18n/tests/en-alt.po | 5 | ||||
-rw-r--r-- | src/zope/i18n/tests/en-default.mo | bin | 343 -> 932 bytes | |||
-rw-r--r-- | src/zope/i18n/tests/en-default.po | 31 | ||||
-rw-r--r-- | src/zope/i18n/tests/pl-default.mo | bin | 0 -> 570 bytes | |||
-rw-r--r-- | src/zope/i18n/tests/pl-default.po | 22 | ||||
-rw-r--r-- | src/zope/i18n/tests/test_imessagecatalog.py | 4 | ||||
-rw-r--r-- | src/zope/i18n/tests/test_plurals.py | 183 | ||||
-rw-r--r-- | src/zope/i18n/translationdomain.py | 46 | ||||
-rw-r--r-- | src/zope/i18n/zcml.py | 2 |
18 files changed, 392 insertions, 35 deletions
diff --git a/src/zope/i18n/__init__.py b/src/zope/i18n/__init__.py index 3088074..5f7d775 100644 --- a/src/zope/i18n/__init__.py +++ b/src/zope/i18n/__init__.py @@ -39,6 +39,7 @@ class _FallbackNegotiator(object): def getLanguage(self, _allowed, _context): return None + _fallback_negotiator = _FallbackNegotiator() @@ -79,8 +80,10 @@ def negotiate(context): negotiator = queryUtility(INegotiator, default=_fallback_negotiator) return negotiator.getLanguage(ALLOWED_LANGUAGES, context) + def translate(msgid, domain=None, mapping=None, context=None, - target_language=None, default=None): + target_language=None, default=None, msgid_plural=None, + default_plural=None, number=None): """Translate text. First setup some test components: @@ -160,6 +163,9 @@ def translate(msgid, domain=None, mapping=None, context=None, domain = msgid.domain default = msgid.default mapping = msgid.mapping + msgid_plural = msgid.msgid_plural + default_plural = msgid.default_plural + number = msgid.number if default is None: default = text_type(msgid) @@ -181,7 +187,10 @@ def translate(msgid, domain=None, mapping=None, context=None, if target_language is None and context is not None: target_language = negotiate(context) - return util.translate(msgid, mapping, context, target_language, default) + return util.translate( + msgid, mapping, context, target_language, default, + msgid_plural, default_plural, number) + def interpolate(text, mapping=None): """Insert the data passed from mapping into the text. @@ -197,15 +206,15 @@ def interpolate(text, mapping=None): Interpolation variables can be used more than once in the text: - >>> print(interpolate(u"This is $name version ${version}. ${name} $version!", - ... mapping)) + >>> print(interpolate( + ... u"This is $name version ${version}. ${name} $version!", mapping)) 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: - >>> print(interpolate(u"This is $name $version. $unknown $$name $${version}.", - ... mapping)) + >>> print(interpolate( + ... u"This is $name $version. $unknown $$name $${version}.", mapping)) This is Zope 3. $unknown $$name $${version}. >>> print(interpolate(u"This is ${name}")) diff --git a/src/zope/i18n/format.py b/src/zope/i18n/format.py index ee2e239..96f5ef3 100644 --- a/src/zope/i18n/format.py +++ b/src/zope/i18n/format.py @@ -33,6 +33,7 @@ try: except NameError: pass # Py3 + def roundHalfUp(n): """Works like round() in python2.x @@ -42,18 +43,20 @@ def roundHalfUp(n): """ return math.floor(n + math.copysign(0.5, n)) + def _findFormattingCharacterInPattern(char, pattern): return [entry for entry in pattern if isinstance(entry, tuple) and entry[0] == char] + class DateTimeParseError(Exception): """Error is raised when parsing of datetime failed.""" + @implementer(IDateTimeFormat) class DateTimeFormat(object): __doc__ = IDateTimeFormat.__doc__ - _DATETIMECHARS = "aGyMdEDFwWhHmsSkKz" calendar = None @@ -486,11 +489,11 @@ class NumberFormat(object): return text_type(text) - DEFAULT = 0 IN_QUOTE = 1 IN_DATETIMEFIELD = 2 + class DateTimePatternParseError(Exception): """DateTime Pattern Parse Error""" @@ -760,6 +763,7 @@ SUFFIX = 7 PADDING4 = 8 GROUPING = 9 + class NumberPatternParseError(Exception): """Number Pattern Parse Error""" diff --git a/src/zope/i18n/gettextmessagecatalog.py b/src/zope/i18n/gettextmessagecatalog.py index 307e5be..8e1272b 100644 --- a/src/zope/i18n/gettextmessagecatalog.py +++ b/src/zope/i18n/gettextmessagecatalog.py @@ -14,6 +14,7 @@ """A simple implementation of a Message Catalog. """ +from functools import wraps from gettext import GNUTranslations from zope.i18n.interfaces import IGlobalMessageCatalog from zope.interface import implementer @@ -22,7 +23,31 @@ from zope.interface import implementer class _KeyErrorRaisingFallback(object): def ugettext(self, message): raise KeyError(message) + + def ungettext(self, singular, plural, n): + raise KeyError(singular) + gettext = ugettext + ngettext = ungettext + + +def plural_formatting(func): + """This decorator interpolates the possible formatting marker. + This interpolation marker is usally present for plurals. + Example: `There are %d apples` or `They have %s pies.` + + Please note that the interpolation can be done, alternatively, + using the mapping. This is only present as a conveniance. + """ + @wraps(func) + def pformat(catalog, singular, plural, n, *args, **kwargs): + msg = func(catalog, singular, plural, n, *args, **kwargs) + try: + return msg % n + except TypeError: + # The message cannot be formatted : return it "raw". + return msg + return pformat @implementer(IGlobalMessageCatalog) @@ -33,13 +58,20 @@ class GettextMessageCatalog(object): def __init__(self, language, domain, path_to_file): """Initialize the message catalog""" - self.language = language.decode('utf-8') if isinstance(language, bytes) else language - self.domain = domain.decode("utf-8") if isinstance(domain, bytes) else domain + self.language = ( + language.decode('utf-8') if isinstance(language, bytes) + else language) + self.domain = ( + domain.decode("utf-8") if isinstance(domain, bytes) + else domain) self._path_to_file = path_to_file self.reload() catalog = self._catalog catalog.add_fallback(_KeyErrorRaisingFallback()) - self._gettext = catalog.gettext if str is not bytes else catalog.ugettext + self._gettext = ( + catalog.gettext if str is not bytes else catalog.ugettext) + self._ngettext = ( + catalog.ngettext if str is not bytes else catalog.ungettext) def reload(self): 'See IMessageCatalog' @@ -50,6 +82,23 @@ class GettextMessageCatalog(object): 'See IMessageCatalog' return self._gettext(id) + @plural_formatting + def getPluralMessage(self, singular, plural, n): + 'See IMessageCatalog' + return self._ngettext(singular, plural, n) + + @plural_formatting + def queryPluralMessage(self, singular, plural, n, dft1=None, dft2=None): + 'See IMessageCatalog' + try: + return self._ngettext(singular, plural, n) + except KeyError: + # Here, we use the catalog plural function to determine + # if `n` triggers a plural form or not. + if self._catalog.plural(n): + return dft2 + return dft1 + def queryMessage(self, id, default=None): 'See IMessageCatalog' try: diff --git a/src/zope/i18n/interfaces/__init__.py b/src/zope/i18n/interfaces/__init__.py index 397c8f5..11d8c21 100644 --- a/src/zope/i18n/interfaces/__init__.py +++ b/src/zope/i18n/interfaces/__init__.py @@ -60,13 +60,30 @@ class IMessageCatalog(Interface): An exception is raised if the message id is not found. """ - + def queryMessage(msgid, default=None): """Look for the appropriate text for the given message id. If the message id is not found, default is returned. """ + def getPluralMessage(self, singular, plural, n): + """Get the appropriate text for the given message id and the + plural id. + + An exception is raised if nothing was found. + """ + + def queryPluralMessage(singular, plural, n, dft1=None, dft2=None): + """Look for the appropriate text for the given message id and the + plural id. + + If `n` is evaluated as a singular and the id is not found, + `dft1` is returned. + If `n` is evaluated as a plural and the plural id is not found, + `dft2` is returned. + """ + language = TextLine( title=u"Language", description=u"The language the catalog translates to.", @@ -118,6 +135,10 @@ class ITranslationDomain(Interface): target_language -- The language to translate to. + msgid_plural -- The id of the plural message that should be translated. + + number -- The number of items linked to the plural of the message. + context -- An object that provides contextual information for determining client language preferences. It must implement or have an adapter that implements IUserPreferredLanguages. @@ -133,7 +154,8 @@ class ITranslationDomain(Interface): required=True) def translate(msgid, mapping=None, context=None, target_language=None, - default=None): + default=None, msgid_plural=None, default_plural=None, + number=None): """Return the translation for the message referred to by msgid. Return the default if no translation is found. @@ -147,6 +169,7 @@ class ITranslationDomain(Interface): """ + class IFallbackTranslationDomainFactory(Interface): """Factory for creating fallback translation domains @@ -158,6 +181,7 @@ class IFallbackTranslationDomainFactory(Interface): """Return a fallback translation domain for the given domain id. """ + class ITranslator(Interface): """A collaborative object which contains the domain, context, and locale. @@ -165,7 +189,8 @@ class ITranslator(Interface): the domain, context, and target language. """ - def translate(msgid, mapping=None, default=None): + def translate(msgid, mapping=None, default=None, + msgid_plural=None, default_plural=None, number=None): """Translate the source msgid using the given mapping. See ITranslationService for details. @@ -212,6 +237,7 @@ class IUserPreferredLanguages(Interface): languages first. """ + class IModifiableUserPreferredLanguages(IUserPreferredLanguages): def setPreferredLanguages(languages): diff --git a/src/zope/i18n/negotiator.py b/src/zope/i18n/negotiator.py index 66417c0..3c5fa17 100644 --- a/src/zope/i18n/negotiator.py +++ b/src/zope/i18n/negotiator.py @@ -14,16 +14,17 @@ """Language Negotiator """ from zope.interface import implementer - from zope.i18n.interfaces import INegotiator from zope.i18n.interfaces import IUserPreferredLanguages + def normalize_lang(lang): lang = lang.strip().lower() lang = lang.replace('_', '-') lang = lang.replace(' ', '') return lang + def normalize_langs(langs): # Make a mapping from normalized->original so we keep can match # the normalized lang and return the original string. @@ -32,6 +33,7 @@ def normalize_langs(langs): n_langs[normalize_lang(l)] = l return n_langs + @implementer(INegotiator) class Negotiator(object): diff --git a/src/zope/i18n/simpletranslationdomain.py b/src/zope/i18n/simpletranslationdomain.py index b50fb68..b093db8 100644 --- a/src/zope/i18n/simpletranslationdomain.py +++ b/src/zope/i18n/simpletranslationdomain.py @@ -18,8 +18,10 @@ from zope.component import getUtility from zope.i18n.interfaces import ITranslationDomain, INegotiator from zope.i18n import interpolate + text_type = str if bytes is not str else unicode + @implementer(ITranslationDomain) class SimpleTranslationDomain(object): """This is the simplest implementation of the ITranslationDomain I @@ -39,12 +41,14 @@ class SimpleTranslationDomain(object): def __init__(self, domain, messages=None): """Initializes the object. No arguments are needed.""" - self.domain = domain.decode("utf-8") if isinstance(domain, bytes) else domain + self.domain = ( + domain.decode("utf-8") if isinstance(domain, bytes) else domain) self.messages = messages if messages is not None else {} assert self.messages is not None def translate(self, msgid, mapping=None, context=None, - target_language=None, default=None): + target_language=None, default=None, msgid_plural=None, + default_plural=None, number=None): '''See interface ITranslationDomain''' # Find out what the target language should be if target_language is None and context is not None: diff --git a/src/zope/i18n/tests/de-default.mo b/src/zope/i18n/tests/de-default.mo Binary files differindex 392a0ff..f49b8da 100644 --- a/src/zope/i18n/tests/de-default.mo +++ b/src/zope/i18n/tests/de-default.mo diff --git a/src/zope/i18n/tests/de-default.po b/src/zope/i18n/tests/de-default.po index 99a5f8d..7d3b2f6 100644 --- a/src/zope/i18n/tests/de-default.po +++ b/src/zope/i18n/tests/de-default.po @@ -6,9 +6,15 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ISO-8859-1\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" msgid "short_greeting" msgstr "Hallo!" msgid "greeting" msgstr "Hallo $name, wie geht es Dir?" + +msgid "There is one file." +msgid_plural "There are %d files." +msgstr[0] "Es gibt eine Datei." +msgstr[1] "Es gibt %d Dateien." diff --git a/src/zope/i18n/tests/en-alt.mo b/src/zope/i18n/tests/en-alt.mo Binary files differindex e08bb59..2e7a16a 100644 --- a/src/zope/i18n/tests/en-alt.mo +++ b/src/zope/i18n/tests/en-alt.mo diff --git a/src/zope/i18n/tests/en-alt.po b/src/zope/i18n/tests/en-alt.po index de55bbc..eb439fb 100644 --- a/src/zope/i18n/tests/en-alt.po +++ b/src/zope/i18n/tests/en-alt.po @@ -12,3 +12,8 @@ msgstr "Hey!" msgid "special" msgstr "Wow" + +msgid "apple" +msgid_plural "apple" +msgstr[0] "orange" +msgstr[1] "oranges" diff --git a/src/zope/i18n/tests/en-default.mo b/src/zope/i18n/tests/en-default.mo Binary files differindex 4164029..0dc0588 100644 --- a/src/zope/i18n/tests/en-default.mo +++ b/src/zope/i18n/tests/en-default.mo diff --git a/src/zope/i18n/tests/en-default.po b/src/zope/i18n/tests/en-default.po index a3d843b..2432498 100644 --- a/src/zope/i18n/tests/en-default.po +++ b/src/zope/i18n/tests/en-default.po @@ -6,9 +6,40 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ISO-8859-1\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" msgid "short_greeting" msgstr "Hello!" msgid "greeting" msgstr "Hello $name, how are you?" + +msgid "There is one file." +msgid_plural "There are %d files." +msgstr[0] "There is one file." +msgstr[1] "There are %d files." + +msgid "The item is rated 1/5 star." +msgid_plural "The item is rated %s/5 stars." +msgstr[0] "The item is rated 1/5 star." +msgstr[1] "The item is rated %s/5 stars." + +msgid "There is %d chance." +msgid_plural "There are %f chances." +msgstr[0] "There is %d chance." +msgstr[1] "There are %f chances." + +msgid "There is %d ${type}." +msgid_plural "There are %d ${type}." +msgstr[0] "There is %d ${type}." +msgstr[1] "There are %d ${type}." + +msgid "apple" +msgid_plural "apples" +msgstr[0] "apple" +msgstr[1] "apples" + +msgid "banana" +msgid_plural "bananas" +msgstr[0] "banana" +msgstr[1] "bananas" diff --git a/src/zope/i18n/tests/pl-default.mo b/src/zope/i18n/tests/pl-default.mo Binary files differnew file mode 100644 index 0000000..37b73fe --- /dev/null +++ b/src/zope/i18n/tests/pl-default.mo diff --git a/src/zope/i18n/tests/pl-default.po b/src/zope/i18n/tests/pl-default.po new file mode 100644 index 0000000..3951a9c --- /dev/null +++ b/src/zope/i18n/tests/pl-default.po @@ -0,0 +1,22 @@ +msgid "" +msgstr "" +"Project-Id-Version: Zope 3\n" +"PO-Revision-Date: 2018-09-04 11:05+0100\n" +"Last-Translator: Zope 3 contributors\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + + +msgid "short_greeting" +msgstr "Cześć !" + +msgid "greeting" +msgstr "Cześć $name, jak się masz?" + +msgid "There is one file." +msgid_plural "There are %d files." +msgstr[0] "Istnieje %d plik." +msgstr[1] "Istnieją %d pliki." +msgstr[2] "Istnieją %d plików." diff --git a/src/zope/i18n/tests/test_imessagecatalog.py b/src/zope/i18n/tests/test_imessagecatalog.py index 2856c67..803d4f0 100644 --- a/src/zope/i18n/tests/test_imessagecatalog.py +++ b/src/zope/i18n/tests/test_imessagecatalog.py @@ -43,24 +43,20 @@ class TestIMessageCatalog(unittest.TestCase): self.assertEqual(catalog.getMessage('short_greeting'), 'Hello!') self.assertRaises(KeyError, catalog.getMessage, 'foo') - def testQueryMessage(self): catalog = self._catalog self.assertEqual(catalog.queryMessage('short_greeting'), 'Hello!') self.assertEqual(catalog.queryMessage('foo'), None) self.assertEqual(catalog.queryMessage('foo', 'bar'), 'bar') - def testGetLanguage(self): catalog = self._catalog self.assertEqual(catalog.language, 'en') - def testGetDomain(self): catalog = self._catalog self.assertEqual(catalog.domain, 'default') - def testGetIdentifier(self): catalog = self._catalog self.assertEqual(catalog.getIdentifier(), self._getUniqueIndentifier()) diff --git a/src/zope/i18n/tests/test_plurals.py b/src/zope/i18n/tests/test_plurals.py new file mode 100644 index 0000000..4f7813e --- /dev/null +++ b/src/zope/i18n/tests/test_plurals.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Foundation 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. +# +############################################################################## +"""Test a gettext implementation of a Message Catalog. +""" +import os +import unittest +from zope.i18n import tests +from zope.i18n.translationdomain import TranslationDomain +from zope.i18n.gettextmessagecatalog import GettextMessageCatalog +from zope.i18nmessageid import MessageFactory + + +class TestPlurals(unittest.TestCase): + + def _getMessageCatalog(self, locale, variant="default"): + path = os.path.dirname(tests.__file__) + self._path = os.path.join(path, '%s-%s.mo' % (locale, variant)) + catalog = GettextMessageCatalog(locale, variant, self._path) + return catalog + + def _getTranslationDomain(self, locale, variant="default"): + path = os.path.dirname(tests.__file__) + self._path = os.path.join(path, '%s-%s.mo' % (locale, variant)) + catalog = GettextMessageCatalog(locale, variant, self._path) + domain = TranslationDomain('default') + domain.addCatalog(catalog) + return domain + + def test_GermanPlurals(self): + """Germanic languages such as english and german share the plural + rule. We test the german here. + """ + catalog = self._getMessageCatalog('de') + self.assertEqual(catalog.language, 'de') + + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 1), + 'Es gibt eine Datei.') + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 3), + 'Es gibt 3 Dateien.') + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 0), + 'Es gibt 0 Dateien.') + + # Unknown id + self.assertRaises(KeyError, catalog.getPluralMessage, + 'There are %d files.', 'bar', 6) + + # Query without default values + self.assertEqual(catalog.queryPluralMessage( + 'There is one file.', 'There are %d files.', 1), + 'Es gibt eine Datei.') + self.assertEqual(catalog.queryPluralMessage( + 'There is one file.', 'There are %d files.', 3), + 'Es gibt 3 Dateien.') + + # Query with default values + self.assertEqual(catalog.queryPluralMessage( + 'There are %d files.', 'There is one file.', 1, + 'Es gibt 1 Datei.', 'Es gibt %d Dateien !', ), + 'Es gibt 1 Datei.') + self.assertEqual(catalog.queryPluralMessage( + 'There are %d files.', 'There is one file.', 3, + 'Es gibt 1 Datei.', 'Es gibt %d Dateien !', ), + 'Es gibt 3 Dateien !') + + def test_PolishPlurals(self): + """Polish has a complex rule for plurals. It makes for a good + test subject. + """ + catalog = self._getMessageCatalog('pl') + self.assertEqual(catalog.language, 'pl') + + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 0), + u"Istnieją 0 plików.") + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 1), + u"Istnieje 1 plik.") + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 3), + u"Istnieją 3 pliki.") + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 17), + u"Istnieją 17 plików.") + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 23), + u"Istnieją 23 pliki.") + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 28), + u"Istnieją 28 plików.") + + def test_floater(self): + """Test with the number being a float. + We can use %f or %s to make sure it works. + """ + catalog = self._getMessageCatalog('en') + self.assertEqual(catalog.language, 'en') + + # It's cast to integer because of the %d in the translation string. + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 1.0), + 'There is one file.') + self.assertEqual(catalog.getPluralMessage( + 'There is one file.', 'There are %d files.', 3.5), + 'There are 3 files.') + + # It's cast to a string because of the %s in the translation string. + self.assertEqual(catalog.getPluralMessage( + 'The item is rated 1/5 star.', + 'The item is rated %s/5 stars.', 3.5), + 'The item is rated 3.5/5 stars.') + + # It's cast either to an int or a float because of the %s in + # the translation string. + self.assertEqual(catalog.getPluralMessage( + 'There is %d chance.', + 'There are %f chances.', 1.5), + 'There are 1.500000 chances.') + self.assertEqual(catalog.getPluralMessage( + 'There is %d chance.', + 'There are %f chances.', 3.5), + 'There are 3.500000 chances.') + + def test_recursive_translation(self): + domain = self._getTranslationDomain('en') + factory = MessageFactory('default') + translate = domain.translate + + # Singular + banana = factory('banana', msgid_plural='bananas', number=1) + phrase = factory('There is %d ${type}.', + msgid_plural='There are %d ${type}.', + number=1, mapping={'type': banana}) + self.assertEqual( + translate(phrase, target_language="en"), + 'There is 1 banana.') + + # Plural + apple = factory('apple', msgid_plural='apples', number=10) + phrase = factory('There is %d ${type}.', + msgid_plural='There are %d ${type}.', + number=10, mapping={'type': apple}) + self.assertEqual( + translate(phrase, target_language="en"), + 'There are 10 apples.') + + # Straight translation with translatable mapping + apple = factory('apple', msgid_plural='apples', number=75) + self.assertEqual( + translate(msgid='There is %d ${type}.', + msgid_plural='There are %d ${type}.', + mapping={'type': apple}, + target_language="en", number=75), + 'There are 75 apples.') + + # Add another catalog, to test the domain's catalogs iteration + # We add this catalog in first position, to resolve the translations + # there first. + alt_en = self._getMessageCatalog('en', variant="alt") + domain._data[alt_en.getIdentifier()] = alt_en + domain._catalogs[alt_en.language].insert(0, alt_en.getIdentifier()) + + apple = factory('apple', msgid_plural='apples', number=42) + self.assertEqual( + translate(msgid='There is %d ${type}.', + msgid_plural='There are %d ${type}.', + mapping={'type': apple}, + target_language="de", number=42), + 'There are 42 oranges.') diff --git a/src/zope/i18n/translationdomain.py b/src/zope/i18n/translationdomain.py index 684fa9b..c7f064e 100644 --- a/src/zope/i18n/translationdomain.py +++ b/src/zope/i18n/translationdomain.py @@ -34,11 +34,13 @@ LANGUAGE_FALLBACKS = ['en'] text_type = str if bytes is not str else unicode + @zope.interface.implementer(ITranslationDomain) class TranslationDomain(object): def __init__(self, domain, fallbacks=None): - self.domain = domain.decode("utf-8") if isinstance(domain, bytes) else domain + self.domain = ( + domain.decode("utf-8") if isinstance(domain, bytes) else domain) # _catalogs maps (language, domain) to IMessageCatalog instances self._catalogs = {} # _data maps IMessageCatalog.getIdentifier() to IMessageCatalog @@ -63,7 +65,8 @@ class TranslationDomain(object): self._fallbacks = fallbacks def translate(self, msgid, mapping=None, context=None, - target_language=None, default=None): + target_language=None, default=None, + msgid_plural=None, default_plural=None, number=None): """See zope.i18n.interfaces.ITranslationDomain""" # if the msgid is empty, let's save a lot of calculations and return # an empty string. @@ -78,37 +81,43 @@ class TranslationDomain(object): target_language = negotiator.getLanguage(langs, context) return self._recursive_translate( - msgid, mapping, target_language, default, context) + msgid, mapping, target_language, default, context, + msgid_plural, default_plural, number) def _recursive_translate(self, msgid, mapping, target_language, default, - context, seen=None): + context, msgid_plural, default_plural, number, + seen=None): """Recursively translate msg.""" # MessageID attributes override arguments if isinstance(msgid, Message): if msgid.domain != self.domain: - return translate(msgid, msgid.domain, mapping, context, - target_language, default) + return translate( + msgid, msgid.domain, mapping, context, target_language, + default, msgid_plural, default_plural, number) default = msgid.default mapping = msgid.mapping + msgid_plural = msgid.msgid_plural + default_plural = msgid.default_plural + number = msgid.number # Recursively translate mappings, if they are translatable if (mapping is not None and Message in (type(m) for m in mapping.values())): if seen is None: seen = set() - seen.add(msgid) + seen.add((msgid, msgid_plural)) mapping = mapping.copy() for key, value in mapping.items(): if isinstance(value, Message): # TODO Why isn't there an IMessage interface? # https://bugs.launchpad.net/zope3/+bug/220122 - if value in seen: + if (value, value.msgid_plural) in seen: raise ValueError( "Circular reference in mappings detected: %s" % value) mapping[key] = self._recursive_translate( - value, mapping, target_language, - default, context, seen) + value, mapping, target_language, default, context, + msgid_plural, default_plural, number, seen) if default is None: default = text_type(msgid) @@ -128,12 +137,23 @@ class TranslationDomain(object): # 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) + if msgid_plural is not None: + # This is a plural + text = self._data[catalog_names[0]].queryPluralMessage( + msgid, msgid_plural, number, default, default_plural) + else: + text = self._data[catalog_names[0]].queryMessage( + msgid, default) else: for name in catalog_names: catalog = self._data[name] - s = catalog.queryMessage(msgid) + if msgid_plural is not None: + # This is a plural + s = catalog.queryPluralMessage( + msgid, msgid_plural, number, + default, default_plural) + else: + s = catalog.queryMessage(msgid) if s is not None: text = s break diff --git a/src/zope/i18n/zcml.py b/src/zope/i18n/zcml.py index 39a4a29..4df9ccb 100644 --- a/src/zope/i18n/zcml.py +++ b/src/zope/i18n/zcml.py @@ -100,7 +100,7 @@ def registerTranslations(_context, directory, domain='*'): loaded = True domain_file = os.path.basename(domain_path) name = domain_file[:-3] - if not name in domains: + if name not in domains: domains[name] = {} domains[name][language] = domain_path if loaded: |