diff options
Diffstat (limited to 'django/utils/translation.py')
| -rw-r--r-- | django/utils/translation.py | 447 |
1 files changed, 447 insertions, 0 deletions
diff --git a/django/utils/translation.py b/django/utils/translation.py new file mode 100644 index 0000000000..0d40a636c8 --- /dev/null +++ b/django/utils/translation.py @@ -0,0 +1,447 @@ +"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() + |
