OLD | NEW |
(Empty) | |
| 1 """Translation helper functions.""" |
| 2 from __future__ import unicode_literals |
| 3 |
| 4 import gettext as gettext_module |
| 5 import os |
| 6 import re |
| 7 import sys |
| 8 import warnings |
| 9 from collections import OrderedDict |
| 10 from threading import local |
| 11 |
| 12 from django.apps import apps |
| 13 from django.conf import settings |
| 14 from django.conf.locale import LANG_INFO |
| 15 from django.core.exceptions import AppRegistryNotReady |
| 16 from django.core.signals import setting_changed |
| 17 from django.dispatch import receiver |
| 18 from django.utils import lru_cache, six |
| 19 from django.utils._os import upath |
| 20 from django.utils.encoding import force_text |
| 21 from django.utils.safestring import SafeData, mark_safe |
| 22 from django.utils.translation import LANGUAGE_SESSION_KEY |
| 23 |
| 24 # Translations are cached in a dictionary for every language. |
| 25 # The active translations are stored by threadid to make them thread local. |
| 26 _translations = {} |
| 27 _active = local() |
| 28 |
| 29 # The default translation is based on the settings file. |
| 30 _default = None |
| 31 |
| 32 # magic gettext number to separate context from message |
| 33 CONTEXT_SEPARATOR = "\x04" |
| 34 |
| 35 # Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9 |
| 36 # and RFC 3066, section 2.1 |
| 37 accept_language_re = re.compile(r''' |
| 38 ([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*) # "en", "en-au", "x-y-z",
"es-419", "*" |
| 39 (?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:\.0{,3})?))? # Optional "q=1.00", "q=0.
8" |
| 40 (?:\s*,\s*|$) # Multiple accepts per hea
der. |
| 41 ''', re.VERBOSE) |
| 42 |
| 43 language_code_re = re.compile( |
| 44 r'^[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:@[a-z0-9]{1,20})?$', |
| 45 re.IGNORECASE |
| 46 ) |
| 47 |
| 48 language_code_prefix_re = re.compile(r'^/(\w+([@-]\w+)?)(/|$)') |
| 49 |
| 50 |
| 51 @receiver(setting_changed) |
| 52 def reset_cache(**kwargs): |
| 53 """ |
| 54 Reset global state when LANGUAGES setting has been changed, as some |
| 55 languages should no longer be accepted. |
| 56 """ |
| 57 if kwargs['setting'] in ('LANGUAGES', 'LANGUAGE_CODE'): |
| 58 check_for_language.cache_clear() |
| 59 get_languages.cache_clear() |
| 60 get_supported_language_variant.cache_clear() |
| 61 |
| 62 |
| 63 def to_locale(language, to_lower=False): |
| 64 """ |
| 65 Turns a language name (en-us) into a locale name (en_US). If 'to_lower' is |
| 66 True, the last component is lower-cased (en_us). |
| 67 """ |
| 68 p = language.find('-') |
| 69 if p >= 0: |
| 70 if to_lower: |
| 71 return language[:p].lower() + '_' + language[p + 1:].lower() |
| 72 else: |
| 73 # Get correct locale for sr-latn |
| 74 if len(language[p + 1:]) > 2: |
| 75 return language[:p].lower() + '_' + language[p + 1].upper() + la
nguage[p + 2:].lower() |
| 76 return language[:p].lower() + '_' + language[p + 1:].upper() |
| 77 else: |
| 78 return language.lower() |
| 79 |
| 80 |
| 81 def to_language(locale): |
| 82 """Turns a locale name (en_US) into a language name (en-us).""" |
| 83 p = locale.find('_') |
| 84 if p >= 0: |
| 85 return locale[:p].lower() + '-' + locale[p + 1:].lower() |
| 86 else: |
| 87 return locale.lower() |
| 88 |
| 89 |
| 90 class DjangoTranslation(gettext_module.GNUTranslations): |
| 91 """ |
| 92 This class sets up the GNUTranslations context with regard to output |
| 93 charset. |
| 94 |
| 95 This translation object will be constructed out of multiple GNUTranslations |
| 96 objects by merging their catalogs. It will construct an object for the |
| 97 requested language and add a fallback to the default language, if it's |
| 98 different from the requested language. |
| 99 """ |
| 100 domain = 'django' |
| 101 |
| 102 def __init__(self, language, domain=None, localedirs=None): |
| 103 """Create a GNUTranslations() using many locale directories""" |
| 104 gettext_module.GNUTranslations.__init__(self) |
| 105 if domain is not None: |
| 106 self.domain = domain |
| 107 self.set_output_charset('utf-8') # For Python 2 gettext() (#25720) |
| 108 |
| 109 self.__language = language |
| 110 self.__to_language = to_language(language) |
| 111 self.__locale = to_locale(language) |
| 112 self._catalog = None |
| 113 # If a language doesn't have a catalog, use the Germanic default for |
| 114 # pluralization: anything except one is pluralized. |
| 115 self.plural = lambda n: int(n != 1) |
| 116 |
| 117 if self.domain == 'django': |
| 118 if localedirs is not None: |
| 119 # A module-level cache is used for caching 'django' translations |
| 120 warnings.warn("localedirs is ignored when domain is 'django'.",
RuntimeWarning) |
| 121 localedirs = None |
| 122 self._init_translation_catalog() |
| 123 |
| 124 if localedirs: |
| 125 for localedir in localedirs: |
| 126 translation = self._new_gnu_trans(localedir) |
| 127 self.merge(translation) |
| 128 else: |
| 129 self._add_installed_apps_translations() |
| 130 |
| 131 self._add_local_translations() |
| 132 if self.__language == settings.LANGUAGE_CODE and self.domain == 'django'
and self._catalog is None: |
| 133 # default lang should have at least one translation file available. |
| 134 raise IOError("No translation files found for default language %s."
% settings.LANGUAGE_CODE) |
| 135 self._add_fallback(localedirs) |
| 136 if self._catalog is None: |
| 137 # No catalogs found for this language, set an empty catalog. |
| 138 self._catalog = {} |
| 139 |
| 140 def __repr__(self): |
| 141 return "<DjangoTranslation lang:%s>" % self.__language |
| 142 |
| 143 def _new_gnu_trans(self, localedir, use_null_fallback=True): |
| 144 """ |
| 145 Returns a mergeable gettext.GNUTranslations instance. |
| 146 |
| 147 A convenience wrapper. By default gettext uses 'fallback=False'. |
| 148 Using param `use_null_fallback` to avoid confusion with any other |
| 149 references to 'fallback'. |
| 150 """ |
| 151 return gettext_module.translation( |
| 152 domain=self.domain, |
| 153 localedir=localedir, |
| 154 languages=[self.__locale], |
| 155 codeset='utf-8', |
| 156 fallback=use_null_fallback) |
| 157 |
| 158 def _init_translation_catalog(self): |
| 159 """Creates a base catalog using global django translations.""" |
| 160 settingsfile = upath(sys.modules[settings.__module__].__file__) |
| 161 localedir = os.path.join(os.path.dirname(settingsfile), 'locale') |
| 162 translation = self._new_gnu_trans(localedir) |
| 163 self.merge(translation) |
| 164 |
| 165 def _add_installed_apps_translations(self): |
| 166 """Merges translations from each installed app.""" |
| 167 try: |
| 168 app_configs = reversed(list(apps.get_app_configs())) |
| 169 except AppRegistryNotReady: |
| 170 raise AppRegistryNotReady( |
| 171 "The translation infrastructure cannot be initialized before the
" |
| 172 "apps registry is ready. Check that you don't make non-lazy " |
| 173 "gettext calls at import time.") |
| 174 for app_config in app_configs: |
| 175 localedir = os.path.join(app_config.path, 'locale') |
| 176 if os.path.exists(localedir): |
| 177 translation = self._new_gnu_trans(localedir) |
| 178 self.merge(translation) |
| 179 |
| 180 def _add_local_translations(self): |
| 181 """Merges translations defined in LOCALE_PATHS.""" |
| 182 for localedir in reversed(settings.LOCALE_PATHS): |
| 183 translation = self._new_gnu_trans(localedir) |
| 184 self.merge(translation) |
| 185 |
| 186 def _add_fallback(self, localedirs=None): |
| 187 """Sets the GNUTranslations() fallback with the default language.""" |
| 188 # Don't set a fallback for the default language or any English variant |
| 189 # (as it's empty, so it'll ALWAYS fall back to the default language) |
| 190 if self.__language == settings.LANGUAGE_CODE or self.__language.startswi
th('en'): |
| 191 return |
| 192 if self.domain == 'django': |
| 193 # Get from cache |
| 194 default_translation = translation(settings.LANGUAGE_CODE) |
| 195 else: |
| 196 default_translation = DjangoTranslation( |
| 197 settings.LANGUAGE_CODE, domain=self.domain, localedirs=localedir
s |
| 198 ) |
| 199 self.add_fallback(default_translation) |
| 200 |
| 201 def merge(self, other): |
| 202 """Merge another translation into this catalog.""" |
| 203 if not getattr(other, '_catalog', None): |
| 204 return # NullTranslations() has no _catalog |
| 205 if self._catalog is None: |
| 206 # Take plural and _info from first catalog found (generally Django's
). |
| 207 self.plural = other.plural |
| 208 self._info = other._info.copy() |
| 209 self._catalog = other._catalog.copy() |
| 210 else: |
| 211 self._catalog.update(other._catalog) |
| 212 |
| 213 def language(self): |
| 214 """Returns the translation language.""" |
| 215 return self.__language |
| 216 |
| 217 def to_language(self): |
| 218 """Returns the translation language name.""" |
| 219 return self.__to_language |
| 220 |
| 221 |
| 222 def translation(language): |
| 223 """ |
| 224 Returns a translation object in the default 'django' domain. |
| 225 """ |
| 226 global _translations |
| 227 if language not in _translations: |
| 228 _translations[language] = DjangoTranslation(language) |
| 229 return _translations[language] |
| 230 |
| 231 |
| 232 def activate(language): |
| 233 """ |
| 234 Fetches the translation object for a given language and installs it as the |
| 235 current translation object for the current thread. |
| 236 """ |
| 237 if not language: |
| 238 return |
| 239 _active.value = translation(language) |
| 240 |
| 241 |
| 242 def deactivate(): |
| 243 """ |
| 244 Deinstalls the currently active translation object so that further _ calls |
| 245 will resolve against the default translation object, again. |
| 246 """ |
| 247 if hasattr(_active, "value"): |
| 248 del _active.value |
| 249 |
| 250 |
| 251 def deactivate_all(): |
| 252 """ |
| 253 Makes the active translation object a NullTranslations() instance. This is |
| 254 useful when we want delayed translations to appear as the original string |
| 255 for some reason. |
| 256 """ |
| 257 _active.value = gettext_module.NullTranslations() |
| 258 _active.value.to_language = lambda *args: None |
| 259 |
| 260 |
| 261 def get_language(): |
| 262 """Returns the currently selected language.""" |
| 263 t = getattr(_active, "value", None) |
| 264 if t is not None: |
| 265 try: |
| 266 return t.to_language() |
| 267 except AttributeError: |
| 268 pass |
| 269 # If we don't have a real translation object, assume it's the default langua
ge. |
| 270 return settings.LANGUAGE_CODE |
| 271 |
| 272 |
| 273 def get_language_bidi(): |
| 274 """ |
| 275 Returns selected language's BiDi layout. |
| 276 |
| 277 * False = left-to-right layout |
| 278 * True = right-to-left layout |
| 279 """ |
| 280 lang = get_language() |
| 281 if lang is None: |
| 282 return False |
| 283 else: |
| 284 base_lang = get_language().split('-')[0] |
| 285 return base_lang in settings.LANGUAGES_BIDI |
| 286 |
| 287 |
| 288 def catalog(): |
| 289 """ |
| 290 Returns the current active catalog for further processing. |
| 291 This can be used if you need to modify the catalog or want to access the |
| 292 whole message catalog instead of just translating one string. |
| 293 """ |
| 294 global _default |
| 295 |
| 296 t = getattr(_active, "value", None) |
| 297 if t is not None: |
| 298 return t |
| 299 if _default is None: |
| 300 _default = translation(settings.LANGUAGE_CODE) |
| 301 return _default |
| 302 |
| 303 |
| 304 def do_translate(message, translation_function): |
| 305 """ |
| 306 Translates 'message' using the given 'translation_function' name -- which |
| 307 will be either gettext or ugettext. It uses the current thread to find the |
| 308 translation object to use. If no current translation is activated, the |
| 309 message will be run through the default translation object. |
| 310 """ |
| 311 global _default |
| 312 |
| 313 # str() is allowing a bytestring message to remain bytestring on Python 2 |
| 314 eol_message = message.replace(str('\r\n'), str('\n')).replace(str('\r'), str
('\n')) |
| 315 |
| 316 if len(eol_message) == 0: |
| 317 # Returns an empty value of the corresponding type if an empty message |
| 318 # is given, instead of metadata, which is the default gettext behavior. |
| 319 result = type(message)("") |
| 320 else: |
| 321 _default = _default or translation(settings.LANGUAGE_CODE) |
| 322 translation_object = getattr(_active, "value", _default) |
| 323 |
| 324 result = getattr(translation_object, translation_function)(eol_message) |
| 325 |
| 326 if isinstance(message, SafeData): |
| 327 return mark_safe(result) |
| 328 |
| 329 return result |
| 330 |
| 331 |
| 332 def gettext(message): |
| 333 """ |
| 334 Returns a string of the translation of the message. |
| 335 |
| 336 Returns a string on Python 3 and an UTF-8-encoded bytestring on Python 2. |
| 337 """ |
| 338 return do_translate(message, 'gettext') |
| 339 |
| 340 |
| 341 if six.PY3: |
| 342 ugettext = gettext |
| 343 else: |
| 344 def ugettext(message): |
| 345 return do_translate(message, 'ugettext') |
| 346 |
| 347 |
| 348 def pgettext(context, message): |
| 349 msg_with_ctxt = "%s%s%s" % (context, CONTEXT_SEPARATOR, message) |
| 350 result = ugettext(msg_with_ctxt) |
| 351 if CONTEXT_SEPARATOR in result: |
| 352 # Translation not found |
| 353 # force unicode, because lazy version expects unicode |
| 354 result = force_text(message) |
| 355 return result |
| 356 |
| 357 |
| 358 def gettext_noop(message): |
| 359 """ |
| 360 Marks strings for translation but doesn't translate them now. This can be |
| 361 used to store strings in global variables that should stay in the base |
| 362 language (because they might be used externally) and will be translated |
| 363 later. |
| 364 """ |
| 365 return message |
| 366 |
| 367 |
| 368 def do_ntranslate(singular, plural, number, translation_function): |
| 369 global _default |
| 370 |
| 371 t = getattr(_active, "value", None) |
| 372 if t is not None: |
| 373 return getattr(t, translation_function)(singular, plural, number) |
| 374 if _default is None: |
| 375 _default = translation(settings.LANGUAGE_CODE) |
| 376 return getattr(_default, translation_function)(singular, plural, number) |
| 377 |
| 378 |
| 379 def ngettext(singular, plural, number): |
| 380 """ |
| 381 Returns a string of the translation of either the singular or plural, |
| 382 based on the number. |
| 383 |
| 384 Returns a string on Python 3 and an UTF-8-encoded bytestring on Python 2. |
| 385 """ |
| 386 return do_ntranslate(singular, plural, number, 'ngettext') |
| 387 |
| 388 |
| 389 if six.PY3: |
| 390 ungettext = ngettext |
| 391 else: |
| 392 def ungettext(singular, plural, number): |
| 393 """ |
| 394 Returns a unicode strings of the translation of either the singular or |
| 395 plural, based on the number. |
| 396 """ |
| 397 return do_ntranslate(singular, plural, number, 'ungettext') |
| 398 |
| 399 |
| 400 def npgettext(context, singular, plural, number): |
| 401 msgs_with_ctxt = ("%s%s%s" % (context, CONTEXT_SEPARATOR, singular), |
| 402 "%s%s%s" % (context, CONTEXT_SEPARATOR, plural), |
| 403 number) |
| 404 result = ungettext(*msgs_with_ctxt) |
| 405 if CONTEXT_SEPARATOR in result: |
| 406 # Translation not found |
| 407 result = ungettext(singular, plural, number) |
| 408 return result |
| 409 |
| 410 |
| 411 def all_locale_paths(): |
| 412 """ |
| 413 Returns a list of paths to user-provides languages files. |
| 414 """ |
| 415 globalpath = os.path.join( |
| 416 os.path.dirname(upath(sys.modules[settings.__module__].__file__)), 'loca
le') |
| 417 return [globalpath] + list(settings.LOCALE_PATHS) |
| 418 |
| 419 |
| 420 @lru_cache.lru_cache(maxsize=1000) |
| 421 def check_for_language(lang_code): |
| 422 """ |
| 423 Checks whether there is a global language file for the given language |
| 424 code. This is used to decide whether a user-provided language is |
| 425 available. |
| 426 |
| 427 lru_cache should have a maxsize to prevent from memory exhaustion attacks, |
| 428 as the provided language codes are taken from the HTTP request. See also |
| 429 <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>. |
| 430 """ |
| 431 # First, a quick check to make sure lang_code is well-formed (#21458) |
| 432 if lang_code is None or not language_code_re.search(lang_code): |
| 433 return False |
| 434 for path in all_locale_paths(): |
| 435 if gettext_module.find('django', path, [to_locale(lang_code)]) is not No
ne: |
| 436 return True |
| 437 return False |
| 438 |
| 439 |
| 440 @lru_cache.lru_cache() |
| 441 def get_languages(): |
| 442 """ |
| 443 Cache of settings.LANGUAGES in an OrderedDict for easy lookups by key. |
| 444 """ |
| 445 return OrderedDict(settings.LANGUAGES) |
| 446 |
| 447 |
| 448 @lru_cache.lru_cache(maxsize=1000) |
| 449 def get_supported_language_variant(lang_code, strict=False): |
| 450 """ |
| 451 Returns the language-code that's listed in supported languages, possibly |
| 452 selecting a more generic variant. Raises LookupError if nothing found. |
| 453 |
| 454 If `strict` is False (the default), the function will look for an alternativ
e |
| 455 country-specific variant when the currently checked is not found. |
| 456 |
| 457 lru_cache should have a maxsize to prevent from memory exhaustion attacks, |
| 458 as the provided language codes are taken from the HTTP request. See also |
| 459 <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>. |
| 460 """ |
| 461 if lang_code: |
| 462 # If 'fr-ca' is not supported, try special fallback or language-only 'fr
'. |
| 463 possible_lang_codes = [lang_code] |
| 464 try: |
| 465 possible_lang_codes.extend(LANG_INFO[lang_code]['fallback']) |
| 466 except KeyError: |
| 467 pass |
| 468 generic_lang_code = lang_code.split('-')[0] |
| 469 possible_lang_codes.append(generic_lang_code) |
| 470 supported_lang_codes = get_languages() |
| 471 |
| 472 for code in possible_lang_codes: |
| 473 if code in supported_lang_codes and check_for_language(code): |
| 474 return code |
| 475 if not strict: |
| 476 # if fr-fr is not supported, try fr-ca. |
| 477 for supported_code in supported_lang_codes: |
| 478 if supported_code.startswith(generic_lang_code + '-'): |
| 479 return supported_code |
| 480 raise LookupError(lang_code) |
| 481 |
| 482 |
| 483 def get_language_from_path(path, strict=False): |
| 484 """ |
| 485 Returns the language-code if there is a valid language-code |
| 486 found in the `path`. |
| 487 |
| 488 If `strict` is False (the default), the function will look for an alternativ
e |
| 489 country-specific variant when the currently checked is not found. |
| 490 """ |
| 491 regex_match = language_code_prefix_re.match(path) |
| 492 if not regex_match: |
| 493 return None |
| 494 lang_code = regex_match.group(1) |
| 495 try: |
| 496 return get_supported_language_variant(lang_code, strict=strict) |
| 497 except LookupError: |
| 498 return None |
| 499 |
| 500 |
| 501 def get_language_from_request(request, check_path=False): |
| 502 """ |
| 503 Analyzes the request to find what language the user wants the system to |
| 504 show. Only languages listed in settings.LANGUAGES are taken into account. |
| 505 If the user requests a sublanguage where we have a main language, we send |
| 506 out the main language. |
| 507 |
| 508 If check_path is True, the URL path prefix will be checked for a language |
| 509 code, otherwise this is skipped for backwards compatibility. |
| 510 """ |
| 511 if check_path: |
| 512 lang_code = get_language_from_path(request.path_info) |
| 513 if lang_code is not None: |
| 514 return lang_code |
| 515 |
| 516 supported_lang_codes = get_languages() |
| 517 |
| 518 if hasattr(request, 'session'): |
| 519 lang_code = request.session.get(LANGUAGE_SESSION_KEY) |
| 520 if lang_code in supported_lang_codes and lang_code is not None and check
_for_language(lang_code): |
| 521 return lang_code |
| 522 |
| 523 lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME) |
| 524 |
| 525 try: |
| 526 return get_supported_language_variant(lang_code) |
| 527 except LookupError: |
| 528 pass |
| 529 |
| 530 accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '') |
| 531 for accept_lang, unused in parse_accept_lang_header(accept): |
| 532 if accept_lang == '*': |
| 533 break |
| 534 |
| 535 if not language_code_re.search(accept_lang): |
| 536 continue |
| 537 |
| 538 try: |
| 539 return get_supported_language_variant(accept_lang) |
| 540 except LookupError: |
| 541 continue |
| 542 |
| 543 try: |
| 544 return get_supported_language_variant(settings.LANGUAGE_CODE) |
| 545 except LookupError: |
| 546 return settings.LANGUAGE_CODE |
| 547 |
| 548 |
| 549 def parse_accept_lang_header(lang_string): |
| 550 """ |
| 551 Parses the lang_string, which is the body of an HTTP Accept-Language |
| 552 header, and returns a list of (lang, q-value), ordered by 'q' values. |
| 553 |
| 554 Any format errors in lang_string results in an empty list being returned. |
| 555 """ |
| 556 result = [] |
| 557 pieces = accept_language_re.split(lang_string.lower()) |
| 558 if pieces[-1]: |
| 559 return [] |
| 560 for i in range(0, len(pieces) - 1, 3): |
| 561 first, lang, priority = pieces[i:i + 3] |
| 562 if first: |
| 563 return [] |
| 564 if priority: |
| 565 priority = float(priority) |
| 566 else: |
| 567 priority = 1.0 |
| 568 result.append((lang, priority)) |
| 569 result.sort(key=lambda k: k[1], reverse=True) |
| 570 return result |
OLD | NEW |