summaryrefslogtreecommitdiff
path: root/django/utils/translation.py
blob: 0d40a636c828f26e31287db96e1902f1fd54e931 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
"translation helper functions"

import os
import re
import sys
import gettext as gettext_module
from cStringIO import StringIO

from django.utils.functional import lazy

try:
    import threading
    hasThreads = True
except ImportError:
    hasThreads = False

if hasThreads:
    currentThread = threading.currentThread
else:
    def currentThread():
        return 'no threading'

# translations are cached in a dictionary for
# every language+app tuple. The active translations
# are stored by threadid to make them thread local.
_translations = {}
_active = {}

# the default translation is based on the settings file
_default = None

# this is a cache for accept-header to translation
# object mappings to prevent the accept parser to
# run multiple times for one user
_accepted = {}

def to_locale(language):
    "turn a language name (en-us) into a locale name (en_US)"
    p = language.find('-')
    if p >= 0:
        return language[:p].lower()+'_'+language[p+1:].upper()
    else:
        return language.lower()

def to_language(locale):
    "turns a locale name (en_US) into a language name (en-us)"
    p = locale.find('_')
    if p >= 0:
        return locale[:p].lower()+'-'+locale[p+1:].lower()
    else:
        return locale.lower()

class DjangoTranslation(gettext_module.GNUTranslations):
    """
    This class sets up the GNUTranslations context with
    regard to output charset. Django uses a defined
    DEFAULT_CHARSET as the output charset on Python 2.4 -
    with Python 2.3, you need to use DjangoTranslation23.
    """

    def __init__(self, *args, **kw):
        from django.conf import settings
        gettext_module.GNUTranslations.__init__(self, *args, **kw)
        # starting with Python 2.4, there is a function to define
        # the output charset. Before 2.4, the output charset is
        # identical with the translation file charset.
        try:
            self.set_output_charset(settings.DEFAULT_CHARSET)
        except AttributeError:
            pass
        self.django_output_charset = settings.DEFAULT_CHARSET
        self.__language = '??'

    def merge(self, other):
        self._catalog.update(other._catalog)
    
    def set_language(self, language):
        self.__language = language

    def language(self):
        return self.__language
    
    def __repr__(self):
        return "<DjangoTranslation lang:%s>" % self.__language


class DjangoTranslation23(DjangoTranslation):

    """
    This is a compatibility class that is only used with Python 2.3.
    The reason is, Python 2.3 doesn't support set_output_charset on
    translation objects and so needs this wrapper class to make sure
    that input charsets from translation files are correctly translated
    to output charsets.

    With a full switch to Python 2.4, this can be removed from the source.
    """

    def gettext(self, msgid):
        res = self.ugettext(msgid)
        return res.encode(self.django_output_charset)

    def ngettext(self, msgid1, msgid2, n):
        res = self.ungettext(msgid1, msgid2, n)
        return res.encode(self.django_output_charset)

def translation(language):
    """
    This function returns a translation object.  app must be the fully
    qualified name of the application.

    This translation object will be constructed out of multiple GNUTranslations
    objects by merging their catalogs. It will construct a object for the requested
    language and add a fallback to the default language, if that is different
    from the requested language.
    """
    global _translations

    t = _translations.get(language, None)
    if t is not None:
        return t
    
    from django.conf import settings

    # set up the right translation class
    klass = DjangoTranslation
    if sys.version_info < (2, 4):
        klass = DjangoTranslation23

    globalpath = os.path.join(os.path.dirname(settings.__file__), 'locale')

    parts = os.environ['DJANGO_SETTINGS_MODULE'].split('.')
    project = __import__(parts[0], {}, {}, [])
    projectpath = os.path.join(os.path.dirname(project.__file__), 'locale')

    def _fetch(lang, fallback=None):

        global _translations

        loc = to_locale(lang)

        res = _translations.get(lang, None)
        if res is not None:
            return res

        def _translation(path):
            try:
                t = gettext_module.translation('django', path, [loc], klass)
                t.set_language(lang)
                return t
            except IOError, e:
                return None
    
        res = _translation(globalpath)

        def _merge(path):
            t = _translation(path)
            if t is not None:
                if res is None:
                    return t
                else:
                    res.merge(t)
            return res

        if hasattr(settings, 'LOCALE_PATHS'):
            for localepath in settings.LOCALE_PATHS:
                if os.path.isdir(localepath):
                    res = _merge(localepath)

        if os.path.isdir(projectpath):
            res = _merge(projectpath)

        for appname in settings.INSTALLED_APPS:
            p = appname.rfind('.')
            if p >= 0:
                app = getattr(__import__(appname[:p], {}, {}, [appname[p+1:]]), appname[p+1:])
            else:
                app = __import__(appname, {}, {}, [])

            apppath = os.path.join(os.path.dirname(app.__file__), 'locale')

            if os.path.isdir(apppath):
                res = _merge(apppath)
  
        if res is None:
            if fallback is not None:
                res = fallback
            else:
                return gettext_module.NullTranslations()
        _translations[lang] = res
        return res

    default_translation = _fetch(settings.LANGUAGE_CODE)
    current_translation = _fetch(language, fallback=default_translation)

    return current_translation

