Skip to content

Commit

Permalink
Added plural capabilities to the different translate methods. It echo…
Browse files Browse the repository at this point in the history
…es the changes made in the zope.i18nmessageid. This needs heavy testing. Also started to clean up the code to match pep8 recommandations.
  • Loading branch information
trollfot committed Sep 6, 2018
1 parent dafecab commit cff01f7
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 34 deletions.
23 changes: 17 additions & 6 deletions src/zope/i18n/__init__.py
Expand Up @@ -39,6 +39,7 @@ class _FallbackNegotiator(object):
def getLanguage(self, _allowed, _context):
return None


_fallback_negotiator = _FallbackNegotiator()


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -181,7 +187,11 @@ 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)
print(util.translate)
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.
Expand All @@ -197,15 +207,16 @@ 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}"))
Expand Down
8 changes: 6 additions & 2 deletions src/zope/i18n/format.py
Expand Up @@ -33,6 +33,7 @@
except NameError:
pass # Py3


def roundHalfUp(n):
"""Works like round() in python2.x
Expand All @@ -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
Expand Down Expand Up @@ -486,11 +489,11 @@ def format(self, obj, pattern=None, rounding=True):
return text_type(text)



DEFAULT = 0
IN_QUOTE = 1
IN_DATETIMEFIELD = 2


class DateTimePatternParseError(Exception):
"""DateTime Pattern Parse Error"""

Expand Down Expand Up @@ -760,6 +763,7 @@ def buildDateTimeInfo(dt, calendar, pattern):
PADDING4 = 8
GROUPING = 9


class NumberPatternParseError(Exception):
"""Number Pattern Parse Error"""

Expand Down
6 changes: 3 additions & 3 deletions src/zope/i18n/gettextmessagecatalog.py
Expand Up @@ -26,7 +26,7 @@ def ugettext(self, message):

def ungettext(self, singular, plural, n):
raise KeyError(singular)

gettext = ugettext
ngettext = ungettext

Expand Down Expand Up @@ -91,14 +91,14 @@ def queryPluralMessage(self, singular, plural, n, dft1=None, dft2=None):
if self._catalog.plural(n):
return dft2
return dft1

def queryMessage(self, id, default=None):
'See IMessageCatalog'
try:
return self._gettext(id)
except KeyError:
return default

def getIdentifier(self):
'See IMessageCatalog'
return self._path_to_file
13 changes: 11 additions & 2 deletions src/zope/i18n/interfaces/__init__.py
Expand Up @@ -121,6 +121,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.
Expand All @@ -136,7 +140,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.
Expand All @@ -150,6 +155,7 @@ def translate(msgid, mapping=None, context=None, target_language=None,
"""


class IFallbackTranslationDomainFactory(Interface):
"""Factory for creating fallback translation domains
Expand All @@ -161,14 +167,16 @@ def __call__(domain_id=u""):
"""Return a fallback translation domain for the given domain id.
"""


class ITranslator(Interface):
"""A collaborative object which contains the domain, context, and locale.
It is expected that object be constructed with enough information to find
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.
Expand Down Expand Up @@ -215,6 +223,7 @@ def getPreferredLanguages():
languages first.
"""


class IModifiableUserPreferredLanguages(IUserPreferredLanguages):

def setPreferredLanguages(languages):
Expand Down
4 changes: 3 additions & 1 deletion src/zope/i18n/negotiator.py
Expand Up @@ -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.
Expand All @@ -32,6 +33,7 @@ def normalize_langs(langs):
n_langs[normalize_lang(l)] = l
return n_langs


@implementer(INegotiator)
class Negotiator(object):

Expand Down
8 changes: 6 additions & 2 deletions src/zope/i18n/simpletranslationdomain.py
Expand Up @@ -18,8 +18,10 @@
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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/zope/i18n/tests/pl-default.po
Expand Up @@ -19,4 +19,4 @@ 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 pliko'w."
msgstr[2] "Istnieją %d plików."
6 changes: 3 additions & 3 deletions src/zope/i18n/tests/test_plurals.py
Expand Up @@ -77,7 +77,7 @@ def test_PolishPlurals(self):

self.assertEqual(catalog.getPluralMessage(
'There is one file.', 'There are %d files.', 0),
"Istnieją 0 pliko'w.")
"Istnieją 0 plików.")

self.assertEqual(catalog.getPluralMessage(
'There is one file.', 'There are %d files.', 1),
Expand All @@ -89,15 +89,15 @@ def test_PolishPlurals(self):

self.assertEqual(catalog.getPluralMessage(
'There is one file.', 'There are %d files.', 17),
"Istnieją 17 pliko'w.")
"Istnieją 17 plików.")

self.assertEqual(catalog.getPluralMessage(
'There is one file.', 'There are %d files.', 23),
"Istnieją 23 pliki.")

self.assertEqual(catalog.getPluralMessage(
'There is one file.', 'There are %d files.', 28),
"Istnieją 28 pliko'w.")
"Istnieją 28 plików.")


def test_suite():
Expand Down
46 changes: 33 additions & 13 deletions src/zope/i18n/translationdomain.py
Expand Up @@ -34,11 +34,13 @@

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
Expand All @@ -63,7 +65,8 @@ def setLanguageFallbacks(self, fallbacks=None):
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.
Expand All @@ -78,37 +81,43 @@ def translate(self, msgid, mapping=None, context=None,
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)
Expand All @@ -128,12 +137,23 @@ def _recursive_translate(self, msgid, mapping, target_language, default,
# 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
Expand Down
2 changes: 1 addition & 1 deletion src/zope/i18n/zcml.py
Expand Up @@ -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:
Expand Down

0 comments on commit cff01f7

Please sign in to comment.