448 lines
14 KiB
Python
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()
|
|
|