web/lib/django/utils/translation/trans_real.py
changeset 38 77b6da96e6f1
equal deleted inserted replaced
37:8d941af65caf 38:77b6da96e6f1
       
     1 """Translation helper functions."""
       
     2 
       
     3 import locale
       
     4 import os
       
     5 import re
       
     6 import sys
       
     7 import warnings
       
     8 import gettext as gettext_module
       
     9 from cStringIO import StringIO
       
    10 
       
    11 from django.utils.importlib import import_module
       
    12 from django.utils.safestring import mark_safe, SafeData
       
    13 from django.utils.thread_support import currentThread
       
    14 
       
    15 # Translations are cached in a dictionary for every language+app tuple.
       
    16 # The active translations are stored by threadid to make them thread local.
       
    17 _translations = {}
       
    18 _active = {}
       
    19 
       
    20 # The default translation is based on the settings file.
       
    21 _default = None
       
    22 
       
    23 # This is a cache for normalized accept-header languages to prevent multiple
       
    24 # file lookups when checking the same locale on repeated requests.
       
    25 _accepted = {}
       
    26 
       
    27 # Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9.
       
    28 accept_language_re = re.compile(r'''
       
    29         ([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*)   # "en", "en-au", "x-y-z", "*"
       
    30         (?:;q=(0(?:\.\d{,3})?|1(?:.0{,3})?))?   # Optional "q=1.00", "q=0.8"
       
    31         (?:\s*,\s*|$)                            # Multiple accepts per header.
       
    32         ''', re.VERBOSE)
       
    33 
       
    34 def to_locale(language, to_lower=False):
       
    35     """
       
    36     Turns a language name (en-us) into a locale name (en_US). If 'to_lower' is
       
    37     True, the last component is lower-cased (en_us).
       
    38     """
       
    39     p = language.find('-')
       
    40     if p >= 0:
       
    41         if to_lower:
       
    42             return language[:p].lower()+'_'+language[p+1:].lower()
       
    43         else:
       
    44             # Get correct locale for sr-latn
       
    45             if len(language[p+1:]) > 2:
       
    46                 return language[:p].lower()+'_'+language[p+1].upper()+language[p+2:].lower()
       
    47             return language[:p].lower()+'_'+language[p+1:].upper()
       
    48     else:
       
    49         return language.lower()
       
    50 
       
    51 def to_language(locale):
       
    52     """Turns a locale name (en_US) into a language name (en-us)."""
       
    53     p = locale.find('_')
       
    54     if p >= 0:
       
    55         return locale[:p].lower()+'-'+locale[p+1:].lower()
       
    56     else:
       
    57         return locale.lower()
       
    58 
       
    59 class DjangoTranslation(gettext_module.GNUTranslations):
       
    60     """
       
    61     This class sets up the GNUTranslations context with regard to output
       
    62     charset. Django uses a defined DEFAULT_CHARSET as the output charset on
       
    63     Python 2.4.
       
    64     """
       
    65     def __init__(self, *args, **kw):
       
    66         from django.conf import settings
       
    67         gettext_module.GNUTranslations.__init__(self, *args, **kw)
       
    68         # Starting with Python 2.4, there's a function to define
       
    69         # the output charset. Before 2.4, the output charset is
       
    70         # identical with the translation file charset.
       
    71         try:
       
    72             self.set_output_charset('utf-8')
       
    73         except AttributeError:
       
    74             pass
       
    75         self.django_output_charset = 'utf-8'
       
    76         self.__language = '??'
       
    77 
       
    78     def merge(self, other):
       
    79         self._catalog.update(other._catalog)
       
    80 
       
    81     def set_language(self, language):
       
    82         self.__language = language
       
    83 
       
    84     def language(self):
       
    85         return self.__language
       
    86 
       
    87     def __repr__(self):
       
    88         return "<DjangoTranslation lang:%s>" % self.__language
       
    89 
       
    90 def translation(language):
       
    91     """
       
    92     Returns a translation object.
       
    93 
       
    94     This translation object will be constructed out of multiple GNUTranslations
       
    95     objects by merging their catalogs. It will construct a object for the
       
    96     requested language and add a fallback to the default language, if it's
       
    97     different from the requested language.
       
    98     """
       
    99     global _translations
       
   100 
       
   101     t = _translations.get(language, None)
       
   102     if t is not None:
       
   103         return t
       
   104 
       
   105     from django.conf import settings
       
   106 
       
   107     globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
       
   108 
       
   109     if settings.SETTINGS_MODULE is not None:
       
   110         parts = settings.SETTINGS_MODULE.split('.')
       
   111         project = import_module(parts[0])
       
   112         projectpath = os.path.join(os.path.dirname(project.__file__), 'locale')
       
   113     else:
       
   114         projectpath = None
       
   115 
       
   116     def _fetch(lang, fallback=None):
       
   117 
       
   118         global _translations
       
   119 
       
   120         loc = to_locale(lang)
       
   121 
       
   122         res = _translations.get(lang, None)
       
   123         if res is not None:
       
   124             return res
       
   125 
       
   126         def _translation(path):
       
   127             try:
       
   128                 t = gettext_module.translation('django', path, [loc], DjangoTranslation)
       
   129                 t.set_language(lang)
       
   130                 return t
       
   131             except IOError, e:
       
   132                 return None
       
   133 
       
   134         res = _translation(globalpath)
       
   135 
       
   136         # We want to ensure that, for example,  "en-gb" and "en-us" don't share
       
   137         # the same translation object (thus, merging en-us with a local update
       
   138         # doesn't affect en-gb), even though they will both use the core "en"
       
   139         # translation. So we have to subvert Python's internal gettext caching.
       
   140         base_lang = lambda x: x.split('-', 1)[0]
       
   141         if base_lang(lang) in [base_lang(trans) for trans in _translations]:
       
   142             res._info = res._info.copy()
       
   143             res._catalog = res._catalog.copy()
       
   144 
       
   145         def _merge(path):
       
   146             t = _translation(path)
       
   147             if t is not None:
       
   148                 if res is None:
       
   149                     return t
       
   150                 else:
       
   151                     res.merge(t)
       
   152             return res
       
   153 
       
   154         for localepath in settings.LOCALE_PATHS:
       
   155             if os.path.isdir(localepath):
       
   156                 res = _merge(localepath)
       
   157 
       
   158         for appname in settings.INSTALLED_APPS:
       
   159             app = import_module(appname)
       
   160             apppath = os.path.join(os.path.dirname(app.__file__), 'locale')
       
   161 
       
   162             if os.path.isdir(apppath):
       
   163                 res = _merge(apppath)
       
   164 
       
   165         if projectpath and os.path.isdir(projectpath):
       
   166             res = _merge(projectpath)
       
   167 
       
   168         if res is None:
       
   169             if fallback is not None:
       
   170                 res = fallback
       
   171             else:
       
   172                 return gettext_module.NullTranslations()
       
   173         _translations[lang] = res
       
   174         return res
       
   175 
       
   176     default_translation = _fetch(settings.LANGUAGE_CODE)
       
   177     current_translation = _fetch(language, fallback=default_translation)
       
   178 
       
   179     return current_translation
       
   180 
       
   181 def activate(language):
       
   182     """
       
   183     Fetches the translation object for a given tuple of application name and
       
   184     language and installs it as the current translation object for the current
       
   185     thread.
       
   186     """
       
   187     if isinstance(language, basestring) and language == 'no':
       
   188         warnings.warn(
       
   189             "The use of the language code 'no' is deprecated. "
       
   190             "Please use the 'nb' translation instead.",
       
   191             PendingDeprecationWarning
       
   192         )
       
   193     _active[currentThread()] = translation(language)
       
   194 
       
   195 def deactivate():
       
   196     """
       
   197     Deinstalls the currently active translation object so that further _ calls
       
   198     will resolve against the default translation object, again.
       
   199     """
       
   200     global _active
       
   201     if currentThread() in _active:
       
   202         del _active[currentThread()]
       
   203 
       
   204 def deactivate_all():
       
   205     """
       
   206     Makes the active translation object a NullTranslations() instance. This is
       
   207     useful when we want delayed translations to appear as the original string
       
   208     for some reason.
       
   209     """
       
   210     _active[currentThread()] = gettext_module.NullTranslations()
       
   211 
       
   212 def get_language():
       
   213     """Returns the currently selected language."""
       
   214     t = _active.get(currentThread(), None)
       
   215     if t is not None:
       
   216         try:
       
   217             return to_language(t.language())
       
   218         except AttributeError:
       
   219             pass
       
   220     # If we don't have a real translation object, assume it's the default language.
       
   221     from django.conf import settings
       
   222     return settings.LANGUAGE_CODE
       
   223 
       
   224 def get_language_bidi():
       
   225     """
       
   226     Returns selected language's BiDi layout.
       
   227     
       
   228     * False = left-to-right layout
       
   229     * True = right-to-left layout
       
   230     """
       
   231     from django.conf import settings
       
   232     
       
   233     base_lang = get_language().split('-')[0]
       
   234     return base_lang in settings.LANGUAGES_BIDI
       
   235 
       
   236 def catalog():
       
   237     """
       
   238     Returns the current active catalog for further processing.
       
   239     This can be used if you need to modify the catalog or want to access the
       
   240     whole message catalog instead of just translating one string.
       
   241     """
       
   242     global _default, _active
       
   243     t = _active.get(currentThread(), None)
       
   244     if t is not None:
       
   245         return t
       
   246     if _default is None:
       
   247         from django.conf import settings
       
   248         _default = translation(settings.LANGUAGE_CODE)
       
   249     return _default
       
   250 
       
   251 def do_translate(message, translation_function):
       
   252     """
       
   253     Translates 'message' using the given 'translation_function' name -- which
       
   254     will be either gettext or ugettext. It uses the current thread to find the
       
   255     translation object to use. If no current translation is activated, the
       
   256     message will be run through the default translation object.
       
   257     """
       
   258     eol_message = message.replace('\r\n', '\n').replace('\r', '\n')
       
   259     global _default, _active
       
   260     t = _active.get(currentThread(), None)
       
   261     if t is not None:
       
   262         result = getattr(t, translation_function)(eol_message)
       
   263     else:
       
   264         if _default is None:
       
   265             from django.conf import settings
       
   266             _default = translation(settings.LANGUAGE_CODE)
       
   267         result = getattr(_default, translation_function)(eol_message)
       
   268     if isinstance(message, SafeData):
       
   269         return mark_safe(result)
       
   270     return result
       
   271 
       
   272 def gettext(message):
       
   273     return do_translate(message, 'gettext')
       
   274 
       
   275 def ugettext(message):
       
   276     return do_translate(message, 'ugettext')
       
   277 
       
   278 def gettext_noop(message):
       
   279     """
       
   280     Marks strings for translation but doesn't translate them now. This can be
       
   281     used to store strings in global variables that should stay in the base
       
   282     language (because they might be used externally) and will be translated
       
   283     later.
       
   284     """
       
   285     return message
       
   286 
       
   287 def do_ntranslate(singular, plural, number, translation_function):
       
   288     global _default, _active
       
   289 
       
   290     t = _active.get(currentThread(), None)
       
   291     if t is not None:
       
   292         return getattr(t, translation_function)(singular, plural, number)
       
   293     if _default is None:
       
   294         from django.conf import settings
       
   295         _default = translation(settings.LANGUAGE_CODE)
       
   296     return getattr(_default, translation_function)(singular, plural, number)
       
   297 
       
   298 def ngettext(singular, plural, number):
       
   299     """
       
   300     Returns a UTF-8 bytestring of the translation of either the singular or
       
   301     plural, based on the number.
       
   302     """
       
   303     return do_ntranslate(singular, plural, number, 'ngettext')
       
   304 
       
   305 def ungettext(singular, plural, number):
       
   306     """
       
   307     Returns a unicode strings of the translation of either the singular or
       
   308     plural, based on the number.
       
   309     """
       
   310     return do_ntranslate(singular, plural, number, 'ungettext')
       
   311 
       
   312 def check_for_language(lang_code):
       
   313     """
       
   314     Checks whether there is a global language file for the given language
       
   315     code. This is used to decide whether a user-provided language is
       
   316     available. This is only used for language codes from either the cookies or
       
   317     session.
       
   318     """
       
   319     from django.conf import settings
       
   320     globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
       
   321     if gettext_module.find('django', globalpath, [to_locale(lang_code)]) is not None:
       
   322         return True
       
   323     else:
       
   324         return False
       
   325 
       
   326 def get_language_from_request(request):
       
   327     """
       
   328     Analyzes the request to find what language the user wants the system to
       
   329     show. Only languages listed in settings.LANGUAGES are taken into account.
       
   330     If the user requests a sublanguage where we have a main language, we send
       
   331     out the main language.
       
   332     """
       
   333     global _accepted
       
   334     from django.conf import settings
       
   335     globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
       
   336     supported = dict(settings.LANGUAGES)
       
   337 
       
   338     if hasattr(request, 'session'):
       
   339         lang_code = request.session.get('django_language', None)
       
   340         if lang_code in supported and lang_code is not None and check_for_language(lang_code):
       
   341             return lang_code
       
   342 
       
   343     lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
       
   344 
       
   345     if lang_code and lang_code not in supported:
       
   346         lang_code = lang_code.split('-')[0] # e.g. if fr-ca is not supported fallback to fr
       
   347 
       
   348     if lang_code and lang_code in supported and check_for_language(lang_code):
       
   349         return lang_code
       
   350 
       
   351     accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
       
   352     for accept_lang, unused in parse_accept_lang_header(accept):
       
   353         if accept_lang == '*':
       
   354             break
       
   355 
       
   356         # We have a very restricted form for our language files (no encoding
       
   357         # specifier, since they all must be UTF-8 and only one possible
       
   358         # language each time. So we avoid the overhead of gettext.find() and
       
   359         # work out the MO file manually.
       
   360 
       
   361         # 'normalized' is the root name of the locale in POSIX format (which is
       
   362         # the format used for the directories holding the MO files).
       
   363         normalized = locale.locale_alias.get(to_locale(accept_lang, True))
       
   364         if not normalized:
       
   365             continue
       
   366         # Remove the default encoding from locale_alias.
       
   367         normalized = normalized.split('.')[0]
       
   368 
       
   369         if normalized in _accepted:
       
   370             # We've seen this locale before and have an MO file for it, so no
       
   371             # need to check again.
       
   372             return _accepted[normalized]
       
   373 
       
   374         for lang, dirname in ((accept_lang, normalized),
       
   375                 (accept_lang.split('-')[0], normalized.split('_')[0])):
       
   376             if lang.lower() not in supported:
       
   377                 continue
       
   378             langfile = os.path.join(globalpath, dirname, 'LC_MESSAGES',
       
   379                     'django.mo')
       
   380             if os.path.exists(langfile):
       
   381                 _accepted[normalized] = lang
       
   382                 return lang
       
   383 
       
   384     return settings.LANGUAGE_CODE
       
   385 
       
   386 dot_re = re.compile(r'\S')
       
   387 def blankout(src, char):
       
   388     """
       
   389     Changes every non-whitespace character to the given char.
       
   390     Used in the templatize function.
       
   391     """
       
   392     return dot_re.sub(char, src)
       
   393 
       
   394 inline_re = re.compile(r"""^\s*trans\s+((?:".*?")|(?:'.*?'))\s*""")
       
   395 block_re = re.compile(r"""^\s*blocktrans(?:\s+|$)""")
       
   396 endblock_re = re.compile(r"""^\s*endblocktrans$""")
       
   397 plural_re = re.compile(r"""^\s*plural$""")
       
   398 constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""")
       
   399 
       
   400 def templatize(src):
       
   401     """
       
   402     Turns a Django template into something that is understood by xgettext. It
       
   403     does so by translating the Django translation tags into standard gettext
       
   404     function invocations.
       
   405     """
       
   406     from django.template import Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK
       
   407     out = StringIO()
       
   408     intrans = False
       
   409     inplural = False
       
   410     singular = []
       
   411     plural = []
       
   412     for t in Lexer(src, None).tokenize():
       
   413         if intrans:
       
   414             if t.token_type == TOKEN_BLOCK:
       
   415                 endbmatch = endblock_re.match(t.contents)
       
   416                 pluralmatch = plural_re.match(t.contents)
       
   417                 if endbmatch:
       
   418                     if inplural:
       
   419                         out.write(' ngettext(%r,%r,count) ' % (''.join(singular), ''.join(plural)))
       
   420                         for part in singular:
       
   421                             out.write(blankout(part, 'S'))
       
   422                         for part in plural:
       
   423                             out.write(blankout(part, 'P'))
       
   424                     else:
       
   425                         out.write(' gettext(%r) ' % ''.join(singular))
       
   426                         for part in singular:
       
   427                             out.write(blankout(part, 'S'))
       
   428                     intrans = False
       
   429                     inplural = False
       
   430                     singular = []
       
   431                     plural = []
       
   432                 elif pluralmatch:
       
   433                     inplural = True
       
   434                 else:
       
   435                     raise SyntaxError("Translation blocks must not include other block tags: %s" % t.contents)
       
   436             elif t.token_type == TOKEN_VAR:
       
   437                 if inplural:
       
   438                     plural.append('%%(%s)s' % t.contents)
       
   439                 else:
       
   440                     singular.append('%%(%s)s' % t.contents)
       
   441             elif t.token_type == TOKEN_TEXT:
       
   442                 if inplural:
       
   443                     plural.append(t.contents)
       
   444                 else:
       
   445                     singular.append(t.contents)
       
   446         else:
       
   447             if t.token_type == TOKEN_BLOCK:
       
   448                 imatch = inline_re.match(t.contents)
       
   449                 bmatch = block_re.match(t.contents)
       
   450                 cmatches = constant_re.findall(t.contents)
       
   451                 if imatch:
       
   452                     g = imatch.group(1)
       
   453                     if g[0] == '"': g = g.strip('"')
       
   454                     elif g[0] == "'": g = g.strip("'")
       
   455                     out.write(' gettext(%r) ' % g)
       
   456                 elif bmatch:
       
   457                     for fmatch in constant_re.findall(t.contents):
       
   458                         out.write(' _(%s) ' % fmatch)
       
   459                     intrans = True
       
   460                     inplural = False
       
   461                     singular = []
       
   462                     plural = []
       
   463                 elif cmatches:
       
   464                     for cmatch in cmatches:
       
   465                         out.write(' _(%s) ' % cmatch)
       
   466                 else:
       
   467                     out.write(blankout(t.contents, 'B'))
       
   468             elif t.token_type == TOKEN_VAR:
       
   469                 parts = t.contents.split('|')
       
   470                 cmatch = constant_re.match(parts[0])
       
   471                 if cmatch:
       
   472                     out.write(' _(%s) ' % cmatch.group(1))
       
   473                 for p in parts[1:]:
       
   474                     if p.find(':_(') >= 0:
       
   475                         out.write(' %s ' % p.split(':',1)[1])
       
   476                     else:
       
   477                         out.write(blankout(p, 'F'))
       
   478             else:
       
   479                 out.write(blankout(t.contents, 'X'))
       
   480     return out.getvalue()
       
   481 
       
   482 def parse_accept_lang_header(lang_string):
       
   483     """
       
   484     Parses the lang_string, which is the body of an HTTP Accept-Language
       
   485     header, and returns a list of (lang, q-value), ordered by 'q' values.
       
   486 
       
   487     Any format errors in lang_string results in an empty list being returned.
       
   488     """
       
   489     result = []
       
   490     pieces = accept_language_re.split(lang_string)
       
   491     if pieces[-1]:
       
   492         return []
       
   493     for i in range(0, len(pieces) - 1, 3):
       
   494         first, lang, priority = pieces[i : i + 3]
       
   495         if first:
       
   496             return []
       
   497         priority = priority and float(priority) or 1.0
       
   498         result.append((lang, priority))
       
   499     result.sort(lambda x, y: -cmp(x[1], y[1]))
       
   500     return result
       
   501 
       
   502 # get_date_formats and get_partial_date_formats aren't used anymore by Django
       
   503 # and are kept for backward compatibility.
       
   504 # Note, it's also important to keep format names marked for translation.
       
   505 # For compatibility we still want to have formats on translation catalogs.
       
   506 # That makes template code like {{ my_date|date:_('DATE_FORMAT') }} still work
       
   507 def get_date_formats():
       
   508     """
       
   509     Checks whether translation files provide a translation for some technical
       
   510     message ID to store date and time formats. If it doesn't contain one, the
       
   511     formats provided in the settings will be used.
       
   512     """
       
   513     warnings.warn(
       
   514         "'django.utils.translation.get_date_formats' is deprecated. "
       
   515         "Please update your code to use the new i18n aware formatting.",
       
   516         PendingDeprecationWarning
       
   517     )
       
   518     from django.conf import settings
       
   519     date_format = ugettext('DATE_FORMAT')
       
   520     datetime_format = ugettext('DATETIME_FORMAT')
       
   521     time_format = ugettext('TIME_FORMAT')
       
   522     if date_format == 'DATE_FORMAT':
       
   523         date_format = settings.DATE_FORMAT
       
   524     if datetime_format == 'DATETIME_FORMAT':
       
   525         datetime_format = settings.DATETIME_FORMAT
       
   526     if time_format == 'TIME_FORMAT':
       
   527         time_format = settings.TIME_FORMAT
       
   528     return date_format, datetime_format, time_format
       
   529 
       
   530 def get_partial_date_formats():
       
   531     """
       
   532     Checks whether translation files provide a translation for some technical
       
   533     message ID to store partial date formats. If it doesn't contain one, the
       
   534     formats provided in the settings will be used.
       
   535     """
       
   536     warnings.warn(
       
   537         "'django.utils.translation.get_partial_date_formats' is deprecated. "
       
   538         "Please update your code to use the new i18n aware formatting.",
       
   539         PendingDeprecationWarning
       
   540     )
       
   541     from django.conf import settings
       
   542     year_month_format = ugettext('YEAR_MONTH_FORMAT')
       
   543     month_day_format = ugettext('MONTH_DAY_FORMAT')
       
   544     if year_month_format == 'YEAR_MONTH_FORMAT':
       
   545         year_month_format = settings.YEAR_MONTH_FORMAT
       
   546     if month_day_format == 'MONTH_DAY_FORMAT':
       
   547         month_day_format = settings.MONTH_DAY_FORMAT
       
   548     return year_month_format, month_day_format
       
   549