def activate(language):
    """
    This function fetches the translation object for a given
    tuple of application name and language and installs it as
    the current translation object for the current thread.
    """
    _active[currentThread()] = translation(language)

def deactivate():
    """
    This function deinstalls the currently active translation
    object so that further _ calls will resolve against the
    default translation object, again.
    """
    global _active

    if _active.has_key(currentThread()):
        del _active[currentThread()]

def get_language():
    """
    This function returns the currently selected language.
    """
    t = _active.get(currentThread(), None)
    if t is not None:
        try:
            return to_language(t.language())
        except AttributeError:
            pass
    # if we don't have a real translation object, we assume
    # it's the default language.
    from django.conf.settings import LANGUAGE_CODE
    return LANGUAGE_CODE

def gettext(message):
    """
    This function will be patched into the builtins module to
    provide the _ helper function. It will use the current
    thread as a discriminator to find the translation object
    to use. If no current translation is activated, the
    message will be run through the default translation
    object.
    """
    global _default, _active

    t = _active.get(currentThread(), None)
    if t is not None:
        return t.gettext(message)
    if _default is None:
        from django.conf import settings
        _default = translation(settings.LANGUAGE_CODE)
    return _default.gettext(message)

def gettext_noop(message):
    """
    This function is used to just mark strings for translation
    but to not translate them now. This can be used to store
    strings in global variables that should stay in the base
    language (because they might be used externally) and will
    be translated later on.
    """
    return message

def ngettext(singular, plural, number):
    """
    This function returns the translation of either the singular
    or plural, based on the number.
    """
    global _default, _active

    t = _active.get(currentThread(), None)
    if t is not None:
        return t.ngettext(singular, plural, number)
    if _default is None:
        from django.conf import settings
        _default = translation('*', settings.LANGUAGE_CODE)
    return _default.ngettext(singular, plural, number)

gettext_lazy = lazy(gettext, str)
ngettext_lazy = lazy(ngettext, str)

def check_for_language(lang_code):
    """
    This function checks wether there is a global language
    file for the given language code. This is used to decide
    wether a user-provided language is available.
    """
    from django.conf import settings
    globalpath = os.path.join(os.path.dirname(settings.__file__), 'locale')
    if gettext_module.find('django', globalpath, [to_locale(lang_code)]) is not None:
        return True
    else:
        return False

