11"Translation helper functions"
22
3- import os , re , sys
3+ import locale
4+ import os
5+ import re
6+ import sys
47import gettext as gettext_module
58from cStringIO import StringIO
69from django .utils .functional import lazy
@@ -25,15 +28,25 @@ def currentThread():
2528# The default translation is based on the settings file.
2629_default = None
2730
28- # This is a cache for accept-header to translation object mappings to prevent
29- # the accept parser to run multiple times for one user .
31+ # This is a cache for normalised accept-header languages to prevent multiple
32+ # file lookups when checking the same locale on repeated requests .
3033_accepted = {}
3134
32- def to_locale (language ):
35+ # Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9.
36+ accept_language_re = re .compile (r'''
37+ ([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*) # "en", "en-au", "x-y-z", "*"
38+ (?:;q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8"
39+ (?:\s*,\s*|$) # Multiple accepts per header.
40+ ''' , re .VERBOSE )
41+
42+ def to_locale (language , to_lower = False ):
3343 "Turns a language name (en-us) into a locale name (en_US)."
3444 p = language .find ('-' )
3545 if p >= 0 :
36- return language [:p ].lower ()+ '_' + language [p + 1 :].upper ()
46+ if to_lower :
47+ return language [:p ].lower ()+ '_' + language [p + 1 :].lower ()
48+ else :
49+ return language [:p ].lower ()+ '_' + language [p + 1 :].upper ()
3750 else :
3851 return language .lower ()
3952
@@ -309,46 +322,40 @@ def get_language_from_request(request):
309322 if lang_code in supported and lang_code is not None and check_for_language (lang_code ):
310323 return lang_code
311324
312- lang_code = request .COOKIES .get ('django_language' , None )
313- if lang_code in supported and lang_code is not None and check_for_language (lang_code ):
325+ lang_code = request .COOKIES .get ('django_language' )
326+ if lang_code and lang_code in supported and check_for_language (lang_code ):
314327 return lang_code
315328
316- accept = request .META .get ('HTTP_ACCEPT_LANGUAGE' , None )
317- if accept is not None :
318-
319- t = _accepted .get (accept , None )
320- if t is not None :
321- return t
322-
323- def _parsed (el ):
324- p = el .find (';q=' )
325- if p >= 0 :
326- lang = el [:p ].strip ()
327- order = int (float (el [p + 3 :].strip ())* 100 )
328- else :
329- lang = el
330- order = 100
331- p = lang .find ('-' )
332- if p >= 0 :
333- mainlang = lang [:p ]
334- else :
335- mainlang = lang
336- return (lang , mainlang , order )
337-
338- langs = [_parsed (el ) for el in accept .split (',' )]
339- langs .sort (lambda a ,b : - 1 * cmp (a [2 ], b [2 ]))
340-
341- for lang , mainlang , order in langs :
342- if lang in supported or mainlang in supported :
343- langfile = gettext_module .find ('django' , globalpath , [to_locale (lang )])
344- if langfile :
345- # reconstruct the actual language from the language
346- # filename, because otherwise we might incorrectly
347- # report de_DE if we only have de available, but
348- # did find de_DE because of language normalization
349- lang = langfile [len (globalpath ):].split (os .path .sep )[1 ]
350- _accepted [accept ] = lang
351- return lang
329+ accept = request .META .get ('HTTP_ACCEPT_LANGUAGE' , '' )
330+ for lang , unused in parse_accept_lang_header (accept ):
331+ if lang == '*' :
332+ break
333+
334+ # We have a very restricted form for our language files (no encoding
335+ # specifier, since they all must be UTF-8 and only one possible
336+ # language each time. So we avoid the overhead of gettext.find() and
337+ # look up the MO file manually.
338+
339+ normalized = locale .locale_alias .get (to_locale (lang , True ))
340+ if not normalized :
341+ continue
342+
343+ # Remove the default encoding from locale_alias
344+ normalized = normalized .split ('.' )[0 ]
345+
346+ if normalized in _accepted :
347+ # We've seen this locale before and have an MO file for it, so no
348+ # need to check again.
349+ return _accepted [normalized ]
350+
351+ for lang in (normalized , normalized .split ('_' )[0 ]):
352+ if lang not in supported :
353+ continue
354+ langfile = os .path .join (globalpath , lang , 'LC_MESSAGES' ,
355+ 'django.mo' )
356+ if os .path .exists (langfile ):
357+ _accepted [normalized ] = lang
358+ return lang
352359
353360 return settings .LANGUAGE_CODE
354361
@@ -494,3 +501,24 @@ def string_concat(*strings):
494501 return '' .join ([str (el ) for el in strings ])
495502
496503string_concat = lazy (string_concat , str )
504+
505+ def parse_accept_lang_header (lang_string ):
506+ """
507+ Parses the lang_string, which is the body of an HTTP Accept-Language
508+ header, and returns a list of (lang, q-value), ordered by 'q' values.
509+
510+ Any format errors in lang_string results in an empty list being returned.
511+ """
512+ result = []
513+ pieces = accept_language_re .split (lang_string )
514+ if pieces [- 1 ]:
515+ return []
516+ for i in range (0 , len (pieces ) - 1 , 3 ):
517+ first , lang , priority = pieces [i : i + 3 ]
518+ if first :
519+ return []
520+ priority = priority and float (priority ) or 1.0
521+ result .append ((lang , priority ))
522+ result .sort (lambda x , y : - cmp (x [1 ], y [1 ]))
523+ return result
524+
0 commit comments