PageRenderTime 29ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/django/utils/translation/trans_real.py

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