django1/django/utils/translation.py

448 lines
14 KiB
Python

"translation helper functions"
import os
import re
import sys
import gettext as gettext_module
from cStringIO import StringIO
from django.utils.functional import lazy
try:
import threading
hasThreads = True
except ImportError:
hasThreads = False
if hasThreads:
currentThread = threading.currentThread
else:
def currentThread():
return 'no threading'
# translations are cached in a dictionary for
# every language+app tuple. The active translations
# are stored by threadid to make them thread local.
_translations = {}
_active = {}
# the default translation is based on the settings file
_default = None
# this is a cache for accept-header to translation
# object mappings to prevent the accept parser to
# run multiple times for one user
_accepted = {}
def to_locale(language):
"turn a language name (en-us) into a locale name (en_US)"
p = language.find('-')
if p >= 0:
return language[:p].lower()+'_'+language[p+1:].upper()
else:
return language.lower()
def to_language(locale):
"turns a locale name (en_US) into a language name (en-us)"
p = locale.find('_')
if p >= 0:
return locale[:p].lower()+'-'+locale[p+1:].lower()
else:
return locale.lower()
class DjangoTranslation(gettext_module.GNUTranslations):
"""
This class sets up the GNUTranslations context with
regard to output charset. Django uses a defined
DEFAULT_CHARSET as the output charset on Python 2.4 -
with Python 2.3, you need to use DjangoTranslation23.
"""
def __init__(self, *args, **kw):
from django.conf import settings
gettext_module.GNUTranslations.__init__(self, *args, **kw)
# starting with Python 2.4, there is a function to define
# the output charset. Before 2.4, the output charset is
# identical with the translation file charset.
try:
self.set_output_charset(settings.DEFAULT_CHARSET)
except AttributeError:
pass
self.django_output_charset = settings.DEFAULT_CHARSET
self.__language = '??'
def merge(self, other):
self._catalog.update(other._catalog)
def set_language(self, language):
self.__language = language
def language(self):
return self.__language
def __repr__(self):
return "<DjangoTranslation lang:%s>" % self.__language
class DjangoTranslation23(DjangoTranslation):
"""
This is a compatibility class that is only used with Python 2.3.
The reason is, Python 2.3 doesn't support set_output_charset on
translation objects and so needs this wrapper class to make sure
that input charsets from translation files are correctly translated
to output charsets.
With a full switch to Python 2.4, this can be removed from the source.
"""
def gettext(self, msgid):
res = self.ugettext(msgid)
return res.encode(self.django_output_charset)
def ngettext(self, msgid1, msgid2, n):
res = self.ungettext(msgid1, msgid2, n)
return res.encode(self.django_output_charset)
def translation(language):
"""
This function returns a translation object. app must be the fully
qualified name of the application.
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 that is different
from the requested language.
"""
global _translations
t = _translations.get(language, None)
if t is not None:
return t
from django.conf import settings
# set up the right translation class
klass = DjangoTranslation
if sys.version_info < (2, 4):
klass = DjangoTranslation23
globalpath = os.path.join(os.path.dirname(settings.__file__), 'locale')
parts = os.environ['DJANGO_SETTINGS_MODULE'].split('.')
project = __import__(parts[0], {}, {}, [])
projectpath = os.path.join(os.path.dirname(project.__file__), 'locale')
def _fetch(lang, fallback=None):
global _translations
loc = to_locale(lang)
res = _translations.get(lang, None)
if res is not None:
return res
def _translation(path):
try:
t = gettext_module.translation('django', path, [loc], klass)
t.set_language(lang)
return t
except IOError, e:
return None
res = _translation(globalpath)
def _merge(path):
t = _translation(path)
if t is not None:
if res is None:
return t
else:
res.merge(t)
return res
if hasattr(settings, 'LOCALE_PATHS'):
for localepath in settings.LOCALE_PATHS:
if os.path.isdir(localepath):
res = _merge(localepath)
if os.path.isdir(projectpath):
res = _merge(projectpath)
for appname in settings.INSTALLED_APPS:
p = appname.rfind('.')
if p >= 0:
app = getattr(__import__(appname[:p], {}, {}, [appname[p+1:]]), appname[p+1:])
else:
app = __import__(appname, {}, {}, [])
apppath = os.path.join(os.path.dirname(app.__file__), 'locale')
if os.path.isdir(apppath):
res = _merge(apppath)
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):
"""
This function fetches the translation object for a given
tuple of application name and language and installs it as
the current translation object for the current thread.
"""
_active[currentThread()] = translation(language)
def deactivate():
"""
This function deinstalls the currently active translation
object so that further _ calls will resolve against the
default translation object, again.
"""
global _active
if _active.has_key(currentThread()):
del _active[currentThread()]
def get_language():
"""
This function returns the currently selected language.
"""
t = _active.get(currentThread(), None)
if t is not None:
try:
return to_language(t.language())
except AttributeError:
pass
# if we don't have a real translation object, we assume
# it's the default language.
from django.conf.settings import LANGUAGE_CODE
return LANGUAGE_CODE
def gettext(message):
"""
This function will be patched into the builtins module to
provide the _ helper function. It will use the current
thread as a discriminator to find the translation object
to use. If no current translation is activated, the
message will be run through the default translation
object.
"""
global _default, _active
t = _active.get(currentThread(), None)
if t is not None:
return t.gettext(message)
if _default is None:
from django.conf import settings
_default = translation(settings.LANGUAGE_CODE)
return _default.gettext(message)
def gettext_noop(message):
"""
This function is used to just mark strings for translation
but to not translate them now. This can be used to store
strings in global variables that should stay in the base
language (because they might be used externally) and will
be translated later on.
"""
return message
def ngettext(singular, plural, number):
"""
This function returns the translation of either the singular
or plural, based on the number.
"""
global _default, _active
t = _active.get(currentThread(), None)
if t is not None:
return t.ngettext(singular, plural, number)
if _default is None:
from django.conf import settings
_default = translation('*', settings.LANGUAGE_CODE)
return _default.ngettext(singular, plural, number)
gettext_lazy = lazy(gettext, str)
ngettext_lazy = lazy(ngettext, str)
def check_for_language(lang_code):
"""
This function checks wether there is a global language
file for the given language code. This is used to decide
wether a user-provided language is available.
"""
from django.conf import settings
globalpath = os.path.join(os.path.dirname(settings.__file__), 'locale')
if gettext_module.find('django', globalpath, [to_locale(lang_code)]) is not None:
return True
else:
return False
def get_language_from_request(request):
"""
analyze the request to find what language the user
wants the system to show.
"""
global _accepted
from django.conf import settings
globalpath = os.path.join(os.path.dirname(settings.__file__), 'locale')
if hasattr(request, 'session'):
lang_code = request.session.get('django_language', None)
if lang_code is not None and check_for_language(lang_code):
return lang_code
lang_code = request.COOKIES.get('django_language', None)
if lang_code is not None and check_for_language(lang_code):
return lang_code
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None)
if accept is not None:
t = _accepted.get(accept, None)
if t is not None:
return t
def _parsed(el):
p = el.find(';q=')
if p >= 0:
lang = el[:p].strip()
order = int(float(el[p+3:].strip())*100)
else:
lang = el
order = 100
if lang.find('-') >= 0:
(lang, sublang) = lang.split('-')
lang = lang.lower() + '_' + sublang.upper()
return (lang, order)
langs = [_parsed(el) for el in accept.split(',')]
langs.sort(lambda a,b: -1*cmp(a[1], b[1]))
for lang, order in langs:
langfile = gettext_module.find('django', globalpath, [to_locale(lang)])
if langfile:
# reconstruct the actual language from the language
# filename, because otherwise we might incorrectly
# report de_DE if we only have de available, but
# did find de_DE because of language normalization
lang = langfile[len(globalpath):].split('/')[1]
_accepted[accept] = lang
return lang
return settings.LANGUAGE_CODE
def install():
"""
This installs the gettext function as the default
translation function under the name _.
"""
__builtins__['_'] = gettext
dot_re = re.compile(r'\S')
def blankout(src, char):
"""
This is used in the templateize function and changes every
non-whitespace character to the given char.
"""
return dot_re.sub(char, src)
inline_re = re.compile(r"""^\s*trans\s+((?:".*?")|(?:'.*?'))\s*""")
block_re = re.compile(r"""^\s*blocktrans(?:\s+|$)""")
endblock_re = re.compile(r"""^\s*endblocktrans$""")
plural_re = re.compile(r"""^\s*plural$""")
constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""")
def templateize(src):
from django.core.template import tokenize, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK
"""
This function turns a django template into something that is
understood by xgettext. It does so by translating the django
translation tags into standard gettext function invocations.
"""
out = StringIO()
intrans = False
inplural = False
singular = []
plural = []
for t in tokenize(src):
if intrans:
if t.token_type == TOKEN_BLOCK:
endbmatch = endblock_re.match(t.contents)
pluralmatch = plural_re.match(t.contents)
if endbmatch:
if inplural:
out.write(' ngettext(%r,%r,count) ' % (''.join(singular), ''.join(plural)))
for part in singular:
out.write(blankout(part, 'S'))
for part in plural:
out.write(blankout(part, 'P'))
else:
out.write(' gettext(%r) ' % ''.join(singular))
for part in singular:
out.write(blankout(part, 'S'))
intrans = False
inplural = False
singular = []
plural = []
elif pluralmatch:
inplural = True
else:
raise SyntaxError, "translation blocks must not include other block tags: %s" % t.contents
elif t.token_type == TOKEN_VAR:
if inplural:
plural.append('%%(%s)s' % t.contents)
else:
singular.append('%%(%s)s' % t.contents)
elif t.token_type == TOKEN_TEXT:
if inplural:
plural.append(t.contents)
else:
singular.append(t.contents)
else:
if t.token_type == TOKEN_BLOCK:
imatch = inline_re.match(t.contents)
bmatch = block_re.match(t.contents)
cmatches = constant_re.findall(t.contents)
if imatch:
g = imatch.group(1)
if g[0] == '"': g = g.strip('"')
elif g[0] == "'": g = g.strip("'")
out.write(' gettext(%r) ' % g)
elif bmatch:
intrans = True
inplural = False
singular = []
plural = []
elif cmatches:
for cmatch in cmatches:
out.write(' _(%s) ' % cmatch)
else:
out.write(blankout(t.contents, 'B'))
elif t.token_type == TOKEN_VAR:
parts = t.contents.split('|')
cmatch = constant_re.match(parts[0])
if cmatch:
out.write(' _(%s) ' % cmatch.group(1))
for p in parts[1:]:
if p.find(':_(') >= 0:
out.write(' %s ' % p.split(':',1)[1])
else:
out.write(blankout(p, 'F'))
else:
out.write(blankout(t.contents, 'X'))
return out.getvalue()