From d899605e30eef8e77f70184eac15fad1bf770586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20v=2E=20L=C3=B6wis?= Date: Thu, 21 Nov 2002 21:45:32 +0000 Subject: [PATCH] Patch #633547: Support plural forms. Do TODOs in test suite. --- Doc/lib/libgettext.tex | 45 ++++ Lib/gettext.py | 140 ++++++++++++- Lib/test/output/test_gettext | 46 ----- Lib/test/test_gettext.py | 385 ++++++++++++++++++++++++----------- Misc/ACKS | 1 + Misc/NEWS | 3 +- 6 files changed, 444 insertions(+), 176 deletions(-) delete mode 100644 Lib/test/output/test_gettext diff --git a/Doc/lib/libgettext.tex b/Doc/lib/libgettext.tex index 4c811072913..b75d13173b0 100644 --- a/Doc/lib/libgettext.tex +++ b/Doc/lib/libgettext.tex @@ -69,6 +69,32 @@ Like \function{gettext()}, but look the message up in the specified \var{domain}. \end{funcdesc} +\begin{funcdesc}{ngettext}{singular, plural, n} + +Like \function{gettext()}, but consider plural forms. If a translation +is found, apply the plural formula to \var{n}, and return the +resulting message (some languages have more than two plural forms). +If no translation is found, return \var{singular} if \var{n} is 1; +return \var{plural} otherwise. + +The Plural formula is taken from the catalog header. It is a C or +Python expression that has a free variable n; the expression evaluates +to the index of the plural in the catalog. See the GNU gettext +documentation for the precise syntax to be used in .po files, and the +formulas for a variety of languages. + +\versionadded{2.3} + +\end{funcdesc} + +\begin{funcdesc}{dngettext}{domain, singular, plural, n} +Like \function{ngettext()}, but look the message up in the specified +\var{domain}. + +\versionadded{2.3} +\end{funcdesc} + + Note that GNU \program{gettext} also defines a \function{dcgettext()} method, but this was deemed not useful and so it is currently unimplemented. @@ -207,6 +233,21 @@ Otherwise, return the translated message as a Unicode string. Overridden in derived classes. \end{methoddesc} +\begin{methoddesc}[NullTranslations]{ngettext}{singular, plural, n} +If a fallback has been set, forward \method{ngettext} to the fallback. +Otherwise, return the translated message. Overridden in derived classes. + +\versionadded{2.3} +\end{methoddesc} + +\begin{methoddesc}[NullTranslations]{ungettext}{singular, plural, n} +If a fallback has been set, forward \method{ungettext} to the fallback. +Otherwise, return the translated message as a Unicode string. +Overridden in derived classes. + +\versionadded{2.3} +\end{methoddesc} + \begin{methoddesc}[NullTranslations]{info}{} Return the ``protected'' \member{_info} variable. \end{methoddesc} @@ -263,6 +304,9 @@ returns a Unicode string by passing both the translated message string and the value of the ``protected'' \member{_charset} variable to the builtin \function{unicode()} function. +To facilitate plural forms, the methods \method{ngettext} and +\method{ungettext} are overridden as well. + \subsubsection{Solaris message catalog support} The Solaris operating system defines its own binary @@ -534,6 +578,7 @@ this module: \begin{itemize} \item Peter Funk \item James Henstridge + \Juan David Ib\'a\~nez Palomar \item Marc-Andr\'e Lemburg \item Martin von L\"owis \item Fran\c cois Pinard diff --git a/Lib/gettext.py b/Lib/gettext.py index f7649e696dd..2be677b325a 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -32,6 +32,8 @@ internationalized, to the local language and cultural habits. # Francois Pinard and Marc-Andre Lemburg also contributed valuably to this # module. # +# J. David Ibanez implemented plural forms. +# # TODO: # - Lazy loading of .mo files. Currently the entire catalog is loaded into # memory, but that's probably bad for large translated programs. Instead, @@ -43,18 +45,76 @@ internationalized, to the local language and cultural habits. # - Support Solaris .mo file formats. Unfortunately, we've been unable to # find this format documented anywhere. -import os -import sys -import struct -import copy + +import copy, os, re, struct, sys from errno import ENOENT + __all__ = ["bindtextdomain","textdomain","gettext","dgettext", "find","translation","install","Catalog"] _default_localedir = os.path.join(sys.prefix, 'share', 'locale') +def test(condition, true, false): + """ + Implements the C expression: + + condition ? true : false + + Required to correctly interpret plural forms. + """ + if condition: + return true + else: + return false + + +def c2py(plural): + """ + Gets a C expression as used in PO files for plural forms and + returns a Python lambda function that implements an equivalent + expression. + """ + # Security check, allow only the "n" identifier + from StringIO import StringIO + import token, tokenize + tokens = tokenize.generate_tokens(StringIO(plural).readline) + danger = [ x for x in tokens if x[0] == token.NAME and x[1] != 'n' ] + if danger: + raise ValueError, 'dangerous expression' + + # Replace some C operators by their Python equivalents + plural = plural.replace('&&', ' and ') + plural = plural.replace('||', ' or ') + + expr = re.compile(r'\![^=]') + plural = expr.sub(' not ', plural) + + # Regular expression and replacement function used to transform + # "a?b:c" to "test(a,b,c)". + expr = re.compile(r'(.*?)\?(.*?):(.*)') + def repl(x): + return "test(%s, %s, %s)" % (x.group(1), x.group(2), + expr.sub(repl, x.group(3))) + + # Code to transform the plural expression, taking care of parentheses + stack = [''] + for c in plural: + if c == '(': + stack.append('') + elif c == ')': + if len(stack) == 0: + raise ValueError, 'unbalanced parenthesis in plural form' + s = expr.sub(repl, stack.pop()) + stack[-1] += '(%s)' % s + else: + stack[-1] += c + plural = expr.sub(repl, stack.pop()) + + return eval('lambda n: int(%s)' % plural) + + def _expand_lang(locale): from locale import normalize @@ -121,11 +181,27 @@ class NullTranslations: return self._fallback.gettext(message) return message + def ngettext(self, msgid1, msgid2, n): + if self._fallback: + return self._fallback.ngettext(msgid1, msgid2, n) + if n == 1: + return msgid1 + else: + return msgid2 + def ugettext(self, message): if self._fallback: return self._fallback.ugettext(message) return unicode(message) + def ungettext(self, msgid1, msgid2, n): + if self._fallback: + return self._fallback.ungettext(msgid1, msgid2, n) + if n == 1: + return unicode(msgid1) + else: + return unicode(msgid2) + def info(self): return self._info @@ -169,8 +245,16 @@ class GNUTranslations(NullTranslations): tlen, toff = unpack(ii, buf[transidx:transidx+8]) tend = toff + tlen if mend < buflen and tend < buflen: + msg = buf[moff:mend] tmsg = buf[toff:tend] - catalog[buf[moff:mend]] = tmsg + if msg.find('\x00') >= 0: + # Plural forms + msgid1, msgid2 = msg.split('\x00') + tmsg = tmsg.split('\x00') + for i in range(len(tmsg)): + catalog[(msgid1, i)] = tmsg[i] + else: + catalog[msg] = tmsg else: raise IOError(0, 'File is corrupt', filename) # See if we're looking at GNU .mo conventions for metadata @@ -186,6 +270,12 @@ class GNUTranslations(NullTranslations): self._info[k] = v if k == 'content-type': self._charset = v.split('charset=')[1] + elif k == 'plural-forms': + v = v.split(';') +## nplurals = v[0].split('nplurals=')[1] +## nplurals = int(nplurals.strip()) + plural = v[1].split('plural=')[1] + self.plural = c2py(plural) # advance to next entry in the seek tables masteridx += 8 transidx += 8 @@ -198,6 +288,19 @@ class GNUTranslations(NullTranslations): return self._fallback.gettext(message) return message + + def ngettext(self, msgid1, msgid2, n): + try: + return self._catalog[(msgid1, self.plural(n))] + except KeyError: + if self._fallback: + return self._fallback.ngettext(msgid1, msgid2, n) + if n == 1: + return msgid1 + else: + return msgid2 + + def ugettext(self, message): try: tmsg = self._catalog[message] @@ -208,6 +311,18 @@ class GNUTranslations(NullTranslations): return unicode(tmsg, self._charset) + def ungettext(self, msgid1, msgid2, n): + try: + tmsg = self._catalog[(msgid1, self.plural(n))] + except KeyError: + if self._fallback: + return self._fallback.ungettext(msgid1, msgid2, n) + if n == 1: + tmsg = msgid1 + else: + tmsg = msgid2 + return unicode(tmsg, self._charset) + # Locate a .mo file using the gettext strategy def find(domain, localedir=None, languages=None, all=0): @@ -311,10 +426,25 @@ def dgettext(domain, message): return t.gettext(message) +def dngettext(domain, msgid1, msgid2, n): + try: + t = translation(domain, _localedirs.get(domain, None)) + except IOError: + if n == 1: + return msgid1 + else: + return msgid2 + return t.ngettext(msgid1, msgid2, n) + + def gettext(message): return dgettext(_current_domain, message) +def ngettext(msgid1, msgid2, n): + return dngettext(_current_domain, msgid1, msgid2, n) + + # dcgettext() has been deemed unnecessary and is not implemented. # James Henstridge's Catalog constructor from GNOME gettext. Documented usage diff --git a/Lib/test/output/test_gettext b/Lib/test/output/test_gettext deleted file mode 100644 index 0ff8a3f7ab9..00000000000 --- a/Lib/test/output/test_gettext +++ /dev/null @@ -1,46 +0,0 @@ -test_gettext -test api 1 -installing gettext -albatross -bacon -Throatwobbler Mangrove -wink wink -albatross -bacon -Throatwobbler Mangrove -wink wink -albatross -bacon -Throatwobbler Mangrove -wink wink -albatross -bacon -Throatwobbler Mangrove -wink wink -Guvf zbqhyr cebivqrf vagreangvbanyvmngvba naq ybpnyvmngvba -fhccbeg sbe lbhe Clguba cebtenzf ol cebivqvat na vagresnpr gb gur TAH -trggrkg zrffntr pngnybt yvoenel. -wink wink -bacon -test api 2 -True -gettext -albatross -bacon -Throatwobbler Mangrove -wink wink -albatross -bacon -Throatwobbler Mangrove -wink wink -albatross -bacon -Throatwobbler Mangrove -wink wink -albatross -bacon -Throatwobbler Mangrove -wink wink -Guvf zbqhyr cebivqrf vagreangvbanyvmngvba naq ybpnyvmngvba -fhccbeg sbe lbhe Clguba cebtenzf ol cebivqvat na vagresnpr gb gur TAH -trggrkg zrffntr pngnybt yvoenel. diff --git a/Lib/test/test_gettext.py b/Lib/test/test_gettext.py index 08787d0636c..2a1f24c479d 100644 --- a/Lib/test/test_gettext.py +++ b/Lib/test/test_gettext.py @@ -2,124 +2,34 @@ import os import base64 import gettext +import unittest +from unittest import TestCase -def test_api_1(localedir, mofile): - print 'test api 1' - - # Test basic interface - - print 'installing gettext' - gettext.install('gettext', localedir) - - # test some translations - print _('albatross') - print _(u'mullusk') - print _(r'Raymond Luxury Yach-t') - print _(ur'nudge nudge') - - # double quotes - print _("albatross") - print _(u"mullusk") - print _(r"Raymond Luxury Yach-t") - print _(ur"nudge nudge") - - # triple single quotes - print _('''albatross''') - print _(u'''mullusk''') - print _(r'''Raymond Luxury Yach-t''') - print _(ur'''nudge nudge''') - - # triple double quotes - print _("""albatross""") - print _(u"""mullusk""") - print _(r"""Raymond Luxury Yach-t""") - print _(ur"""nudge nudge""") - - # multiline strings - print _('''This module provides internationalization and localization -support for your Python programs by providing an interface to the GNU -gettext message catalog library.''') - - # test the alternative interface - fp = open(os.path.join(mofile), 'rb') - t = gettext.GNUTranslations(fp) - fp.close() - - t.install() - - print _('nudge nudge') - - # try unicode return type - t.install(unicode=1) - - print _('mullusk') - - - -def test_api_2(localedir, mofile): - print 'test api 2' - - gettext.bindtextdomain('gettext', localedir) - print gettext.bindtextdomain('gettext') == localedir - - gettext.textdomain('gettext') - # should return 'gettext' - print gettext.textdomain() - - # local function override builtin - _ = gettext.gettext - - # test some translations - print _('albatross') - print _(u'mullusk') - print _(r'Raymond Luxury Yach-t') - print _(ur'nudge nudge') - - # double quotes - print _("albatross") - print _(u"mullusk") - print _(r"Raymond Luxury Yach-t") - print _(ur"nudge nudge") - - # triple single quotes - print _('''albatross''') - print _(u'''mullusk''') - print _(r'''Raymond Luxury Yach-t''') - print _(ur'''nudge nudge''') - - # triple double quotes - print _("""albatross""") - print _(u"""mullusk""") - print _(r"""Raymond Luxury Yach-t""") - print _(ur"""nudge nudge""") - - # multiline strings - print _('''This module provides internationalization and localization -support for your Python programs by providing an interface to the GNU -gettext message catalog library.''') - - # Now test dgettext() - def _(message): - return gettext.dgettext('gettext') - +# TODO: +# - Add new tests, for example for "dgettext" +# - Remove dummy tests, for example testing for single and double quotes +# has no sense, it would have if we were testing a parser (i.e. pygettext) +# - Tests should have only one assert. GNU_MO_DATA = '''\ -3hIElQAAAAAFAAAAHAAAAEQAAAAHAAAAbAAAAAAAAACIAAAAFQAAAIkAAAChAAAAnwAAAAcAAABB -AQAACwAAAEkBAAAZAQAAVQEAABYAAABvAgAAoQAAAIYCAAAFAAAAKAMAAAkAAAAuAwAAAQAAAAQA -AAACAAAAAAAAAAUAAAAAAAAAAwAAAABSYXltb25kIEx1eHVyeSBZYWNoLXQAVGhpcyBtb2R1bGUg -cHJvdmlkZXMgaW50ZXJuYXRpb25hbGl6YXRpb24gYW5kIGxvY2FsaXphdGlvbgpzdXBwb3J0IGZv -ciB5b3VyIFB5dGhvbiBwcm9ncmFtcyBieSBwcm92aWRpbmcgYW4gaW50ZXJmYWNlIHRvIHRoZSBH -TlUKZ2V0dGV4dCBtZXNzYWdlIGNhdGFsb2cgbGlicmFyeS4AbXVsbHVzawBudWRnZSBudWRnZQBQ -cm9qZWN0LUlkLVZlcnNpb246IDIuMApQTy1SZXZpc2lvbi1EYXRlOiAyMDAwLTA4LTI5IDEyOjE5 -LTA0OjAwCkxhc3QtVHJhbnNsYXRvcjogQmFycnkgQS4gV2Fyc2F3IDxiYXJyeUBweXRob24ub3Jn -PgpMYW5ndWFnZS1UZWFtOiBYWCA8cHl0aG9uLWRldkBweXRob24ub3JnPgpNSU1FLVZlcnNpb246 -IDEuMApDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9a29pOF9yCkNvbnRlbnQtVHJh -bnNmZXItRW5jb2Rpbmc6IG5vbmUKR2VuZXJhdGVkLUJ5OiBweWdldHRleHQucHkgMS4xCgBUaHJv -YXR3b2JibGVyIE1hbmdyb3ZlAEd1dmYgemJxaHlyIGNlYml2cXJmIHZhZ3JlYW5ndmJhbnl2bW5n -dmJhIG5hcSB5YnBueXZtbmd2YmEKZmhjY2JlZyBzYmUgbGJoZSBDbGd1YmEgY2VidGVuemYgb2wg -Y2ViaXZxdmF0IG5hIHZhZ3Jlc25wciBnYiBndXIgVEFICnRyZ2dya2cgenJmZm50ciBwbmdueWJ0 -IHl2b2VuZWwuAGJhY29uAHdpbmsgd2luawA= +3hIElQAAAAAGAAAAHAAAAEwAAAALAAAAfAAAAAAAAACoAAAAFQAAAKkAAAAjAAAAvwAAAKEAAADj +AAAABwAAAIUBAAALAAAAjQEAAEUBAACZAQAAFgAAAN8CAAAeAAAA9gIAAKEAAAAVAwAABQAAALcD +AAAJAAAAvQMAAAEAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABQAAAAYAAAACAAAAAFJh +eW1vbmQgTHV4dXJ5IFlhY2gtdABUaGVyZSBpcyAlcyBmaWxlAFRoZXJlIGFyZSAlcyBmaWxlcwBU +aGlzIG1vZHVsZSBwcm92aWRlcyBpbnRlcm5hdGlvbmFsaXphdGlvbiBhbmQgbG9jYWxpemF0aW9u +CnN1cHBvcnQgZm9yIHlvdXIgUHl0aG9uIHByb2dyYW1zIGJ5IHByb3ZpZGluZyBhbiBpbnRlcmZh +Y2UgdG8gdGhlIEdOVQpnZXR0ZXh0IG1lc3NhZ2UgY2F0YWxvZyBsaWJyYXJ5LgBtdWxsdXNrAG51 +ZGdlIG51ZGdlAFByb2plY3QtSWQtVmVyc2lvbjogMi4wClBPLVJldmlzaW9uLURhdGU6IDIwMDAt +MDgtMjkgMTI6MTktMDQ6MDAKTGFzdC1UcmFuc2xhdG9yOiBKLiBEYXZpZCBJYsOhw7FleiA8ai1k +YXZpZEBub29zLmZyPgpMYW5ndWFnZS1UZWFtOiBYWCA8cHl0aG9uLWRldkBweXRob24ub3JnPgpN +SU1FLVZlcnNpb246IDEuMApDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9aXNvLTg4 +NTktMQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBub25lCkdlbmVyYXRlZC1CeTogcHlnZXR0 +ZXh0LnB5IDEuMQpQbHVyYWwtRm9ybXM6IG5wbHVyYWxzPTI7IHBsdXJhbD1uIT0xOwoAVGhyb2F0 +d29iYmxlciBNYW5ncm92ZQBIYXkgJXMgZmljaGVybwBIYXkgJXMgZmljaGVyb3MAR3V2ZiB6YnFo +eXIgY2ViaXZxcmYgdmFncmVhbmd2YmFueXZtbmd2YmEgbmFxIHlicG55dm1uZ3ZiYQpmaGNjYmVn +IHNiZSBsYmhlIENsZ3ViYSBjZWJ0ZW56ZiBvbCBjZWJpdnF2YXQgbmEgdmFncmVzbnByIGdiIGd1 +ciBUQUgKdHJnZ3JrZyB6cmZmbnRyIHBuZ255YnQgeXZvZW5lbC4AYmFjb24Ad2luayB3aW5rAA== ''' @@ -139,17 +49,236 @@ def teardown(): os.removedirs(LOCALEDIR) -try: - setup() - test_api_1(os.curdir, MOFILE) - test_api_2(os.curdir, MOFILE) -finally: - teardown() - pass +class GettextTestCase1(TestCase): + def setUp(self): + self.localedir = os.curdir + self.mofile = MOFILE + + gettext.install('gettext', self.localedir) + + + def test_some_translations(self): + # test some translations + assert _('albatross') == 'albatross' + assert _(u'mullusk') == 'bacon' + assert _(r'Raymond Luxury Yach-t') == 'Throatwobbler Mangrove' + assert _(ur'nudge nudge') == 'wink wink' + + + def test_double_quotes(self): + # double quotes + assert _("albatross") == 'albatross' + assert _(u"mullusk") == 'bacon' + assert _(r"Raymond Luxury Yach-t") == 'Throatwobbler Mangrove' + assert _(ur"nudge nudge") == 'wink wink' + + + def test_triple_single_quotes(self): + # triple single quotes + assert _('''albatross''') == 'albatross' + assert _(u'''mullusk''') == 'bacon' + assert _(r'''Raymond Luxury Yach-t''') == 'Throatwobbler Mangrove' + assert _(ur'''nudge nudge''') == 'wink wink' + + + def test_triple_double_quotes(self): + # triple double quotes + assert _("""albatross""") == 'albatross' + assert _(u"""mullusk""") == 'bacon' + assert _(r"""Raymond Luxury Yach-t""") == 'Throatwobbler Mangrove' + assert _(ur"""nudge nudge""") == 'wink wink' + + + def test_multiline_strings(self): + # multiline strings + assert _('''This module provides internationalization and localization +support for your Python programs by providing an interface to the GNU +gettext message catalog library.''') == '''Guvf zbqhyr cebivqrf vagreangvbanyvmngvba naq ybpnyvmngvba +fhccbeg sbe lbhe Clguba cebtenzf ol cebivqvat na vagresnpr gb gur TAH +trggrkg zrffntr pngnybt yvoenel.''' + + + def test_the_alternative_interface(self): + # test the alternative interface + fp = open(os.path.join(self.mofile), 'rb') + t = gettext.GNUTranslations(fp) + fp.close() + + t.install() + + assert _('nudge nudge') == 'wink wink' + + # try unicode return type + t.install(unicode=1) + + assert _('mullusk') == 'bacon' + + +class GettextTestCase2(TestCase): + def setUp(self): + self.localedir = os.curdir + + gettext.bindtextdomain('gettext', self.localedir) + gettext.textdomain('gettext') + + self._ = gettext.gettext + + + def test_bindtextdomain(self): + assert gettext.bindtextdomain('gettext') == self.localedir + + + def test_textdomain(self): + assert gettext.textdomain() == 'gettext' + + + def test_some_translations(self): + # test some translations + assert self._('albatross') == 'albatross' + assert self._(u'mullusk') == 'bacon' + assert self._(r'Raymond Luxury Yach-t') == 'Throatwobbler Mangrove' + assert self._(ur'nudge nudge') == 'wink wink' + + + def test_double_quotes(self): + # double quotes + assert self._("albatross") == 'albatross' + assert self._(u"mullusk") == 'bacon' + assert self._(r"Raymond Luxury Yach-t") == 'Throatwobbler Mangrove' + assert self._(ur"nudge nudge") == 'wink wink' + + + def test_triple_single_quotes(self): + # triple single quotes + assert self._('''albatross''') == 'albatross' + assert self._(u'''mullusk''') == 'bacon' + assert self._(r'''Raymond Luxury Yach-t''') == 'Throatwobbler Mangrove' + assert self._(ur'''nudge nudge''') == 'wink wink' + + + def test_triple_double_quotes(self): + # triple double quotes + assert self._("""albatross""") == 'albatross' + assert self._(u"""mullusk""") == 'bacon' + assert self._(r"""Raymond Luxury Yach-t""") == 'Throatwobbler Mangrove' + assert self._(ur"""nudge nudge""") == 'wink wink' + + + def test_multiline_strings(self): + # multiline strings + assert self._('''This module provides internationalization and localization +support for your Python programs by providing an interface to the GNU +gettext message catalog library.''') == '''Guvf zbqhyr cebivqrf vagreangvbanyvmngvba naq ybpnyvmngvba +fhccbeg sbe lbhe Clguba cebtenzf ol cebivqvat na vagresnpr gb gur TAH +trggrkg zrffntr pngnybt yvoenel.''' + + + + +class PluralFormsTestCase(TestCase): + def setUp(self): + self.mofile = MOFILE + + def test_plural_forms1(self): + x = gettext.ngettext('There is %s file', 'There are %s files', 1) + assert x == 'Hay %s fichero' + + x = gettext.ngettext('There is %s file', 'There are %s files', 2) + assert x == 'Hay %s ficheros' + + + def test_plural_forms2(self): + fp = open(os.path.join(self.mofile), 'rb') + t = gettext.GNUTranslations(fp) + fp.close() + + x = t.ngettext('There is %s file', 'There are %s files', 1) + assert x == 'Hay %s fichero' + + x = t.ngettext('There is %s file', 'There are %s files', 2) + assert x == 'Hay %s ficheros' + + + def test_hu(self): + f = gettext.c2py('0') + s = ''.join([ str(f(x)) for x in range(200) ]) + assert s == "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + + def test_de(self): + f = gettext.c2py('n != 1') + s = ''.join([ str(f(x)) for x in range(200) ]) + assert s == "10111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + + + def test_fr(self): + f = gettext.c2py('n>1') + s = ''.join([ str(f(x)) for x in range(200) ]) + assert s == "00111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + + + def test_gd(self): + f = gettext.c2py('n==1 ? 0 : n==2 ? 1 : 2') + s = ''.join([ str(f(x)) for x in range(200) ]) + assert s == "20122222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222" + + + def test_gd2(self): + # Tests the combination of parentheses and "?:" + f = gettext.c2py('n==1 ? 0 : (n==2 ? 1 : 2)') + s = ''.join([ str(f(x)) for x in range(200) ]) + assert s == "20122222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222" + + + def test_lt(self): + f = gettext.c2py('n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2') + s = ''.join([ str(f(x)) for x in range(200) ]) + assert s == "20111111112222222222201111111120111111112011111111201111111120111111112011111111201111111120111111112011111111222222222220111111112011111111201111111120111111112011111111201111111120111111112011111111" + + + def test_ru(self): + f = gettext.c2py('n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2') + s = ''.join([ str(f(x)) for x in range(200) ]) + assert s == "20111222222222222222201112222220111222222011122222201112222220111222222011122222201112222220111222222011122222222222222220111222222011122222201112222220111222222011122222201112222220111222222011122222" + + + def test_pl(self): + f = gettext.c2py('n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2') + s = ''.join([ str(f(x)) for x in range(200) ]) + assert s == "20111222222222222222221112222222111222222211122222221112222222111222222211122222221112222222111222222211122222222222222222111222222211122222221112222222111222222211122222221112222222111222222211122222" + + + def test_sl(self): + f = gettext.c2py('n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3') + s = ''.join([ str(f(x)) for x in range(200) ]) + assert s == "30122333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333012233333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333" + + + def test_security(self): + # Test for a dangerous expression + try: + gettext.c2py("os.chmod('/etc/passwd',0777)") + except ValueError: + pass + else: + raise AssertionError + + + +if __name__ == '__main__': + try: + setup() + unittest.main() + finally: + teardown() + # For reference, here's the .po file used to created the .mo data above. +# +# The original version was automatically generated from the sources with +# pygettext. Later it was manually modified to add plural forms support. ''' # Dummy translation for Python's test_gettext.py module. @@ -160,12 +289,13 @@ msgid "" msgstr "" "Project-Id-Version: 2.0\n" "PO-Revision-Date: 2000-08-29 12:19-04:00\n" -"Last-Translator: Barry A. Warsaw \n" +"Last-Translator: J. David Ibanez \n" "Language-Team: XX \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=koi8_r\n" -"Content-Transfer-Encoding: none\n" +"Content-Type: text/plain; charset=iso-8859-1\n" +"Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.1\n" +"Plural-Forms: nplurals=2; plural=n!=1;\n" #: test_gettext.py:19 test_gettext.py:25 test_gettext.py:31 test_gettext.py:37 #: test_gettext.py:51 test_gettext.py:80 test_gettext.py:86 test_gettext.py:92 @@ -198,4 +328,11 @@ msgstr "" "Guvf zbqhyr cebivqrf vagreangvbanyvmngvba naq ybpnyvmngvba\n" "fhccbeg sbe lbhe Clguba cebtenzf ol cebivqvat na vagresnpr gb gur TAH\n" "trggrkg zrffntr pngnybt yvoenel." + +# Manually added, as neither pygettext nor xgettext support plural forms +# in Python. +msgid "There is %s file" +msgid_plural "There are %s files" +msgstr[0] "Hay %s fichero" +msgstr[1] "Hay %s ficheros" ''' diff --git a/Misc/ACKS b/Misc/ACKS index 5e09c0ca120..a60e3a4931a 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -251,6 +251,7 @@ Michael Hudson Jim Hugunin Greg Humphreys Jeremy Hylton +Juan David Ibáñez Palomar Tony Ingraldi John Interrante Ben Jackson diff --git a/Misc/NEWS b/Misc/NEWS index 365be5e7d50..9fcd57ea479 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -577,7 +577,8 @@ Library - gettext.translation has an optional fallback argument, and gettext.find an optional all argument. Translations will now fallback - on a per-message basis. + on a per-message basis. The module supports plural forms, by means + of gettext.[d]ngettext and Translation.[u]ngettext. - distutils bdist commands now offer a --skip-build option.