Ticket #3907: trans_real.py

File trans_real.py, 17.1 KB (added by nmariz@…, 17 years ago)

Patch for disabling locate from client browser to be loaded if doesn't exists in settings.py

Line 
1"Translation helper functions"
2
3import os, re, sys
4import gettext as gettext_module
5from cStringIO import StringIO
6
7try:
8 import threading
9 hasThreads = True
10except ImportError:
11 hasThreads = False
12
13if hasThreads:
14 currentThread = threading.currentThread
15else:
16 def currentThread():
17 return 'no threading'
18
19# Translations are cached in a dictionary for every language+app tuple.
20# The active translations are stored by threadid to make them thread local.
21_translations = {}
22_active = {}
23
24# The default translation is based on the settings file.
25_default = None
26
27# This is a cache for accept-header to translation object mappings to prevent
28# the accept parser to run multiple times for one user.
29_accepted = {}
30
31def to_locale(language):
32 "Turns a language name (en-us) into a locale name (en_US)."
33 p = language.find('-')
34 if p >= 0:
35 return language[:p].lower()+'_'+language[p+1:].upper()
36 else:
37 return language.lower()
38
39def to_language(locale):
40 "Turns a locale name (en_US) into a language name (en-us)."
41 p = locale.find('_')
42 if p >= 0:
43 return locale[:p].lower()+'-'+locale[p+1:].lower()
44 else:
45 return locale.lower()
46
47class DjangoTranslation(gettext_module.GNUTranslations):
48 """
49 This class sets up the GNUTranslations context with regard to output
50 charset. Django uses a defined DEFAULT_CHARSET as the output charset on
51 Python 2.4. With Python 2.3, use DjangoTranslation23.
52 """
53 def __init__(self, *args, **kw):
54 from django.conf import settings
55 gettext_module.GNUTranslations.__init__(self, *args, **kw)
56 # Starting with Python 2.4, there's a function to define
57 # the output charset. Before 2.4, the output charset is
58 # identical with the translation file charset.
59 try:
60 self.set_output_charset(settings.DEFAULT_CHARSET)
61 except AttributeError:
62 pass
63 self.django_output_charset = settings.DEFAULT_CHARSET
64 self.__language = '??'
65
66 def merge(self, other):
67 self._catalog.update(other._catalog)
68
69 def set_language(self, language):
70 self.__language = language
71
72 def language(self):
73 return self.__language
74
75 def __repr__(self):
76 return "<DjangoTranslation lang:%s>" % self.__language
77
78class DjangoTranslation23(DjangoTranslation):
79 """
80 Compatibility class that is only used with Python 2.3.
81 Python 2.3 doesn't support set_output_charset on translation objects and
82 needs this wrapper class to make sure input charsets from translation files
83 are correctly translated to output charsets.
84
85 With a full switch to Python 2.4, this can be removed from the source.
86 """
87 def gettext(self, msgid):
88 res = self.ugettext(msgid)
89 return res.encode(self.django_output_charset)
90
91 def ngettext(self, msgid1, msgid2, n):
92 res = self.ungettext(msgid1, msgid2, n)
93 return res.encode(self.django_output_charset)
94
95def translation(language):
96 """
97 Returns a translation object.
98
99 This translation object will be constructed out of multiple GNUTranslations
100 objects by merging their catalogs. It will construct a object for the
101 requested language and add a fallback to the default language, if it's
102 different from the requested language.
103 """
104 global _translations
105
106 t = _translations.get(language, None)
107 if t is not None:
108 return t
109
110 from django.conf import settings
111
112 # set up the right translation class
113 klass = DjangoTranslation
114 if sys.version_info < (2, 4):
115 klass = DjangoTranslation23
116
117 globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
118
119 if settings.SETTINGS_MODULE is not None:
120 parts = settings.SETTINGS_MODULE.split('.')
121 project = __import__(parts[0], {}, {}, [])
122 projectpath = os.path.join(os.path.dirname(project.__file__), 'locale')
123 else:
124 projectpath = None
125
126 def _fetch(lang, fallback=None):
127
128 global _translations
129
130 loc = to_locale(lang)
131
132 res = _translations.get(lang, None)
133 if res is not None:
134 return res
135
136 def _translation(path):
137 try:
138 t = gettext_module.translation('django', path, [loc], klass)
139 t.set_language(lang)
140 return t
141 except IOError, e:
142 return None
143
144 res = _translation(globalpath)
145
146 def _merge(path):
147 t = _translation(path)
148 if t is not None:
149 if res is None:
150 return t
151 else:
152 res.merge(t)
153 return res
154
155 if hasattr(settings, 'LOCALE_PATHS'):
156 for localepath in settings.LOCALE_PATHS:
157 if os.path.isdir(localepath):
158 res = _merge(localepath)
159
160 if projectpath and os.path.isdir(projectpath):
161 res = _merge(projectpath)
162
163 for appname in settings.INSTALLED_APPS:
164 p = appname.rfind('.')
165 if p >= 0:
166 app = getattr(__import__(appname[:p], {}, {}, [appname[p+1:]]), appname[p+1:])
167 else:
168 app = __import__(appname, {}, {}, [])
169
170 apppath = os.path.join(os.path.dirname(app.__file__), 'locale')
171
172 if os.path.isdir(apppath):
173 res = _merge(apppath)
174
175 if res is None:
176 if fallback is not None:
177 res = fallback
178 else:
179 return gettext_module.NullTranslations()
180 _translations[lang] = res
181 return res
182
183 default_translation = _fetch(settings.LANGUAGE_CODE)
184 current_translation = _fetch(language, fallback=default_translation)
185
186 return current_translation
187
188def activate(language):
189 """
190 Fetches the translation object for a given tuple of application name and
191 language and installs it as the current translation object for the current
192 thread.
193 """
194 _active[currentThread()] = translation(language)
195
196def deactivate():
197 """
198 Deinstalls the currently active translation object so that further _ calls
199 will resolve against the default translation object, again.
200 """
201 global _active
202 if _active.has_key(currentThread()):
203 del _active[currentThread()]
204
205def get_language():
206 "Returns the currently selected language."
207 t = _active.get(currentThread(), None)
208 if t is not None:
209 try:
210 return to_language(t.language())
211 except AttributeError:
212 pass
213 # If we don't have a real translation object, assume it's the default language.
214 from django.conf import settings
215 return settings.LANGUAGE_CODE
216
217def get_language_bidi():
218 """
219 Returns selected language's BiDi layout.
220 False = left-to-right layout
221 True = right-to-left layout
222 """
223 from django.conf import settings
224 return get_language() in settings.LANGUAGES_BIDI
225
226def catalog():
227 """
228 This function returns the current active catalog for further processing.
229 This can be used if you need to modify the catalog or want to access the
230 whole message catalog instead of just translating one string.
231 """
232 global _default, _active
233 t = _active.get(currentThread(), None)
234 if t is not None:
235 return t
236 if _default is None:
237 from django.conf import settings
238 _default = translation(settings.LANGUAGE_CODE)
239 return _default
240
241def gettext(message):
242 """
243 This function will be patched into the builtins module to provide the _
244 helper function. It will use the current thread as a discriminator to find
245 the translation object to use. If no current translation is activated, the
246 message will be run through the default translation object.
247 """
248 global _default, _active
249 t = _active.get(currentThread(), None)
250 if t is not None:
251 return t.gettext(message)
252 if _default is None:
253 from django.conf import settings
254 _default = translation(settings.LANGUAGE_CODE)
255 return _default.gettext(message)
256
257def gettext_noop(message):
258 """
259 Marks strings for translation but doesn't translate them now. This can be
260 used to store strings in global variables that should stay in the base
261 language (because they might be used externally) and will be translated later.
262 """
263 return message
264
265def ngettext(singular, plural, number):
266 """
267 Returns the translation of either the singular or plural, based on the number.
268 """
269 global _default, _active
270
271 t = _active.get(currentThread(), None)
272 if t is not None:
273 return t.ngettext(singular, plural, number)
274 if _default is None:
275 from django.conf import settings
276 _default = translation(settings.LANGUAGE_CODE)
277 return _default.ngettext(singular, plural, number)
278
279def check_for_language(lang_code):
280 """
281 Checks whether there is a global language file for the given language code.
282 This is used to decide whether a user-provided language is available. This is
283 only used for language codes from either the cookies or session.
284 """
285 from django.conf import settings
286 globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
287 if gettext_module.find('django', globalpath, [to_locale(lang_code)]) is not None:
288 return True
289 else:
290 return False
291
292def get_language_from_request(request):
293 """
294 Analyzes the request to find what language the user wants the system to show.
295 Only languages listed in settings.LANGUAGES are taken into account. If the user
296 requests a sublanguage where we have a main language, we send out the main language.
297 """
298 global _accepted
299 from django.conf import settings
300 globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
301 supported = dict(settings.LANGUAGES)
302
303 if hasattr(request, 'session'):
304 lang_code = request.session.get('django_language', None)
305 if lang_code in supported and lang_code is not None and check_for_language(lang_code):
306 return lang_code
307
308 lang_code = request.COOKIES.get('django_language', None)
309 if lang_code in supported and lang_code is not None and check_for_language(lang_code):
310 return lang_code
311
312 accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None)
313 if accept is not None:
314
315 t = _accepted.get(accept, None)
316 if t is not None:
317 return t
318
319 def _parsed(el):
320 p = el.find(';q=')
321 if p >= 0:
322 lang = el[:p].strip()
323 order = int(float(el[p+3:].strip())*100)
324 else:
325 lang = el
326 order = 100
327 p = lang.find('-')
328 if p >= 0:
329 mainlang = lang[:p]
330 else:
331 mainlang = lang
332 return (lang, mainlang, order)
333
334 langs = [_parsed(el) for el in accept.split(',')]
335 langs.sort(lambda a,b: -1*cmp(a[2], b[2]))
336
337 for lang, mainlang, order in langs:
338 if lang in supported or mainlang in supported:
339 if not lang in supported and mainlang in supported:
340 lang = mainlang
341 langfile = gettext_module.find('django', globalpath, [to_locale(lang)])
342 if langfile:
343 # reconstruct the actual language from the language
344 # filename, because otherwise we might incorrectly
345 # report de_DE if we only have de available, but
346 # did find de_DE because of language normalization
347 lang = langfile[len(globalpath):].split(os.path.sep)[1]
348 _accepted[accept] = lang
349 return lang
350
351 return settings.LANGUAGE_CODE
352
353def get_date_formats():
354 """
355 This function checks whether translation files provide a translation for some
356 technical message ID to store date and time formats. If it doesn't contain
357 one, the formats provided in the settings will be used.
358 """
359 from django.conf import settings
360 date_format = _('DATE_FORMAT')
361 datetime_format = _('DATETIME_FORMAT')
362 time_format = _('TIME_FORMAT')
363 if date_format == 'DATE_FORMAT':
364 date_format = settings.DATE_FORMAT
365 if datetime_format == 'DATETIME_FORMAT':
366 datetime_format = settings.DATETIME_FORMAT
367 if time_format == 'TIME_FORMAT':
368 time_format = settings.TIME_FORMAT
369 return date_format, datetime_format, time_format
370
371def get_partial_date_formats():
372 """
373 This function checks whether translation files provide a translation for some
374 technical message ID to store partial date formats. If it doesn't contain
375 one, the formats provided in the settings will be used.
376 """
377 from django.conf import settings
378 year_month_format = _('YEAR_MONTH_FORMAT')
379 month_day_format = _('MONTH_DAY_FORMAT')
380 if year_month_format == 'YEAR_MONTH_FORMAT':
381 year_month_format = settings.YEAR_MONTH_FORMAT
382 if month_day_format == 'MONTH_DAY_FORMAT':
383 month_day_format = settings.MONTH_DAY_FORMAT
384 return year_month_format, month_day_format
385
386def install():
387 """
388 Installs the gettext function as the default translation function under
389 the name '_'.
390 """
391 __builtins__['_'] = gettext
392
393dot_re = re.compile(r'\S')
394def blankout(src, char):
395 """
396 Changes every non-whitespace character to the given char.
397 Used in the templatize function.
398 """
399 return dot_re.sub(char, src)
400
401inline_re = re.compile(r"""^\s*trans\s+((?:".*?")|(?:'.*?'))\s*""")
402block_re = re.compile(r"""^\s*blocktrans(?:\s+|$)""")
403endblock_re = re.compile(r"""^\s*endblocktrans$""")
404plural_re = re.compile(r"""^\s*plural$""")
405constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""")
406def templatize(src):
407 """
408 Turns a Django template into something that is understood by xgettext. It
409 does so by translating the Django translation tags into standard gettext
410 function invocations.
411 """
412 from django.template import Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK
413 out = StringIO()
414 intrans = False
415 inplural = False
416 singular = []
417 plural = []
418 for t in Lexer(src, None).tokenize():
419 if intrans:
420 if t.token_type == TOKEN_BLOCK:
421 endbmatch = endblock_re.match(t.contents)
422 pluralmatch = plural_re.match(t.contents)
423 if endbmatch:
424 if inplural:
425 out.write(' ngettext(%r,%r,count) ' % (''.join(singular), ''.join(plural)))
426 for part in singular:
427 out.write(blankout(part, 'S'))
428 for part in plural:
429 out.write(blankout(part, 'P'))
430 else:
431 out.write(' gettext(%r) ' % ''.join(singular))
432 for part in singular:
433 out.write(blankout(part, 'S'))
434 intrans = False
435 inplural = False
436 singular = []
437 plural = []
438 elif pluralmatch:
439 inplural = True
440 else:
441 raise SyntaxError, "Translation blocks must not include other block tags: %s" % t.contents
442 elif t.token_type == TOKEN_VAR:
443 if inplural:
444 plural.append('%%(%s)s' % t.contents)
445 else:
446 singular.append('%%(%s)s' % t.contents)
447 elif t.token_type == TOKEN_TEXT:
448 if inplural:
449 plural.append(t.contents)
450 else:
451 singular.append(t.contents)
452 else:
453 if t.token_type == TOKEN_BLOCK:
454 imatch = inline_re.match(t.contents)
455 bmatch = block_re.match(t.contents)
456 cmatches = constant_re.findall(t.contents)
457 if imatch:
458 g = imatch.group(1)
459 if g[0] == '"': g = g.strip('"')
460 elif g[0] == "'": g = g.strip("'")
461 out.write(' gettext(%r) ' % g)
462 elif bmatch:
463 intrans = True
464 inplural = False
465 singular = []
466 plural = []
467 elif cmatches:
468 for cmatch in cmatches:
469 out.write(' _(%s) ' % cmatch)
470 else:
471 out.write(blankout(t.contents, 'B'))
472 elif t.token_type == TOKEN_VAR:
473 parts = t.contents.split('|')
474 cmatch = constant_re.match(parts[0])
475 if cmatch:
476 out.write(' _(%s) ' % cmatch.group(1))
477 for p in parts[1:]:
478 if p.find(':_(') >= 0:
479 out.write(' %s ' % p.split(':',1)[1])
480 else:
481 out.write(blankout(p, 'F'))
482 else:
483 out.write(blankout(t.contents, 'X'))
484 return out.getvalue()
485
486def string_concat(*strings):
487 """"
488 lazy variant of string concatenation, needed for translations that are
489 constructed from multiple parts. Handles lazy strings and non-strings by
490 first turning all arguments to strings, before joining them.
491 """
492 return ''.join([str(el) for el in strings])
493
Back to Top