def get_language_from_request(request):
    """
    analyze the request to find what language the user
    wants the system to show.
    """
    global _accepted

    from django.conf import settings
    globalpath = os.path.join(os.path.dirname(settings.__file__), 'locale')

    if hasattr(request, 'session'):
        lang_code = request.session.get('django_language', None)
        if lang_code is not None and check_for_language(lang_code):
            return lang_code
    
    lang_code = request.COOKIES.get('django_language', None)
    if lang_code is not None and check_for_language(lang_code):
        return lang_code
    
    accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None)
    if accept is not None:

        t = _accepted.get(accept, None)
        if t is not None:
            return t

        def _parsed(el):
            p = el.find(';q=')
            if p >= 0:
                lang = el[:p].strip()
                order = int(float(el[p+3:].strip())*100)
            else:
                lang = el
                order = 100
            if lang.find('-') >= 0:
                (lang, sublang) = lang.split('-')
                lang = lang.lower() + '_' + sublang.upper()
            return (lang, order)

        langs = [_parsed(el) for el in accept.split(',')]
        langs.sort(lambda a,b: -1*cmp(a[1], b[1]))

        for lang, order in langs:
            langfile = gettext_module.find('django', globalpath, [to_locale(lang)])
            if langfile:
                # reconstruct the actual language from the language
                # filename, because otherwise we might incorrectly
                # report de_DE if we only have de available, but
                # did find de_DE because of language normalization
                lang = langfile[len(globalpath):].split('/')[1]
                _accepted[accept] = lang
                return lang
    
    return settings.LANGUAGE_CODE

def install():
    """
    This installs the gettext function as the default
    translation function under the name _.
    """
    __builtins__['_'] = gettext


dot_re = re.compile(r'\S')
def blankout(src, char):
    """
    This is used in the templateize function and changes every
    non-whitespace character to the given char.
    """
    return dot_re.sub(char, src)

inline_re = re.compile(r"""^\s*trans\s+((?:".*?")|(?:'.*?'))\s*""")
block_re = re.compile(r"""^\s*blocktrans(?:\s+|$)""")
endblock_re = re.compile(r"""^\s*endblocktrans$""")
plural_re = re.compile(r"""^\s*plural$""")
constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""")
def templateize(src):
    from django.core.template import tokenize, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK
    """
    This function turns a django template into something that is
    understood by xgettext. It does so by translating the django
    translation tags into standard gettext function invocations.
    """
    out = StringIO()
    intrans = False
    inplural = False
    singular = []
    plural = []
    for t in tokenize(src):
        if intrans:
            if t.token_type == TOKEN_BLOCK:
                endbmatch = endblock_re.match(t.contents)
                pluralmatch = plural_re.match(t.contents)
                if endbmatch:
                    if inplural:
                        out.write(' ngettext(%r,%r,count) ' % (''.join(singular), ''.join(plural)))
                        for part in singular:
                            out.write(blankout(part, 'S'))
                        for part in plural:
                            out.write(blankout(part, 'P'))
                    else:
                        out.write(' gettext(%r) ' % ''.join(singular))
                        for part in singular:
                            out.write(blankout(part, 'S'))
                    intrans = False
                    inplural = False
                    singular = []
                    plural = []
                elif pluralmatch:
                    inplural = True
                else:
                    raise SyntaxError, "translation blocks must not include other block tags: %s" % t.contents
            elif t.token_type == TOKEN_VAR:
                if inplural:
                    plural.append('%%(%s)s' % t.contents)
                else:
                    singular.append('%%(%s)s' % t.contents)
            elif t.token_type == TOKEN_TEXT:
                if inplural:
                    plural.append(t.contents)
                else:
                    singular.append(t.contents)
        else:
            if t.token_type == TOKEN_BLOCK:
                imatch = inline_re.match(t.contents)
                bmatch = block_re.match(t.contents)
                cmatches = constant_re.findall(t.contents)
                if imatch:
                    g = imatch.group(1)
                    if g[0] == '"': g = g.strip('"')
                    elif g[0] == "'": g = g.strip("'")
                    out.write(' gettext(%r) ' % g)
                elif bmatch:
                    intrans = True
                    inplural = False
                    singular = []
                    plural = []
                elif cmatches:
                    for cmatch in cmatches:
                        out.write(' _(%s) ' % cmatch)
                else:
                    out.write(blankout(t.contents, 'B'))
            elif t.token_type == TOKEN_VAR:
                parts = t.contents.split('|')
                cmatch = constant_re.match(parts[0])
                if cmatch:
                    out.write(' _(%s) ' % cmatch.group(1))
                for p in parts[1:]:
                    if p.find(':_(') >= 0:
                        out.write(' %s ' % p.split(':',1)[1])
                    else:
                        out.write(blankout(p, 'F'))
            else:
                out.write(blankout(t.contents, 'X'))
    return out.getvalue()