Refactored DjangoTranslation class

Also fixes #18192 and #21055.
This commit is contained in:
Doug Beck 2014-04-30 18:04:30 +02:00 committed by Claude Paroz
parent 7c54f8cced
commit a5f6cbce07
3 changed files with 108 additions and 96 deletions

View File

@ -104,6 +104,7 @@ answer newbie questions, and generally made Django that much better:
Batman Batman
Oliver Beattie <oliver@obeattie.com> Oliver Beattie <oliver@obeattie.com>
Brian Beck <http://blog.brianbeck.com/> Brian Beck <http://blog.brianbeck.com/>
Doug Beck <doug@douglasbeck.com>
Shannon -jj Behrens <http://jjinux.blogspot.com/> Shannon -jj Behrens <http://jjinux.blogspot.com/>
Esdras Beleza <linux@esdrasbeleza.com> Esdras Beleza <linux@esdrasbeleza.com>
Božidar Benko <bbenko@gmail.com> Božidar Benko <bbenko@gmail.com>

View File

@ -10,6 +10,7 @@ from threading import local
import warnings import warnings
from django.apps import apps from django.apps import apps
from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.test.signals import setting_changed from django.test.signals import setting_changed
from django.utils.deprecation import RemovedInDjango19Warning from django.utils.deprecation import RemovedInDjango19Warning
@ -101,107 +102,103 @@ class DjangoTranslation(gettext_module.GNUTranslations):
""" """
This class sets up the GNUTranslations context with regard to output This class sets up the GNUTranslations context with regard to output
charset. charset.
This translation object will be constructed out of multiple GNUTranslations
objects by merging their catalogs. It will construct an object for the
requested language and add a fallback to the default language, if it's
different from the requested language.
""" """
def __init__(self, *args, **kw): def __init__(self, language):
gettext_module.GNUTranslations.__init__(self, *args, **kw) """Create a GNUTranslations() using many locale directories"""
self.set_output_charset('utf-8') gettext_module.GNUTranslations.__init__(self)
self.__language = '??'
def merge(self, other):
self._catalog.update(other._catalog)
def set_language(self, language):
self.__language = language self.__language = language
self.__to_language = to_language(language) self.__to_language = to_language(language)
self.__locale = to_locale(language)
self.plural = lambda n: int(n != 1)
def language(self): self._init_translation_catalog()
return self.__language self._add_installed_apps_translations()
self._add_local_translations()
def to_language(self): self._add_fallback()
return self.__to_language
def __repr__(self): def __repr__(self):
return "<DjangoTranslation lang:%s>" % self.__language return "<DjangoTranslation lang:%s>" % self.__language
def _new_gnu_trans(self, localedir, use_null_fallback=True):
"""
Returns a mergeable gettext.GNUTranslations instance.
A convenience wrapper. By default gettext uses 'fallback=False'.
Using param `use_null_fallback` to avoid confusion with any other
references to 'fallback'.
"""
translation = gettext_module.translation(
domain='django',
localedir=localedir,
languages=[self.__locale],
codeset='utf-8',
fallback=use_null_fallback)
if not hasattr(translation, '_catalog'):
# provides merge support for NullTranslations()
translation._catalog = {}
translation._info = {}
return translation
def _init_translation_catalog(self):
"""Creates a base catalog using global django translations."""
settingsfile = upath(sys.modules[settings.__module__].__file__)
localedir = os.path.join(os.path.dirname(settingsfile), 'locale')
use_null_fallback = True
if self.__language == settings.LANGUAGE_CODE:
# default lang should be present and parseable, if not
# gettext will raise an IOError (refs #18192).
use_null_fallback = False
translation = self._new_gnu_trans(localedir, use_null_fallback)
self._info = translation._info.copy()
self._catalog = translation._catalog.copy()
def _add_installed_apps_translations(self):
"""Merges translations from each installed app."""
for app_config in reversed(list(apps.get_app_configs())):
localedir = os.path.join(app_config.path, 'locale')
translation = self._new_gnu_trans(localedir)
self.merge(translation)
def _add_local_translations(self):
"""Merges translations defined in LOCALE_PATHS."""
for localedir in reversed(settings.LOCALE_PATHS):
translation = self._new_gnu_trans(localedir)
self.merge(translation)
def _add_fallback(self):
"""Sets the GNUTranslations() fallback with the default language."""
if self.__language == settings.LANGUAGE_CODE:
return
default_translation = translation(settings.LANGUAGE_CODE)
self.add_fallback(default_translation)
def merge(self, other):
"""Merge another translation into this catalog."""
self._catalog.update(other._catalog)
def language(self):
"""Returns the translation language."""
return self.__language
def to_language(self):
"""Returns the translation language name."""
return self.__to_language
def translation(language): def translation(language):
""" """
Returns a translation object. Returns a translation object.
This translation object will be constructed out of multiple GNUTranslations
objects by merging their catalogs. It will construct a object for the
requested language and add a fallback to the default language, if it's
different from the requested language.
""" """
global _translations global _translations
if not language in _translations:
t = _translations.get(language, None) _translations[language] = DjangoTranslation(language)
if t is not None: return _translations[language]
return t
from django.conf import settings
globalpath = os.path.join(os.path.dirname(upath(sys.modules[settings.__module__].__file__)), 'locale')
def _fetch(lang, fallback=None):
global _translations
res = _translations.get(lang, None)
if res is not None:
return res
loc = to_locale(lang)
def _translation(path):
try:
t = gettext_module.translation('django', path, [loc], DjangoTranslation)
t.set_language(lang)
return t
except IOError:
return None
res = _translation(globalpath)
# We want to ensure that, for example, "en-gb" and "en-us" don't share
# the same translation object (thus, merging en-us with a local update
# doesn't affect en-gb), even though they will both use the core "en"
# translation. So we have to subvert Python's internal gettext caching.
base_lang = lambda x: x.split('-', 1)[0]
if any(base_lang(lang) == base_lang(trans) for trans in _translations):
res._info = res._info.copy()
res._catalog = res._catalog.copy()
def _merge(path):
t = _translation(path)
if t is not None:
if res is None:
return t
else:
res.merge(t)
return res
for app_config in reversed(list(apps.get_app_configs())):
apppath = os.path.join(app_config.path, 'locale')
if os.path.isdir(apppath):
res = _merge(apppath)
for localepath in reversed(settings.LOCALE_PATHS):
if os.path.isdir(localepath):
res = _merge(localepath)
if res is None:
if fallback is not None:
res = fallback
else:
return gettext_module.NullTranslations()
_translations[lang] = res
return res
default_translation = _fetch(settings.LANGUAGE_CODE)
current_translation = _fetch(language, fallback=default_translation)
return current_translation
def activate(language): def activate(language):
@ -244,7 +241,6 @@ def get_language():
except AttributeError: except AttributeError:
pass pass
# If we don't have a real translation object, assume it's the default language. # If we don't have a real translation object, assume it's the default language.
from django.conf import settings
return settings.LANGUAGE_CODE return settings.LANGUAGE_CODE
@ -255,8 +251,6 @@ def get_language_bidi():
* False = left-to-right layout * False = left-to-right layout
* True = right-to-left layout * True = right-to-left layout
""" """
from django.conf import settings
base_lang = get_language().split('-')[0] base_lang = get_language().split('-')[0]
return base_lang in settings.LANGUAGES_BIDI return base_lang in settings.LANGUAGES_BIDI
@ -273,7 +267,6 @@ def catalog():
if t is not None: if t is not None:
return t return t
if _default is None: if _default is None:
from django.conf import settings
_default = translation(settings.LANGUAGE_CODE) _default = translation(settings.LANGUAGE_CODE)
return _default return _default
@ -294,7 +287,6 @@ def do_translate(message, translation_function):
result = getattr(t, translation_function)(eol_message) result = getattr(t, translation_function)(eol_message)
else: else:
if _default is None: if _default is None:
from django.conf import settings
_default = translation(settings.LANGUAGE_CODE) _default = translation(settings.LANGUAGE_CODE)
result = getattr(_default, translation_function)(eol_message) result = getattr(_default, translation_function)(eol_message)
if isinstance(message, SafeData): if isinstance(message, SafeData):
@ -343,7 +335,6 @@ def do_ntranslate(singular, plural, number, translation_function):
if t is not None: if t is not None:
return getattr(t, translation_function)(singular, plural, number) return getattr(t, translation_function)(singular, plural, number)
if _default is None: if _default is None:
from django.conf import settings
_default = translation(settings.LANGUAGE_CODE) _default = translation(settings.LANGUAGE_CODE)
return getattr(_default, translation_function)(singular, plural, number) return getattr(_default, translation_function)(singular, plural, number)
@ -383,7 +374,6 @@ def all_locale_paths():
""" """
Returns a list of paths to user-provides languages files. Returns a list of paths to user-provides languages files.
""" """
from django.conf import settings
globalpath = os.path.join( globalpath = os.path.join(
os.path.dirname(upath(sys.modules[settings.__module__].__file__)), 'locale') os.path.dirname(upath(sys.modules[settings.__module__].__file__)), 'locale')
return [globalpath] + list(settings.LOCALE_PATHS) return [globalpath] + list(settings.LOCALE_PATHS)
@ -424,7 +414,6 @@ def get_supported_language_variant(lang_code, strict=False):
""" """
global _supported global _supported
if _supported is None: if _supported is None:
from django.conf import settings
_supported = OrderedDict(settings.LANGUAGES) _supported = OrderedDict(settings.LANGUAGES)
if lang_code: if lang_code:
# some browsers use deprecated language codes -- #18419 # some browsers use deprecated language codes -- #18419
@ -472,7 +461,6 @@ def get_language_from_request(request, check_path=False):
If check_path is True, the URL path prefix will be checked for a language If check_path is True, the URL path prefix will be checked for a language
code, otherwise this is skipped for backwards compatibility. code, otherwise this is skipped for backwards compatibility.
""" """
from django.conf import settings
global _supported global _supported
if _supported is None: if _supported is None:
_supported = OrderedDict(settings.LANGUAGES) _supported = OrderedDict(settings.LANGUAGES)
@ -538,7 +526,6 @@ def templatize(src, origin=None):
does so by translating the Django translation tags into standard gettext does so by translating the Django translation tags into standard gettext
function invocations. function invocations.
""" """
from django.conf import settings
from django.template import (Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK, from django.template import (Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK,
TOKEN_COMMENT, TRANSLATOR_COMMENT_MARK) TOKEN_COMMENT, TRANSLATOR_COMMENT_MARK)
src = force_text(src, settings.FILE_CHARSET) src = force_text(src, settings.FILE_CHARSET)

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from contextlib import contextmanager from contextlib import contextmanager
import datetime import datetime
import decimal import decimal
import gettext as gettext_module
from importlib import import_module from importlib import import_module
import os import os
import pickle import pickle
@ -1338,3 +1339,26 @@ class CountrySpecificLanguageTests(TestCase):
r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-pt,en-US;q=0.8,en;q=0.6,ru;q=0.4'} r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-pt,en-US;q=0.8,en;q=0.6,ru;q=0.4'}
lang = get_language_from_request(r) lang = get_language_from_request(r)
self.assertEqual('pt-br', lang) self.assertEqual('pt-br', lang)
class TranslationFilesMissing(TestCase):
def setUp(self):
super(TranslationFilesMissing, self).setUp()
self.gettext_find_builtin = gettext_module.find
def tearDown(self):
gettext_module.find = self.gettext_find_builtin
super(TranslationFilesMissing, self).tearDown()
def patchGettextFind(self):
gettext_module.find = lambda *args, **kw: None
def test_failure_finding_default_mo_files(self):
'''
Ensure IOError is raised if the default language is unparseable.
Refs: #18192
'''
self.patchGettextFind()
trans_real._translations = {}
self.assertRaises(IOError, activate, 'en')