Ticket #3907: trans_real.py

File trans_real.py, 17.1 KB (added by nmariz@…, 8 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