diff --git a/Makefile b/Makefile index 04c36d4..92280cd 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: clean-pyc test test: - @cd tests; python tests.py + @pytest tox-test: @tox diff --git a/flask_babel/__init__.py b/flask_babel/__init__.py index 2664676..7a479fa 100644 --- a/flask_babel/__init__.py +++ b/flask_babel/__init__.py @@ -15,6 +15,7 @@ from contextlib import contextmanager from flask import current_app, request from flask.ctx import has_request_context +from flask.helpers import locked_cached_property from babel import dates, numbers, support, Locale from pytz import timezone, UTC from werkzeug.datastructures import ImmutableDict @@ -185,6 +186,12 @@ def domain(self): """ return self.app.config['BABEL_DOMAIN'] + @locked_cached_property + def domain_instance(self): + """The message domain for the translations. + """ + return Domain(domain=self.app.config['BABEL_DOMAIN']) + @property def translation_directories(self): directories = self.app.config.get( @@ -205,33 +212,7 @@ def get_translations(): object if used outside of the request or if a translation cannot be found. """ - ctx = _get_current_context() - - if ctx is None: - return support.NullTranslations() - - translations = getattr(ctx, 'babel_translations', None) - if translations is None: - translations = support.Translations() - - babel = current_app.extensions['babel'] - for dirname in babel.translation_directories: - catalog = support.Translations.load( - dirname, - [get_locale()], - babel.domain - ) - translations.merge(catalog) - # FIXME: Workaround for merge() being really, really stupid. It - # does not copy _info, plural(), or any other instance variables - # populated by GNUTranslations. We probably want to stop using - # `support.Translations.merge` entirely. - if hasattr(catalog, 'plural'): - translations.plural = catalog.plural - - ctx.babel_translations = translations - - return translations + return get_domain().get_translations() def get_locale(): @@ -472,7 +453,7 @@ def format_number(number): :rtype: unicode """ locale = get_locale() - return numbers.format_number(number, locale=locale) + return numbers.format_decimal(number, locale=locale) def format_decimal(number, format=None): @@ -536,108 +517,167 @@ def format_scientific(number, format=None): return numbers.format_scientific(number, format=format, locale=locale) -def gettext(string, **variables): - """Translates a string with the current locale and passes in the - given keyword arguments as mapping to a string formatting string. +class Domain(object): + """Localization domain. By default will use look for tranlations in Flask + application directory and "messages" domain - all message catalogs should + be called ``messages.mo``. + """ + + def __init__(self, translation_directories=None, domain='messages'): + if isinstance(translation_directories, string_types): + translation_directories = [translation_directories] + self._translation_directories = translation_directories + self.domain = domain + self.cache = {} - :: + def __repr__(self): + return ''.format(self._translation_directories, self.domain) - gettext(u'Hello World!') - gettext(u'Hello %(name)s!', name='World') - """ - t = get_translations() - if t is None: - return string if not variables else string % variables - s = t.ugettext(string) - return s if not variables else s % variables -_ = gettext + @property + def translation_directories(self): + if self._translation_directories is not None: + return self._translation_directories + babel = current_app.extensions['babel'] + return babel.translation_directories + def as_default(self): + """Set this domain as default for the current request""" + ctx = _get_current_context() -def ngettext(singular, plural, num, **variables): - """Translates a string with the current locale and passes in the - given keyword arguments as mapping to a string formatting string. - The `num` parameter is used to dispatch between singular and various - plural forms of the message. It is available in the format string - as ``%(num)d`` or ``%(num)s``. The source language should be - English or a similar language which only has one plural form. + if ctx is None: + raise RuntimeError("No request context") - :: + ctx.babel_domain = self - ngettext(u'%(num)d Apple', u'%(num)d Apples', num=len(apples)) - """ - variables.setdefault('num', num) - t = get_translations() - if t is None: - s = singular if num == 1 else plural - return s if not variables else s % variables + def get_translations_cache(self, ctx): + """Returns dictionary-like object for translation caching""" + return self.cache - s = t.ungettext(singular, plural, num) - return s if not variables else s % variables + def get_translations(self): + ctx = _get_current_context() + if ctx is None: + return support.NullTranslations() -def pgettext(context, string, **variables): - """Like :func:`gettext` but with a context. + cache = self.get_translations_cache(ctx) + locale = get_locale() + try: + return cache[str(locale), self.domain] + except KeyError: + translations = support.Translations() - .. versionadded:: 0.7 - """ - t = get_translations() - if t is None: - return string if not variables else string % variables - s = t.upgettext(context, string) - return s if not variables else s % variables + for dirname in self.translation_directories: + catalog = support.Translations.load( + dirname, + [locale], + self.domain + ) + translations.merge(catalog) + # FIXME: Workaround for merge() being really, really stupid. It + # does not copy _info, plural(), or any other instance variables + # populated by GNUTranslations. We probably want to stop using + # `support.Translations.merge` entirely. + if hasattr(catalog, 'plural'): + translations.plural = catalog.plural + cache[str(locale), self.domain] = translations + return translations -def npgettext(context, singular, plural, num, **variables): - """Like :func:`ngettext` but with a context. + def gettext(self, string, **variables): + """Translates a string with the current locale and passes in the + given keyword arguments as mapping to a string formatting string. - .. versionadded:: 0.7 - """ - variables.setdefault('num', num) - t = get_translations() - if t is None: - s = singular if num == 1 else plural + :: + + gettext(u'Hello World!') + gettext(u'Hello %(name)s!', name='World') + """ + t = self.get_translations() + if t is None: + return string if not variables else string % variables + s = t.ugettext(string) return s if not variables else s % variables - s = t.unpgettext(context, singular, plural, num) - return s if not variables else s % variables + def ngettext(self, singular, plural, num, **variables): + """Translates a string with the current locale and passes in the + given keyword arguments as mapping to a string formatting string. + The `num` parameter is used to dispatch between singular and various + plural forms of the message. It is available in the format string + as ``%(num)d`` or ``%(num)s``. The source language should be + English or a similar language which only has one plural form. -def lazy_gettext(string, **variables): - """Like :func:`gettext` but the string returned is lazy which means - it will be translated when it is used as an actual string. + :: - Example:: + ngettext(u'%(num)d Apple', u'%(num)d Apples', num=len(apples)) + """ + variables.setdefault('num', num) + t = self.get_translations() + if t is None: + s = singular if num == 1 else plural + return s if not variables else s % variables - hello = lazy_gettext(u'Hello World') + s = t.ungettext(singular, plural, num) + return s if not variables else s % variables - @app.route('/') - def index(): - return unicode(hello) - """ - return LazyString(gettext, string, **variables) + def pgettext(self, context, string, **variables): + """Like :func:`gettext` but with a context. + .. versionadded:: 0.7 + """ + t = self.get_translations() + if t is None: + return string if not variables else string % variables + s = t.upgettext(context, string) + return s if not variables else s % variables -def lazy_ngettext(singular, plural, num, **variables): - """Like :func:`ngettext` but the string returned is lazy which means - it will be translated when it is used as an actual string. + def npgettext(self, context, singular, plural, num, **variables): + """Like :func:`ngettext` but with a context. - Example:: + .. versionadded:: 0.7 + """ + variables.setdefault('num', num) + t = self.get_translations() + if t is None: + s = singular if num == 1 else plural + return s if not variables else s % variables + s = t.unpgettext(context, singular, plural, num) + return s if not variables else s % variables - apples = lazy_ngettext(u'%(num)d Apple', u'%(num)d Apples', num=len(apples)) + def lazy_gettext(self, string, **variables): + """Like :func:`gettext` but the string returned is lazy which means + it will be translated when it is used as an actual string. - @app.route('/') - def index(): - return unicode(apples) - """ - return LazyString(ngettext, singular, plural, num, **variables) + Example:: + hello = lazy_gettext(u'Hello World') -def lazy_pgettext(context, string, **variables): - """Like :func:`pgettext` but the string returned is lazy which means - it will be translated when it is used as an actual string. + @app.route('/') + def index(): + return unicode(hello) + """ + return LazyString(self.gettext, string, **variables) - .. versionadded:: 0.7 - """ - return LazyString(pgettext, context, string, **variables) + def lazy_ngettext(self, singular, plural, num, **variables): + """Like :func:`ngettext` but the string returned is lazy which means + it will be translated when it is used as an actual string. + + Example:: + + apples = lazy_ngettext(u'%(num)d Apple', u'%(num)d Apples', num=len(apples)) + + @app.route('/') + def index(): + return unicode(apples) + """ + return LazyString(self.ngettext, singular, plural, num, **variables) + + def lazy_pgettext(self, context, string, **variables): + """Like :func:`pgettext` but the string returned is lazy which means + it will be translated when it is used as an actual string. + + .. versionadded:: 0.7 + """ + return LazyString(self.pgettext, context, string, **variables) def _get_current_context(): @@ -646,3 +686,49 @@ def _get_current_context(): if current_app: return current_app + + +def get_domain(): + ctx = _get_current_context() + if ctx is None: + # this will use NullTranslations + return Domain() + + try: + return ctx.babel_domain + except AttributeError: + pass + + babel = current_app.extensions['babel'] + ctx.babel_domain = babel.domain_instance + return ctx.babel_domain + + +# Create shortcuts for the default Flask domain +def gettext(*args, **kwargs): + return get_domain().gettext(*args, **kwargs) +_ = gettext + + +def ngettext(*args, **kwargs): + return get_domain().ngettext(*args, **kwargs) + + +def pgettext(*args, **kwargs): + return get_domain().pgettext(*args, **kwargs) + + +def npgettext(*args, **kwargs): + return get_domain().npgettext(*args, **kwargs) + + +def lazy_gettext(*args, **kwargs): + return LazyString(gettext, *args, **kwargs) + + +def lazy_pgettext(*args, **kwargs): + return LazyString(pgettext, *args, **kwargs) + + +def lazy_ngettext(*args, **kwargs): + return LazyString(ngettext, *args, **kwargs) diff --git a/flask_babel/speaklater.py b/flask_babel/speaklater.py index 489f73b..206ed84 100644 --- a/flask_babel/speaklater.py +++ b/flask_babel/speaklater.py @@ -74,4 +74,4 @@ def __mod__(self, other): return text_type(self) % other def __rmod__(self, other): - return other + text_type(self) \ No newline at end of file + return other + text_type(self) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..29e9bf1 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = + tests/ diff --git a/setup.py b/setup.py index f696bf1..0133fb9 100644 --- a/setup.py +++ b/setup.py @@ -37,5 +37,11 @@ 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' - ] + ], + extras_require={ + 'dev': [ + 'pytest', + 'pytest-mock' + ] + } ) diff --git a/tests/renamed_translations/de/LC_MESSAGES/myapp.mo b/tests/renamed_translations/de/LC_MESSAGES/myapp.mo new file mode 100644 index 0000000..041aa3c Binary files /dev/null and b/tests/renamed_translations/de/LC_MESSAGES/myapp.mo differ diff --git a/tests/renamed_translations/de/LC_MESSAGES/myapp.po b/tests/renamed_translations/de/LC_MESSAGES/myapp.po new file mode 100644 index 0000000..6d6ea80 --- /dev/null +++ b/tests/renamed_translations/de/LC_MESSAGES/myapp.po @@ -0,0 +1,35 @@ +# German translations for PROJECT. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-05-29 17:00+0200\n" +"PO-Revision-Date: 2010-05-30 12:56+0200\n" +"Last-Translator: Armin Ronacher \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: tests.py:94 +#, python-format +msgid "Hello %(name)s!" +msgstr "Hallo %(name)s!" + +#: tests.py:95 tests.py:96 +#, python-format +msgid "%(num)s Apple" +msgid_plural "%(num)s Apples" +msgstr[0] "%(num)s Apfel" +msgstr[1] "%(num)s Äpfel" + +#: tests.py:119 +msgid "Yes" +msgstr "Ja" + diff --git a/tests/test_date_formatting.py b/tests/test_date_formatting.py new file mode 100644 index 0000000..83f1736 --- /dev/null +++ b/tests/test_date_formatting.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement + +from datetime import datetime, timedelta +from threading import Semaphore, Thread + +import flask + +import flask_babel as babel + + +def test_basics(): + app = flask.Flask(__name__) + babel.Babel(app) + d = datetime(2010, 4, 12, 13, 46) + delta = timedelta(days=6) + + with app.test_request_context(): + assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' + assert babel.format_date(d) == 'Apr 12, 2010' + assert babel.format_time(d) == '1:46:00 PM' + assert babel.format_timedelta(delta) == '1 week' + assert babel.format_timedelta(delta, threshold=1) == '6 days' + + with app.test_request_context(): + app.config['BABEL_DEFAULT_TIMEZONE'] = 'Europe/Vienna' + assert babel.format_datetime(d) == 'Apr 12, 2010, 3:46:00 PM' + assert babel.format_date(d) == 'Apr 12, 2010' + assert babel.format_time(d) == '3:46:00 PM' + + with app.test_request_context(): + app.config['BABEL_DEFAULT_LOCALE'] = 'de_DE' + assert babel.format_datetime(d, 'long') == \ + '12. April 2010 um 15:46:00 MESZ' + + +def test_init_app(): + b = babel.Babel() + app = flask.Flask(__name__) + b.init_app(app) + d = datetime(2010, 4, 12, 13, 46) + + with app.test_request_context(): + assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' + assert babel.format_date(d) == 'Apr 12, 2010' + assert babel.format_time(d) == '1:46:00 PM' + + with app.test_request_context(): + app.config['BABEL_DEFAULT_TIMEZONE'] = 'Europe/Vienna' + assert babel.format_datetime(d) == 'Apr 12, 2010, 3:46:00 PM' + assert babel.format_date(d) == 'Apr 12, 2010' + assert babel.format_time(d) == '3:46:00 PM' + + with app.test_request_context(): + app.config['BABEL_DEFAULT_LOCALE'] = 'de_DE' + assert babel.format_datetime(d, 'long') == \ + '12. April 2010 um 15:46:00 MESZ' + + +def test_custom_formats(): + app = flask.Flask(__name__) + app.config.update( + BABEL_DEFAULT_LOCALE='en_US', + BABEL_DEFAULT_TIMEZONE='Pacific/Johnston' + ) + b = babel.Babel(app) + b.date_formats['datetime'] = 'long' + b.date_formats['datetime.long'] = 'MMMM d, yyyy h:mm:ss a' + d = datetime(2010, 4, 12, 13, 46) + + with app.test_request_context(): + assert babel.format_datetime(d) == 'April 12, 2010 3:46:00 AM' + + +def test_custom_locale_selector(): + app = flask.Flask(__name__) + b = babel.Babel(app) + d = datetime(2010, 4, 12, 13, 46) + + the_timezone = 'UTC' + the_locale = 'en_US' + + @b.localeselector + def select_locale(): + return the_locale + + @b.timezoneselector + def select_timezone(): + return the_timezone + + with app.test_request_context(): + assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' + + the_locale = 'de_DE' + the_timezone = 'Europe/Vienna' + + with app.test_request_context(): + assert babel.format_datetime(d) == '12.04.2010, 15:46:00' + + +def test_refreshing(): + app = flask.Flask(__name__) + babel.Babel(app) + d = datetime(2010, 4, 12, 13, 46) + with app.test_request_context(): + assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' + app.config['BABEL_DEFAULT_TIMEZONE'] = 'Europe/Vienna' + babel.refresh() + assert babel.format_datetime(d) == 'Apr 12, 2010, 3:46:00 PM' + + +def test_force_locale(): + app = flask.Flask(__name__) + b = babel.Babel(app) + + @b.localeselector + def select_locale(): + return 'de_DE' + + with app.test_request_context(): + assert str(babel.get_locale()) == 'de_DE' + with babel.force_locale('en_US'): + assert str(babel.get_locale()) == 'en_US' + assert str(babel.get_locale()) == 'de_DE' + + +def test_force_locale_with_threading(): + app = flask.Flask(__name__) + b = babel.Babel(app) + + @b.localeselector + def select_locale(): + return 'de_DE' + + semaphore = Semaphore(value=0) + + def first_request(): + with app.test_request_context(): + with babel.force_locale('en_US'): + assert str(babel.get_locale()) == 'en_US' + semaphore.acquire() + + thread = Thread(target=first_request) + thread.start() + + try: + with app.test_request_context(): + assert str(babel.get_locale()) == 'de_DE' + finally: + semaphore.release() + thread.join() + + +def test_refresh_during_force_locale(): + app = flask.Flask(__name__) + b = babel.Babel(app) + + @b.localeselector + def select_locale(): + return 'de_DE' + + with app.test_request_context(): + with babel.force_locale('en_US'): + assert str(babel.get_locale()) == 'en_US' + babel.refresh() + assert str(babel.get_locale()) == 'en_US' diff --git a/tests/test_gettext.py b/tests/test_gettext.py new file mode 100644 index 0000000..b8b65a3 --- /dev/null +++ b/tests/test_gettext.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement + +import flask + +import flask_babel as babel +from flask_babel import gettext, lazy_gettext, lazy_ngettext, ngettext +from flask_babel._compat import text_type + + +def test_basics(): + app = flask.Flask(__name__) + babel.Babel(app, default_locale='de_DE') + + with app.test_request_context(): + assert gettext(u'Hello %(name)s!', name='Peter') == 'Hallo Peter!' + assert ngettext(u'%(num)s Apple', u'%(num)s Apples', 3) == \ + u'3 Äpfel' + assert ngettext(u'%(num)s Apple', u'%(num)s Apples', 1) == \ + u'1 Apfel' + + +def test_template_basics(): + app = flask.Flask(__name__) + babel.Babel(app, default_locale='de_DE') + + t = lambda x: flask.render_template_string('{{ %s }}' % x) + + with app.test_request_context(): + assert t("gettext('Hello %(name)s!', name='Peter')") == \ + u'Hallo Peter!' + assert t("ngettext('%(num)s Apple', '%(num)s Apples', 3)") == \ + u'3 Äpfel' + assert t("ngettext('%(num)s Apple', '%(num)s Apples', 1)") == \ + u'1 Apfel' + assert flask.render_template_string(''' + {% trans %}Hello {{ name }}!{% endtrans %} + ''', name='Peter').strip() == 'Hallo Peter!' + assert flask.render_template_string(''' + {% trans num=3 %}{{ num }} Apple + {%- pluralize %}{{ num }} Apples{% endtrans %} + ''', name='Peter').strip() == u'3 Äpfel' + + +def test_lazy_gettext(): + app = flask.Flask(__name__) + babel.Babel(app, default_locale='de_DE') + yes = lazy_gettext(u'Yes') + with app.test_request_context(): + assert text_type(yes) == 'Ja' + assert yes.__html__() == 'Ja' + app.config['BABEL_DEFAULT_LOCALE'] = 'en_US' + with app.test_request_context(): + assert text_type(yes) == 'Yes' + assert yes.__html__() == 'Yes' + + +def test_lazy_ngettext(): + app = flask.Flask(__name__) + babel.Babel(app, default_locale='de_DE') + one_apple = lazy_ngettext(u'%(num)s Apple', u'%(num)s Apples', 1) + with app.test_request_context(): + assert text_type(one_apple) == '1 Apfel' + assert one_apple.__html__() == '1 Apfel' + two_apples = lazy_ngettext(u'%(num)s Apple', u'%(num)s Apples', 2) + with app.test_request_context(): + assert text_type(two_apples) == u'2 Äpfel' + assert two_apples.__html__() == u'2 Äpfel' + + +def test_lazy_gettext_defaultdomain(): + app = flask.Flask(__name__) + b = babel.Babel(app, default_locale='de_DE', default_domain='test') + first = lazy_gettext('first') + with app.test_request_context(): + assert text_type(first) == 'erste' + app.config['BABEL_DEFAULT_LOCALE'] = 'en_US' + with app.test_request_context(): + assert text_type(first) == 'first' + + +def test_list_translations(): + app = flask.Flask(__name__) + b = babel.Babel(app, default_locale='de_DE') + translations = b.list_translations() + assert len(translations) == 1 + assert str(translations[0]) == 'de' + + +def test_no_formatting(): + """ + Ensure we don't format strings unless a variable is passed. + """ + app = flask.Flask(__name__) + babel.Babel(app) + + with app.test_request_context(): + assert gettext(u'Test %s') == u'Test %s' + assert gettext(u'Test %(name)s', name=u'test') == u'Test test' + assert gettext(u'Test %s') % 'test' == u'Test test' + + +def test_domain(): + app = flask.Flask(__name__) + b = babel.Babel(app, default_locale='de_DE') + domain = babel.Domain(domain='test') + + with app.test_request_context(): + assert domain.gettext('first') == 'erste' + assert babel.gettext('first') == 'first' + + +def test_as_default(): + app = flask.Flask(__name__) + b = babel.Babel(app, default_locale='de_DE') + domain = babel.Domain(domain='test') + + with app.test_request_context(): + assert babel.gettext('first') == 'first' + domain.as_default() + assert babel.gettext('first') == 'erste' + + +def test_default_domain(): + app = flask.Flask(__name__) + b = babel.Babel(app, default_locale='de_DE', default_domain='test') + + with app.test_request_context(): + assert babel.gettext('first') == 'erste' + + +def test_multiple_apps(): + app1 = flask.Flask(__name__) + b1 = babel.Babel(app1, default_locale='de_DE') + + app2 = flask.Flask(__name__) + b2 = babel.Babel(app2, default_locale='de_DE') + + with app1.test_request_context() as ctx: + assert babel.gettext('Yes') == 'Ja' + + assert ('de_DE', 'messages') in b1.domain_instance.get_translations_cache(ctx) + + with app2.test_request_context() as ctx: + assert 'de_DE', 'messages' not in b2.domain_instance.get_translations_cache(ctx) + + +def test_cache(mocker): + load_mock = mocker.patch( + "babel.support.Translations.load", side_effect=babel.support.Translations.load + ) + + app = flask.Flask(__name__) + b = babel.Babel(app, default_locale="de_DE") + + @b.localeselector + def select_locale(): + return the_locale + + # first request, should load en_US + the_locale = "en_US" + with app.test_request_context() as ctx: + assert b.domain_instance.get_translations_cache(ctx) == {} + assert babel.gettext("Yes") == "Yes" + assert load_mock.call_count == 1 + + # second request, should use en_US from cache + with app.test_request_context() as ctx: + assert set(b.domain_instance.get_translations_cache(ctx)) == { + ("en_US", "messages") + } + assert babel.gettext("Yes") == "Yes" + assert load_mock.call_count == 1 + + # third request, should load de_DE from cache + the_locale = "de_DE" + with app.test_request_context() as ctx: + assert set(b.domain_instance.get_translations_cache(ctx)) == { + ("en_US", "messages") + } + assert babel.gettext("Yes") == "Ja" + assert load_mock.call_count == 2 + + # now everything is cached, so no more loads should happen! + the_locale = "en_US" + with app.test_request_context() as ctx: + assert set(b.domain_instance.get_translations_cache(ctx)) == { + ("en_US", "messages"), + ("de_DE", "messages"), + } + assert babel.gettext("Yes") == "Yes" + assert load_mock.call_count == 2 + + the_locale = "de_DE" + with app.test_request_context() as ctx: + assert set(b.domain_instance.get_translations_cache(ctx)) == { + ("en_US", "messages"), + ("de_DE", "messages"), + } + assert babel.gettext("Yes") == "Ja" + assert load_mock.call_count == 2 diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..2cd6f6c --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement + +import pickle + +import flask +from babel.support import NullTranslations + +import flask_babel as babel +from flask_babel import get_translations, gettext, lazy_gettext + + +def test_no_request_context(): + b = babel.Babel() + app = flask.Flask(__name__) + b.init_app(app) + + with app.app_context(): + assert isinstance(get_translations(), NullTranslations) + + +def test_multiple_directories(): + """ + Ensure we can load translations from multiple directories. + """ + b = babel.Babel() + app = flask.Flask(__name__) + + app.config.update({ + 'BABEL_TRANSLATION_DIRECTORIES': ';'.join(( + 'translations', + 'renamed_translations' + )), + 'BABEL_DEFAULT_LOCALE': 'de_DE' + }) + + b.init_app(app) + + with app.test_request_context(): + translations = b.list_translations() + + assert(len(translations) == 2) + assert(str(translations[0]) == 'de') + assert(str(translations[1]) == 'de') + + assert gettext( + u'Hello %(name)s!', + name='Peter' + ) == 'Hallo Peter!' + + +def test_multiple_directories_different_domain(): + """ + Ensure we can load translations from multiple directories with a + custom domain. + """ + b = babel.Babel() + app = flask.Flask(__name__) + + app.config.update({ + 'BABEL_TRANSLATION_DIRECTORIES': ';'.join(( + 'translations_different_domain', + 'renamed_translations' + )), + 'BABEL_DEFAULT_LOCALE': 'de_DE', + 'BABEL_DOMAIN': 'myapp' + }) + + b.init_app(app) + + with app.test_request_context(): + translations = b.list_translations() + + assert(len(translations) == 2) + assert(str(translations[0]) == 'de') + assert(str(translations[1]) == 'de') + + assert gettext( + u'Hello %(name)s!', + name='Peter' + ) == 'Hallo Peter!' + assert gettext(u'Good bye') == 'Auf Wiedersehen' + + +def test_different_domain(): + """ + Ensure we can load translations from multiple directories. + """ + b = babel.Babel() + app = flask.Flask(__name__) + + app.config.update({ + 'BABEL_TRANSLATION_DIRECTORIES': 'translations_different_domain', + 'BABEL_DEFAULT_LOCALE': 'de_DE', + 'BABEL_DOMAIN': 'myapp' + }) + + b.init_app(app) + + with app.test_request_context(): + translations = b.list_translations() + + assert(len(translations) == 1) + assert(str(translations[0]) == 'de') + + assert gettext(u'Good bye') == 'Auf Wiedersehen' + + +def test_lazy_old_style_formatting(): + lazy_string = lazy_gettext(u'Hello %(name)s') + assert lazy_string % {u'name': u'test'} == u'Hello test' + + lazy_string = lazy_gettext(u'test') + assert u'Hello %s' % lazy_string == u'Hello test' + + +def test_lazy_pickling(): + lazy_string = lazy_gettext(u'Foo') + pickled = pickle.dumps(lazy_string) + unpickled = pickle.loads(pickled) + + assert unpickled == lazy_string diff --git a/tests/test_number_formatting.py b/tests/test_number_formatting.py new file mode 100644 index 0000000..bd59617 --- /dev/null +++ b/tests/test_number_formatting.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement + +from decimal import Decimal + +import flask + +import flask_babel as babel + + +def test_basics(): + app = flask.Flask(__name__) + babel.Babel(app) + n = 1099 + + with app.test_request_context(): + assert babel.format_number(n) == u'1,099' + assert babel.format_decimal(Decimal('1010.99')) == u'1,010.99' + assert babel.format_currency(n, 'USD') == '$1,099.00' + assert babel.format_percent(0.19) == '19%' + assert babel.format_scientific(10000) == u'1E4' diff --git a/tests/tests.py b/tests/tests.py deleted file mode 100644 index aefba35..0000000 --- a/tests/tests.py +++ /dev/null @@ -1,343 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import with_statement - -import sys -import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -import pickle -from threading import Thread, Semaphore - -import unittest -from decimal import Decimal -import flask -from datetime import datetime, timedelta -import flask_babel as babel -from flask_babel import gettext, ngettext, lazy_gettext, lazy_ngettext, get_translations -from babel.support import NullTranslations -from flask_babel._compat import text_type - - -class IntegrationTestCase(unittest.TestCase): - def test_no_request_context(self): - b = babel.Babel() - app = flask.Flask(__name__) - b.init_app(app) - - with app.app_context(): - assert isinstance(get_translations(), NullTranslations) - - def test_multiple_directories(self): - """ - Ensure we can load translations from multiple directories. - """ - b = babel.Babel() - app = flask.Flask(__name__) - - app.config.update({ - 'BABEL_TRANSLATION_DIRECTORIES': ';'.join(( - 'translations', - 'renamed_translations' - )), - 'BABEL_DEFAULT_LOCALE': 'de_DE' - }) - - b.init_app(app) - - with app.test_request_context(): - translations = b.list_translations() - - assert(len(translations) == 2) - assert(str(translations[0]) == 'de') - assert(str(translations[1]) == 'de') - - assert gettext( - u'Hello %(name)s!', - name='Peter' - ) == 'Hallo Peter!' - - def test_different_domain(self): - """ - Ensure we can load translations from multiple directories. - """ - b = babel.Babel() - app = flask.Flask(__name__) - - app.config.update({ - 'BABEL_TRANSLATION_DIRECTORIES': 'translations_different_domain', - 'BABEL_DEFAULT_LOCALE': 'de_DE', - 'BABEL_DOMAIN': 'myapp' - }) - - b.init_app(app) - - with app.test_request_context(): - translations = b.list_translations() - - assert(len(translations) == 1) - assert(str(translations[0]) == 'de') - - assert gettext(u'Good bye') == 'Auf Wiedersehen' - - def test_lazy_old_style_formatting(self): - lazy_string = lazy_gettext(u'Hello %(name)s') - assert lazy_string % {u'name': u'test'} == u'Hello test' - - lazy_string = lazy_gettext(u'test') - assert u'Hello %s' % lazy_string == u'Hello test' - - def test_lazy_pickling(self): - lazy_string = lazy_gettext(u'Foo') - pickled = pickle.dumps(lazy_string) - unpickled = pickle.loads(pickled) - - assert unpickled == lazy_string - - -class DateFormattingTestCase(unittest.TestCase): - - def test_basics(self): - app = flask.Flask(__name__) - babel.Babel(app) - d = datetime(2010, 4, 12, 13, 46) - delta = timedelta(days=6) - - with app.test_request_context(): - assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' - assert babel.format_date(d) == 'Apr 12, 2010' - assert babel.format_time(d) == '1:46:00 PM' - assert babel.format_timedelta(delta) == '1 week' - assert babel.format_timedelta(delta, threshold=1) == '6 days' - - with app.test_request_context(): - app.config['BABEL_DEFAULT_TIMEZONE'] = 'Europe/Vienna' - assert babel.format_datetime(d) == 'Apr 12, 2010, 3:46:00 PM' - assert babel.format_date(d) == 'Apr 12, 2010' - assert babel.format_time(d) == '3:46:00 PM' - - with app.test_request_context(): - app.config['BABEL_DEFAULT_LOCALE'] = 'de_DE' - assert babel.format_datetime(d, 'long') == \ - '12. April 2010 um 15:46:00 MESZ' - - def test_init_app(self): - b = babel.Babel() - app = flask.Flask(__name__) - b.init_app(app) - d = datetime(2010, 4, 12, 13, 46) - - with app.test_request_context(): - assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' - assert babel.format_date(d) == 'Apr 12, 2010' - assert babel.format_time(d) == '1:46:00 PM' - - with app.test_request_context(): - app.config['BABEL_DEFAULT_TIMEZONE'] = 'Europe/Vienna' - assert babel.format_datetime(d) == 'Apr 12, 2010, 3:46:00 PM' - assert babel.format_date(d) == 'Apr 12, 2010' - assert babel.format_time(d) == '3:46:00 PM' - - with app.test_request_context(): - app.config['BABEL_DEFAULT_LOCALE'] = 'de_DE' - assert babel.format_datetime(d, 'long') == \ - '12. April 2010 um 15:46:00 MESZ' - - def test_custom_formats(self): - app = flask.Flask(__name__) - app.config.update( - BABEL_DEFAULT_LOCALE='en_US', - BABEL_DEFAULT_TIMEZONE='Pacific/Johnston' - ) - b = babel.Babel(app) - b.date_formats['datetime'] = 'long' - b.date_formats['datetime.long'] = 'MMMM d, yyyy h:mm:ss a' - d = datetime(2010, 4, 12, 13, 46) - - with app.test_request_context(): - assert babel.format_datetime(d) == 'April 12, 2010 3:46:00 AM' - - def test_custom_locale_selector(self): - app = flask.Flask(__name__) - b = babel.Babel(app) - d = datetime(2010, 4, 12, 13, 46) - - the_timezone = 'UTC' - the_locale = 'en_US' - - @b.localeselector - def select_locale(): - return the_locale - - @b.timezoneselector - def select_timezone(): - return the_timezone - - with app.test_request_context(): - assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' - - the_locale = 'de_DE' - the_timezone = 'Europe/Vienna' - - with app.test_request_context(): - assert babel.format_datetime(d) == '12.04.2010, 15:46:00' - - def test_refreshing(self): - app = flask.Flask(__name__) - babel.Babel(app) - d = datetime(2010, 4, 12, 13, 46) - with app.test_request_context(): - assert babel.format_datetime(d) == 'Apr 12, 2010, 1:46:00 PM' - app.config['BABEL_DEFAULT_TIMEZONE'] = 'Europe/Vienna' - babel.refresh() - assert babel.format_datetime(d) == 'Apr 12, 2010, 3:46:00 PM' - - def test_force_locale(self): - app = flask.Flask(__name__) - b = babel.Babel(app) - - @b.localeselector - def select_locale(): - return 'de_DE' - - with app.test_request_context(): - assert str(babel.get_locale()) == 'de_DE' - with babel.force_locale('en_US'): - assert str(babel.get_locale()) == 'en_US' - assert str(babel.get_locale()) == 'de_DE' - - def test_force_locale_with_threading(self): - app = flask.Flask(__name__) - b = babel.Babel(app) - - @b.localeselector - def select_locale(): - return 'de_DE' - - semaphore = Semaphore(value=0) - - def first_request(): - with app.test_request_context(): - with babel.force_locale('en_US'): - assert str(babel.get_locale()) == 'en_US' - semaphore.acquire() - - thread = Thread(target=first_request) - thread.start() - - try: - with app.test_request_context(): - assert str(babel.get_locale()) == 'de_DE' - finally: - semaphore.release() - thread.join() - - def test_refresh_during_force_locale(self): - app = flask.Flask(__name__) - b = babel.Babel(app) - - @b.localeselector - def select_locale(): - return 'de_DE' - - with app.test_request_context(): - with babel.force_locale('en_US'): - assert str(babel.get_locale()) == 'en_US' - babel.refresh() - assert str(babel.get_locale()) == 'en_US' - - -class NumberFormattingTestCase(unittest.TestCase): - - def test_basics(self): - app = flask.Flask(__name__) - babel.Babel(app) - n = 1099 - - with app.test_request_context(): - assert babel.format_number(n) == u'1,099' - assert babel.format_decimal(Decimal('1010.99')) == u'1,010.99' - assert babel.format_currency(n, 'USD') == '$1,099.00' - assert babel.format_percent(0.19) == '19%' - assert babel.format_scientific(10000) == u'1E4' - - -class GettextTestCase(unittest.TestCase): - - def test_basics(self): - app = flask.Flask(__name__) - babel.Babel(app, default_locale='de_DE') - - with app.test_request_context(): - assert gettext(u'Hello %(name)s!', name='Peter') == 'Hallo Peter!' - assert ngettext(u'%(num)s Apple', u'%(num)s Apples', 3) == \ - u'3 Äpfel' - assert ngettext(u'%(num)s Apple', u'%(num)s Apples', 1) == \ - u'1 Apfel' - - def test_template_basics(self): - app = flask.Flask(__name__) - babel.Babel(app, default_locale='de_DE') - - t = lambda x: flask.render_template_string('{{ %s }}' % x) - - with app.test_request_context(): - assert t("gettext('Hello %(name)s!', name='Peter')") == \ - u'Hallo Peter!' - assert t("ngettext('%(num)s Apple', '%(num)s Apples', 3)") == \ - u'3 Äpfel' - assert t("ngettext('%(num)s Apple', '%(num)s Apples', 1)") == \ - u'1 Apfel' - assert flask.render_template_string(''' - {% trans %}Hello {{ name }}!{% endtrans %} - ''', name='Peter').strip() == 'Hallo Peter!' - assert flask.render_template_string(''' - {% trans num=3 %}{{ num }} Apple - {%- pluralize %}{{ num }} Apples{% endtrans %} - ''', name='Peter').strip() == u'3 Äpfel' - - def test_lazy_gettext(self): - app = flask.Flask(__name__) - babel.Babel(app, default_locale='de_DE') - yes = lazy_gettext(u'Yes') - with app.test_request_context(): - assert text_type(yes) == 'Ja' - assert yes.__html__() == 'Ja' - app.config['BABEL_DEFAULT_LOCALE'] = 'en_US' - with app.test_request_context(): - assert text_type(yes) == 'Yes' - assert yes.__html__() == 'Yes' - - def test_lazy_ngettext(self): - app = flask.Flask(__name__) - babel.Babel(app, default_locale='de_DE') - one_apple = lazy_ngettext(u'%(num)s Apple', u'%(num)s Apples', 1) - with app.test_request_context(): - assert text_type(one_apple) == '1 Apfel' - assert one_apple.__html__() == '1 Apfel' - two_apples = lazy_ngettext(u'%(num)s Apple', u'%(num)s Apples', 2) - with app.test_request_context(): - assert text_type(two_apples) == u'2 Äpfel' - assert two_apples.__html__() == u'2 Äpfel' - - def test_list_translations(self): - app = flask.Flask(__name__) - b = babel.Babel(app, default_locale='de_DE') - translations = b.list_translations() - assert len(translations) == 1 - assert str(translations[0]) == 'de' - - def test_no_formatting(self): - """ - Ensure we don't format strings unless a variable is passed. - """ - app = flask.Flask(__name__) - babel.Babel(app) - - with app.test_request_context(): - assert gettext(u'Test %s') == u'Test %s' - assert gettext(u'Test %(name)s', name=u'test') == u'Test test' - assert gettext(u'Test %s') % 'test' == u'Test test' - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/translations/de/LC_MESSAGES/test.mo b/tests/translations/de/LC_MESSAGES/test.mo new file mode 100644 index 0000000..4ccc9e7 Binary files /dev/null and b/tests/translations/de/LC_MESSAGES/test.mo differ diff --git a/tests/translations/de/LC_MESSAGES/test.po b/tests/translations/de/LC_MESSAGES/test.po new file mode 100644 index 0000000..4fd47ec --- /dev/null +++ b/tests/translations/de/LC_MESSAGES/test.po @@ -0,0 +1,23 @@ +# Translations template for PROJECT. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-05-30 12:56+0200\n" +"PO-Revision-Date: 2012-04-11 15:18+0200\n" +"Last-Translator: Serge S. Koval \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: tests.py:94 +#, python-format +msgid "first" +msgstr "erste" + diff --git a/tox.ini b/tox.ini index dd25176..200c161 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,10 @@ [tox] -envlist = py27, py36 +envlist = py{38,37,36,27} [testenv] -deps = pytz>=2013a +deps = + pytz>=2013a + pytest + pytest-mock whitelist_externals = make -commands = make test +commands = pytest