diff options
Diffstat (limited to 'webapp/django/utils/translation')
-rw-r--r-- | webapp/django/utils/translation/__init__.py | 111 | ||||
-rw-r--r-- | webapp/django/utils/translation/trans_null.py | 53 | ||||
-rw-r--r-- | webapp/django/utils/translation/trans_real.py | 541 |
3 files changed, 705 insertions, 0 deletions
diff --git a/webapp/django/utils/translation/__init__.py b/webapp/django/utils/translation/__init__.py new file mode 100644 index 0000000000..43ce3e591a --- /dev/null +++ b/webapp/django/utils/translation/__init__.py @@ -0,0 +1,111 @@ +""" +Internationalization support. +""" +from django.utils.functional import lazy +from django.utils.encoding import force_unicode + +__all__ = ['gettext', 'gettext_noop', 'gettext_lazy', 'ngettext', + 'ngettext_lazy', 'string_concat', 'activate', 'deactivate', + 'get_language', 'get_language_bidi', 'get_date_formats', + 'get_partial_date_formats', 'check_for_language', 'to_locale', + 'get_language_from_request', 'templatize', 'ugettext', + 'ungettext', 'deactivate_all'] + +# Here be dragons, so a short explanation of the logic won't hurt: +# We are trying to solve two problems: (1) access settings, in particular +# settings.USE_I18N, as late as possible, so that modules can be imported +# without having to first configure Django, and (2) if some other code creates +# a reference to one of these functions, don't break that reference when we +# replace the functions with their real counterparts (once we do access the +# settings). + +def delayed_loader(*args, **kwargs): + """ + Replace each real_* function with the corresponding function from either + trans_real or trans_null (e.g. real_gettext is replaced with + trans_real.gettext or trans_null.gettext). This function is run once, the + first time any i18n method is called. It replaces all the i18n methods at + once at that time. + """ + import traceback + from django.conf import settings + if settings.USE_I18N: + import trans_real as trans + else: + import trans_null as trans + caller = traceback.extract_stack(limit=2)[0][2] + g = globals() + for name in __all__: + if hasattr(trans, name): + g['real_%s' % name] = getattr(trans, name) + + # Make the originally requested function call on the way out the door. + return g['real_%s' % caller](*args, **kwargs) + +g = globals() +for name in __all__: + g['real_%s' % name] = delayed_loader +del g, delayed_loader + +def gettext_noop(message): + return real_gettext_noop(message) + +ugettext_noop = gettext_noop + +def gettext(message): + return real_gettext(message) + +def ngettext(singular, plural, number): + return real_ngettext(singular, plural, number) + +def ugettext(message): + return real_ugettext(message) + +def ungettext(singular, plural, number): + return real_ungettext(singular, plural, number) + +ngettext_lazy = lazy(ngettext, str) +gettext_lazy = lazy(gettext, str) +ungettext_lazy = lazy(ungettext, unicode) +ugettext_lazy = lazy(ugettext, unicode) + +def activate(language): + return real_activate(language) + +def deactivate(): + return real_deactivate() + +def get_language(): + return real_get_language() + +def get_language_bidi(): + return real_get_language_bidi() + +def get_date_formats(): + return real_get_date_formats() + +def get_partial_date_formats(): + return real_get_partial_date_formats() + +def check_for_language(lang_code): + return real_check_for_language(lang_code) + +def to_locale(language): + return real_to_locale(language) + +def get_language_from_request(request): + return real_get_language_from_request(request) + +def templatize(src): + return real_templatize(src) + +def deactivate_all(): + return real_deactivate_all() + +def string_concat(*strings): + """ + Lazy variant of string concatenation, needed for translations that are + constructed from multiple parts. + """ + return u''.join([force_unicode(s) for s in strings]) +string_concat = lazy(string_concat, unicode) diff --git a/webapp/django/utils/translation/trans_null.py b/webapp/django/utils/translation/trans_null.py new file mode 100644 index 0000000000..98c6de6197 --- /dev/null +++ b/webapp/django/utils/translation/trans_null.py @@ -0,0 +1,53 @@ +# These are versions of the functions in django.utils.translation.trans_real +# that don't actually do anything. This is purely for performance, so that +# settings.USE_I18N = False can use this module rather than trans_real.py. + +from django.conf import settings +from django.utils.encoding import force_unicode +from django.utils.safestring import mark_safe, SafeData + +def ngettext(singular, plural, number): + if number == 1: return singular + return plural +ngettext_lazy = ngettext + +def ungettext(singular, plural, number): + return force_unicode(ngettext(singular, plural, number)) + +activate = lambda x: None +deactivate = deactivate_all = lambda: None +get_language = lambda: settings.LANGUAGE_CODE +get_language_bidi = lambda: settings.LANGUAGE_CODE in settings.LANGUAGES_BIDI +get_date_formats = lambda: (settings.DATE_FORMAT, settings.DATETIME_FORMAT, settings.TIME_FORMAT) +get_partial_date_formats = lambda: (settings.YEAR_MONTH_FORMAT, settings.MONTH_DAY_FORMAT) +check_for_language = lambda x: True + +TECHNICAL_ID_MAP = { + "DATE_WITH_TIME_FULL": settings.DATETIME_FORMAT, + "DATE_FORMAT": settings.DATE_FORMAT, + "DATETIME_FORMAT": settings.DATETIME_FORMAT, + "TIME_FORMAT": settings.TIME_FORMAT, + "YEAR_MONTH_FORMAT": settings.YEAR_MONTH_FORMAT, + "MONTH_DAY_FORMAT": settings.MONTH_DAY_FORMAT, +} + +def gettext(message): + result = TECHNICAL_ID_MAP.get(message, message) + if isinstance(message, SafeData): + return mark_safe(result) + return result + +def ugettext(message): + return force_unicode(gettext(message)) + +gettext_noop = gettext_lazy = _ = gettext + +def to_locale(language): + p = language.find('-') + if p >= 0: + return language[:p].lower()+'_'+language[p+1:].upper() + else: + return language.lower() + +def get_language_from_request(request): + return settings.LANGUAGE_CODE diff --git a/webapp/django/utils/translation/trans_real.py b/webapp/django/utils/translation/trans_real.py new file mode 100644 index 0000000000..93bb3c9bf9 --- /dev/null +++ b/webapp/django/utils/translation/trans_real.py @@ -0,0 +1,541 @@ +"""Translation helper functions.""" + +import locale +import os +import re +import sys +import gettext as gettext_module +from cStringIO import StringIO + +from django.utils.safestring import mark_safe, SafeData +from django.utils.thread_support import currentThread + +# 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 normalized accept-header languages to prevent multiple +# file lookups when checking the same locale on repeated requests. +_accepted = {} + +# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9. +accept_language_re = re.compile(r''' + ([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*) # "en", "en-au", "x-y-z", "*" + (?:;q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8" + (?:\s*,\s*|$) # Multiple accepts per header. + ''', re.VERBOSE) + +def to_locale(language, to_lower=False): + """ + Turns a language name (en-us) into a locale name (en_US). If 'to_lower' is + True, the last component is lower-cased (en_us). + """ + p = language.find('-') + if p >= 0: + if to_lower: + return language[:p].lower()+'_'+language[p+1:].lower() + else: + 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, 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's 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('utf-8') + except AttributeError: + pass + self.django_output_charset = 'utf-8' + 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): + """ + Compatibility class that is only used with Python 2.3. + Python 2.3 doesn't support set_output_charset on translation objects and + needs this wrapper class to make sure 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): + """ + 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 + + 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(sys.modules[settings.__module__].__file__), 'locale') + + if settings.SETTINGS_MODULE is not None: + parts = settings.SETTINGS_MODULE.split('.') + project = __import__(parts[0], {}, {}, []) + projectpath = os.path.join(os.path.dirname(project.__file__), 'locale') + else: + projectpath = None + + 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) + + # 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 base_lang(lang) in [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 localepath in settings.LOCALE_PATHS: + if os.path.isdir(localepath): + res = _merge(localepath) + + if projectpath and 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): + """ + 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(): + """ + Deinstalls the currently active translation object so that further _ calls + will resolve against the default translation object, again. + """ + global _active + if currentThread() in _active: + del _active[currentThread()] + +def deactivate_all(): + """ + Makes the active translation object a NullTranslations() instance. This is + useful when we want delayed translations to appear as the original string + for some reason. + """ + _active[currentThread()] = gettext_module.NullTranslations() + +def get_language(): + """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, assume it's the default language. + from django.conf import settings + return settings.LANGUAGE_CODE + +def get_language_bidi(): + """ + Returns selected language's BiDi layout. + False = left-to-right layout + True = right-to-left layout + """ + from django.conf import settings + return get_language() in settings.LANGUAGES_BIDI + +def catalog(): + """ + Returns the current active catalog for further processing. + This can be used if you need to modify the catalog or want to access the + whole message catalog instead of just translating one string. + """ + global _default, _active + t = _active.get(currentThread(), None) + if t is not None: + return t + if _default is None: + from django.conf import settings + _default = translation(settings.LANGUAGE_CODE) + return _default + +def do_translate(message, translation_function): + """ + Translates 'message' using the given 'translation_function' name -- which + will be either gettext or ugettext. It uses the current thread 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: + result = getattr(t, translation_function)(message) + else: + if _default is None: + from django.conf import settings + _default = translation(settings.LANGUAGE_CODE) + result = getattr(_default, translation_function)(message) + if isinstance(message, SafeData): + return mark_safe(result) + return result + +def gettext(message): + return do_translate(message, 'gettext') + +def ugettext(message): + return do_translate(message, 'ugettext') + +def gettext_noop(message): + """ + Marks strings for translation but doesn't 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. + """ + return message + +def do_ntranslate(singular, plural, number, translation_function): + global _default, _active + + t = _active.get(currentThread(), None) + if t is not None: + return getattr(t, translation_function)(singular, plural, number) + if _default is None: + from django.conf import settings + _default = translation(settings.LANGUAGE_CODE) + return getattr(_default, translation_function)(singular, plural, number) + +def ngettext(singular, plural, number): + """ + Returns a UTF-8 bytestring of the translation of either the singular or + plural, based on the number. + """ + return do_ntranslate(singular, plural, number, 'ngettext') + +def ungettext(singular, plural, number): + """ + Returns a unicode strings of the translation of either the singular or + plural, based on the number. + """ + return do_ntranslate(singular, plural, number, 'ungettext') + +def check_for_language(lang_code): + """ + Checks whether there is a global language file for the given language + code. This is used to decide whether a user-provided language is + available. This is only used for language codes from either the cookies or + session. + """ + from django.conf import settings + globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__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): + """ + Analyzes the request to find what language the user wants the system to + show. Only languages listed in settings.LANGUAGES are taken into account. + If the user requests a sublanguage where we have a main language, we send + out the main language. + """ + global _accepted + from django.conf import settings + globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale') + supported = dict(settings.LANGUAGES) + + if hasattr(request, 'session'): + lang_code = request.session.get('django_language', None) + if lang_code in supported and lang_code is not None and check_for_language(lang_code): + return lang_code + + lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME) + if lang_code and lang_code in supported and check_for_language(lang_code): + return lang_code + + accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '') + for accept_lang, unused in parse_accept_lang_header(accept): + if accept_lang == '*': + break + + # We have a very restricted form for our language files (no encoding + # specifier, since they all must be UTF-8 and only one possible + # language each time. So we avoid the overhead of gettext.find() and + # work out the MO file manually. + + # 'normalized' is the root name of the locale in POSIX format (which is + # the format used for the directories holding the MO files). + normalized = locale.locale_alias.get(to_locale(accept_lang, True)) + if not normalized: + continue + # Remove the default encoding from locale_alias. + normalized = normalized.split('.')[0] + + if normalized in _accepted: + # We've seen this locale before and have an MO file for it, so no + # need to check again. + return _accepted[normalized] + + for lang, dirname in ((accept_lang, normalized), + (accept_lang.split('-')[0], normalized.split('_')[0])): + if lang not in supported: + continue + langfile = os.path.join(globalpath, dirname, 'LC_MESSAGES', + 'django.mo') + if os.path.exists(langfile): + _accepted[normalized] = lang + return lang + + return settings.LANGUAGE_CODE + +def get_date_formats(): + """ + Checks whether translation files provide a translation for some technical + message ID to store date and time formats. If it doesn't contain one, the + formats provided in the settings will be used. + """ + from django.conf import settings + date_format = ugettext('DATE_FORMAT') + datetime_format = ugettext('DATETIME_FORMAT') + time_format = ugettext('TIME_FORMAT') + if date_format == 'DATE_FORMAT': + date_format = settings.DATE_FORMAT + if datetime_format == 'DATETIME_FORMAT': + datetime_format = settings.DATETIME_FORMAT + if time_format == 'TIME_FORMAT': + time_format = settings.TIME_FORMAT + return date_format, datetime_format, time_format + +def get_partial_date_formats(): + """ + Checks whether translation files provide a translation for some technical + message ID to store partial date formats. If it doesn't contain one, the + formats provided in the settings will be used. + """ + from django.conf import settings + year_month_format = ugettext('YEAR_MONTH_FORMAT') + month_day_format = ugettext('MONTH_DAY_FORMAT') + if year_month_format == 'YEAR_MONTH_FORMAT': + year_month_format = settings.YEAR_MONTH_FORMAT + if month_day_format == 'MONTH_DAY_FORMAT': + month_day_format = settings.MONTH_DAY_FORMAT + return year_month_format, month_day_format + +dot_re = re.compile(r'\S') +def blankout(src, char): + """ + Changes every non-whitespace character to the given char. + Used in the templatize function. + """ + 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 templatize(src): + """ + 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. + """ + from django.template import Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK + out = StringIO() + intrans = False + inplural = False + singular = [] + plural = [] + for t in Lexer(src, None).tokenize(): + 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: + for fmatch in constant_re.findall(t.contents): + out.write(' _(%s) ' % fmatch) + 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() + +def parse_accept_lang_header(lang_string): + """ + Parses the lang_string, which is the body of an HTTP Accept-Language + header, and returns a list of (lang, q-value), ordered by 'q' values. + + Any format errors in lang_string results in an empty list being returned. + """ + result = [] + pieces = accept_language_re.split(lang_string) + if pieces[-1]: + return [] + for i in range(0, len(pieces) - 1, 3): + first, lang, priority = pieces[i : i + 3] + if first: + return [] + priority = priority and float(priority) or 1.0 + result.append((lang, priority)) + result.sort(lambda x, y: -cmp(x[1], y[1])) + return result |