Code

Ticket #4030: 4030_language_info.diff

File 4030_language_info.diff, 20.9 KB (added by akaihola, 6 years ago)

implementation of the 2008-04-22 design, with tests and documentation

Line 
1Index: django/conf/language_info.py
2===================================================================
3--- django/conf/language_info.py        (revision 0)
4+++ django/conf/language_info.py        (revision 0)
5@@ -0,0 +1,188 @@
6+language_info = {'ar': {'bidi': True,
7+        'language_code': 'ar',
8+        'name': 'Arabic',
9+        'name_local': u'Arabic'},
10+ 'bg': {'bidi': False,
11+        'language_code': 'bg',
12+        'name': 'Bulgarian',
13+        'name_local': u'\u0431\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438'},
14+ 'bn': {'bidi': False,
15+        'language_code': 'bn',
16+        'name': 'Bengali',
17+        'name_local': u'\u09ac\u09be\u0982\u09b2\u09be'},
18+ 'ca': {'bidi': False,
19+        'language_code': 'ca',
20+        'name': 'Catalan',
21+        'name_local': u'Catal\xe0'},
22+ 'cs': {'bidi': False,
23+        'language_code': 'cs',
24+        'name': 'Czech',
25+        'name_local': u'\u010desky'},
26+ 'cy': {'bidi': False,
27+        'language_code': 'cy',
28+        'name': 'Welsh',
29+        'name_local': u'Cymraeg'},
30+ 'da': {'bidi': False,
31+        'language_code': 'da',
32+        'name': 'Danish',
33+        'name_local': u'Dansk'},
34+ 'de': {'bidi': False,
35+        'language_code': 'de',
36+        'name': 'German',
37+        'name_local': u'Deutsch'},
38+ 'el': {'bidi': False,
39+        'language_code': 'el',
40+        'name': 'Greek',
41+        'name_local': u'\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac'},
42+ 'en': {'bidi': False,
43+        'language_code': 'en',
44+        'name': 'English',
45+        'name_local': u'English'},
46+ 'es': {'bidi': False,
47+        'language_code': 'es',
48+        'name': 'Spanish',
49+        'name_local': u'Espa\xf1ol'},
50+ 'es-ar': {'bidi': False,
51+           'language_code': 'es-ar',
52+           'name': 'Argentinean Spanish',
53+           'name_local': u'Espa\xf1ol Argentino'},
54+ 'eu': {'bidi': False,
55+        'language_code': 'eu',
56+        'name': 'Basque',
57+        'name_local': u'Basque'},
58+ 'fa': {'bidi': True,
59+        'language_code': 'fa',
60+        'name': 'Persian',
61+        'name_local': u'Persian'},
62+ 'fi': {'bidi': False,
63+        'language_code': 'fi',
64+        'name': 'Finnish',
65+        'name_local': u'suomi'},
66+ 'fr': {'bidi': False,
67+        'language_code': 'fr',
68+        'name': 'French',
69+        'name_local': u'Fran\xe7ais'},
70+ 'ga': {'bidi': False,
71+        'language_code': 'ga',
72+        'name': 'Irish',
73+        'name_local': u'Irish'},
74+ 'gl': {'bidi': False,
75+        'language_code': 'gl',
76+        'name': 'Galician',
77+        'name_local': u'galego'},
78+ 'he': {'bidi': True,
79+        'language_code': 'he',
80+        'name': 'Hebrew',
81+        'name_local': u'\u05e2\u05d1\u05e8\u05d9\u05ea - Hebrew'},
82+ 'hr': {'bidi': False,
83+        'language_code': 'hr',
84+        'name': 'Croatian',
85+        'name_local': u'Hrvatski'},
86+ 'hu': {'bidi': False,
87+        'language_code': 'hu',
88+        'name': 'Hungarian',
89+        'name_local': u'Magyar'},
90+ 'is': {'bidi': False,
91+        'language_code': 'is',
92+        'name': 'Icelandic',
93+        'name_local': u'\xcdslenska'},
94+ 'it': {'bidi': False,
95+        'language_code': 'it',
96+        'name': 'Italian',
97+        'name_local': u'Italiano'},
98+ 'ja': {'bidi': False,
99+        'language_code': 'ja',
100+        'name': 'Japanese',
101+        'name_local': u'\u65e5\u672c\u8a9e'},
102+ 'ka': {'bidi': False,
103+        'language_code': 'ka',
104+        'name': 'Georgian',
105+        'name_local': u'\u10e5\u10d0\u10e0\u10d7\u10e3\u10da\u10d8'},
106+ 'km': {'bidi': False,
107+        'language_code': 'km',
108+        'name': 'Khmer',
109+        'name_local': u'Khmer'},
110+ 'kn': {'bidi': False,
111+        'language_code': 'kn',
112+        'name': 'Kannada',
113+        'name_local': u'Kannada'},
114+ 'ko': {'bidi': False,
115+        'language_code': 'ko',
116+        'name': 'Korean',
117+        'name_local': u'\ud55c\uad6d\uc5b4'},
118+ 'lv': {'bidi': False,
119+        'language_code': 'lv',
120+        'name': 'Latvian',
121+        'name_local': u'Latvie\u0161u'},
122+ 'mk': {'bidi': False,
123+        'language_code': 'mk',
124+        'name': 'Macedonian',
125+        'name_local': u'\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438'},
126+ 'nl': {'bidi': False,
127+        'language_code': 'nl',
128+        'name': 'Dutch',
129+        'name_local': u'Nederlands'},
130+ 'no': {'bidi': False,
131+        'language_code': 'no',
132+        'name': 'Norwegian',
133+        'name_local': u'Norsk'},
134+ 'pl': {'bidi': False,
135+        'language_code': 'pl',
136+        'name': 'Polish',
137+        'name_local': u'Polski'},
138+ 'pt': {'bidi': False,
139+        'language_code': 'pt',
140+        'name': 'Portugese',
141+        'name_local': u'Portugese'},
142+ 'pt-br': {'bidi': False,
143+           'language_code': 'pt-br',
144+           'name': 'Brazilian Portuguese',
145+           'name_local': u'Portugu\xeas Brasileiro'},
146+ 'ro': {'bidi': False,
147+        'language_code': 'ro',
148+        'name': 'Romanian',
149+        'name_local': u'Romana'},
150+ 'ru': {'bidi': False,
151+        'language_code': 'ru',
152+        'name': 'Russian',
153+        'name_local': u'\u0420\u0443\u0441\u0441\u043a\u0438\u0439'},
154+ 'sk': {'bidi': False,
155+        'language_code': 'sk',
156+        'name': 'Slovak',
157+        'name_local': u'Slovensky'},
158+ 'sl': {'bidi': False,
159+        'language_code': 'sl',
160+        'name': 'Slovenian',
161+        'name_local': u'Slovenski'},
162+ 'sr': {'bidi': False,
163+        'language_code': 'sr',
164+        'name': 'Serbian',
165+        'name_local': u'Srpski'},
166+ 'sv': {'bidi': False,
167+        'language_code': 'sv',
168+        'name': 'Swedish',
169+        'name_local': u'Svenska'},
170+ 'ta': {'bidi': False,
171+        'language_code': 'ta',
172+        'name': 'Tamil',
173+        'name_local': u'\u0ba4\u0bae\u0bbf\u0bb4\u0bcd'},
174+ 'te': {'bidi': False,
175+        'language_code': 'te',
176+        'name': 'Telugu',
177+        'name_local': u'Telugu'},
178+ 'tr': {'bidi': False,
179+        'language_code': 'tr',
180+        'name': 'Turkish',
181+        'name_local': u'T\xfcrk\xe7e'},
182+ 'uk': {'bidi': False,
183+        'language_code': 'uk',
184+        'name': 'Ukrainian',
185+        'name_local': u'Ukrainian'},
186+ 'zh-cn': {'bidi': False,
187+           'language_code': 'zh-cn',
188+           'name': 'Simplified Chinese',
189+           'name_local': u'\u7b80\u4f53\u4e2d\u6587'},
190+ 'zh-tw': {'bidi': False,
191+           'language_code': 'zh-tw',
192+           'name': 'Traditional Chinese',
193+           'name_local': u'\u7e41\u9ad4\u4e2d\u6587'}}
194
195Property changes on: django/conf/language_info.py
196___________________________________________________________________
197Name: svn:eol-style
198   + native
199
200Index: django/templatetags/i18n.py
201===================================================================
202--- django/templatetags/i18n.py (revision 7438)
203+++ django/templatetags/i18n.py (working copy)
204@@ -17,6 +17,34 @@
205         context[self.variable] = [(k, translation.ugettext(v)) for k, v in settings.LANGUAGES]
206         return ''
207 
208+class GetLanguageInfoNode(Node):
209+    def __init__(self, lang_code, variable):
210+        self.lang_code = Variable(lang_code)
211+        self.variable = variable
212+
213+    def render(self, context):
214+        lang_code = self.lang_code.resolve(context)
215+        context[self.variable] = translation.get_language_info(lang_code)
216+        return ''
217+
218+class GetLanguageInfoListNode(Node):
219+    def __init__(self, languages, variable):
220+        self.languages = Variable(languages)
221+        self.variable = variable
222+
223+    def get_language_info(self, language):
224+        # ``language`` is either a language code string or a sequence with the
225+        # language code as its first item
226+        if len(language[0]) > 1:
227+            return translation.get_language_info(language[0])
228+        else:
229+            return translation.get_language_info(str(language))
230+
231+    def render(self, context):
232+        languages = self.languages.resolve(context)
233+        context[self.variable] = [self.get_language_info(l) for l in languages]
234+        return ''
235+
236 class GetCurrentLanguageNode(Node):
237     def __init__(self, variable):
238         self.variable = variable
239@@ -107,6 +135,55 @@
240         raise TemplateSyntaxError, "'get_available_languages' requires 'as variable' (got %r)" % args
241     return GetAvailableLanguagesNode(args[2])
242 
243+def do_get_language_info(parser, token):
244+    """
245+    This will store the language information dictionary for the given language
246+    code in a context variable.
247+
248+    Usage::
249+
250+        {% get_language_info for language_code as l %}
251+        {{ l.language_code }}
252+        {{ l.name }}
253+        {{ l.name_local }}
254+        {{ l.bidi|yesno:"bi-directional,uni-directional" }}
255+    """
256+    args = token.contents.split()
257+    if len(args) != 5 or args[1] != 'for' or args[3] != 'as':
258+        raise TemplateSyntaxError, "'%s' requires 'for string as variable' (got %r)" % (args[0], args[1:])
259+    return GetLanguageInfoNode(args[2], args[4])
260+
261+def do_get_language_info_list(parser, token):
262+    """
263+    This will store a list of language information dictionaries for the given
264+    language codes in a context variable.  The language codes can be specified
265+    ither as a list of strings or a settings.LANGUAGES style tuple (or any
266+    sequence of sequences whose first items are language codes).
267+
268+    Usage::
269+
270+        {% get_language_info_list for LANGUAGES as langs %}
271+        {% for l in langs %}
272+          {{ l.language_code }}
273+          {{ l.name }}
274+          {{ l.name_local }}
275+          {{ l.bidi|yesno:"bi-directional,uni-directional" }}
276+        {% endfor %}
277+    """
278+    args = token.contents.split()
279+    if len(args) != 5 or args[1] != 'for' or args[3] != 'as':
280+        raise TemplateSyntaxError, "'%s' requires 'for sequence as variable' (got %r)" % (args[0], args[1:])
281+    return GetLanguageInfoListNode(args[2], args[4])
282+
283+def language_name(lang_code):
284+    return translation.get_language_info(lang_code)['name']
285+
286+def language_name_local(lang_code):
287+    return translation.get_language_info(lang_code)['name_local']
288+
289+def language_bidi(lang_code):
290+    return translation.get_language_info(lang_code)['bidi']
291+
292 def do_get_current_language(parser, token):
293     """
294     This will store the current language in the context.
295@@ -253,7 +330,13 @@
296             counter)
297 
298 register.tag('get_available_languages', do_get_available_languages)
299+register.tag('get_language_info', do_get_language_info)
300+register.tag('get_language_info_list', do_get_language_info_list)
301 register.tag('get_current_language', do_get_current_language)
302 register.tag('get_current_language_bidi', do_get_current_language_bidi)
303 register.tag('trans', do_translate)
304 register.tag('blocktrans', do_block_translate)
305+
306+register.filter(language_name)
307+register.filter(language_name_local)
308+register.filter(language_bidi)
309Index: django/utils/translation/__init__.py
310===================================================================
311--- django/utils/translation/__init__.py        (revision 7438)
312+++ django/utils/translation/__init__.py        (working copy)
313@@ -3,6 +3,12 @@
314 """
315 from django.utils.functional import lazy
316 from django.utils.encoding import force_unicode
317+try:
318+    from django.conf.language_info import language_info
319+except ImportError:
320+    language_info = {}
321+    from warnings import warn
322+    warn("Module django.conf.language_info not found. Run django/bin/make-language-info.py to generate it and improve performance.")
323 
324 __all__ = ['gettext', 'gettext_noop', 'gettext_lazy', 'ngettext',
325         'ngettext_lazy', 'string_concat', 'activate', 'deactivate',
326@@ -109,3 +115,21 @@
327     """
328     return u''.join([force_unicode(s) for s in strings])
329 string_concat = lazy(string_concat, unicode)
330+
331+def get_language_info(lang_code):
332+    if lang_code not in language_info:
333+        from django.conf import settings
334+        for code, name in settings.LANGUAGES:
335+            if code == lang_code:
336+                old_lang = get_language()
337+                activate(lang_code)
338+                info = {"language_code": lang_code,
339+                        "name": name,
340+                        "name_local": ugettext(name),
341+                        "bidi": lang_code in settings.LANGUAGES_BIDI}
342+                activate(old_lang)
343+                language_info[lang_code] = info
344+                break
345+        else:
346+            raise KeyError("Unknown language code %r." % lang_code)
347+    return language_info[lang_code]
348Index: django/bin/make-language-info.py
349===================================================================
350--- django/bin/make-language-info.py    (revision 0)
351+++ django/bin/make-language-info.py    (revision 0)
352@@ -0,0 +1,29 @@
353+#!/usr/bin/env python
354+
355+LANGUAGE_INFO_PY_FILE = 'django/conf/language_info.py'
356+
357+# Need to ensure that the i18n framework is enabled
358+from django.conf import settings
359+settings.configure(USE_I18N = True)
360+
361+# Don't display the import warning about the missing language info file
362+import warnings
363+warnings.filterwarnings('ignore', category=UserWarning)
364+
365+from django.utils import translation
366+from pprint import pformat
367+
368+def make_language_info():
369+    language_info = {}
370+    for code, name in settings.LANGUAGES:
371+        translation.activate(code)
372+        language_info[code] = translation.get_language_info(code)
373+    from os.path import join, dirname, abspath
374+    target = abspath(join(dirname(__file__), '..', '..', LANGUAGE_INFO_PY_FILE))
375+    print 'Writing language information to\n%s' % target
376+    file(target, 'w').write('language_info = %s\n' % pformat(language_info))
377+    print 'Done.'
378+
379+
380+if __name__ == "__main__":
381+    make_language_info()
382
383Property changes on: django/bin/make-language-info.py
384___________________________________________________________________
385Name: svn:executable
386   + *
387Name: svn:eol-style
388   + native
389
390Index: tests/regressiontests/i18n/language_info.py
391===================================================================
392--- tests/regressiontests/i18n/language_info.py (revision 0)
393+++ tests/regressiontests/i18n/language_info.py (revision 0)
394@@ -0,0 +1,9 @@
395+tests = """
396+>>> from django.utils.translation import get_language_info
397+>>> from pprint import pprint
398+>>> pprint(get_language_info('de'))
399+{'bidi': False,
400+ 'language_code': 'de',
401+ 'name': 'German',
402+ 'name_local': u'Deutsch'}
403+"""
404
405Property changes on: tests/regressiontests/i18n/language_info.py
406___________________________________________________________________
407Name: svn:eol-style
408   + native
409
410Index: tests/regressiontests/i18n/tests.py
411===================================================================
412--- tests/regressiontests/i18n/tests.py (revision 7438)
413+++ tests/regressiontests/i18n/tests.py (working copy)
414@@ -1,5 +1,5 @@
415 # coding: utf-8
416-import misc
417+import misc, language_info
418 
419 regressions = ur"""
420 Format string interpolation should work with *_lazy objects.
421@@ -68,5 +68,6 @@
422 
423 __test__ = {
424     'regressions': regressions,
425+    'language_info': language_info.tests,
426     'misc': misc.tests,
427 }
428Index: tests/regressiontests/templates/tests.py
429===================================================================
430--- tests/regressiontests/templates/tests.py    (revision 7438)
431+++ tests/regressiontests/templates/tests.py    (working copy)
432@@ -755,6 +755,14 @@
433             'i18n17': ('{% load i18n %}{% blocktrans with anton|escape as berta %}{{ berta }}{% endblocktrans %}', {'anton': 'α & β'}, u'α & β'),
434             'i18n18': ('{% load i18n %}{% blocktrans with anton|force_escape as berta %}{{ berta }}{% endblocktrans %}', {'anton': 'α & β'}, u'α & β'),
435 
436+            # retrieving language information
437+            'i18n19': ('{% load i18n %}{% get_language_info for "de" as l %}{{ l.language_code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}', {}, 'de: German/Deutsch bidi=False'),
438+            'i18n20': ('{% load i18n %}{% get_language_info for LANGUAGE_CODE as l %}{{ l.language_code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}', {'LANGUAGE_CODE': 'fi'}, 'fi: Finnish/suomi bidi=False'),
439+            'i18n21': ('{% load i18n %}{% get_language_info_list for langcodes as langs %}{% for l in langs %}{{ l.language_code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}; {% endfor %}', {'langcodes': ['it', 'no']}, 'it: Italian/Italiano bidi=False; no: Norwegian/Norsk bidi=False; '),
440+            'i18n22': ('{% load i18n %}{% get_language_info_list for langcodes as langs %}{% for l in langs %}{{ l.language_code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}; {% endfor %}', {'langcodes': (('sl', 'Slovenian'), ('fa', 'Persian'))}, 'sl: Slovenian/Slovenski bidi=False; fa: Persian/Persian bidi=True; '),
441+            'i18n23': ('{% load i18n %}{{ "hu"|language_name }} {{ "hu"|language_name_local }} {{ "hu"|language_bidi }}', {}, 'Hungarian Magyar False'),
442+            'i18n24': ('{% load i18n %}{{ langcode|language_name }} {{ langcode|language_name_local }} {{ langcode|language_bidi }}', {'langcode': 'nl'}, 'Dutch Nederlands False'),
443+
444             ### HANDLING OF TEMPLATE_STRING_IF_INVALID ###################################
445 
446             'invalidstr01': ('{{ var|default:"Foo" }}', {}, ('Foo','INVALID')),
447Index: docs/i18n.txt
448===================================================================
449--- docs/i18n.txt       (revision 7438)
450+++ docs/i18n.txt       (working copy)
451@@ -266,7 +266,7 @@
452 Each ``RequestContext`` has access to three translation-specific variables:
453 
454     * ``LANGUAGES`` is a list of tuples in which the first element is the
455-      language code and the second is the language name (in that language).
456+      language code and the second is the language name (in English).
457     * ``LANGUAGE_CODE`` is the current user's preferred language, as a string.
458       Example: ``en-us``. (See "How language preference is discovered", below.)
459     * ``LANGUAGE_BIDI`` is the current language's direction. If True, it's a
460@@ -640,6 +640,64 @@
461 .. _session: ../sessions/
462 .. _request object: ../request_response/#httprequest-objects
463 
464+Local names of languages
465+========================
466+
467+The ``get_language_info()`` function provides detailed information about
468+languages::
469+
470+    >>> form django.utils.translation import get_language_info
471+    >>> l = get_language_info('de')
472+    >>> print l['name'], l['name_local'], l['bidi']
473+    German Deutsch False
474+
475+The ``name`` and ``name_local`` attributes of the dictionary contain the name
476+of the language in English and the language itself, respectively.  The ``bidi``
477+attribute is True only for bi-directional languages.
478+
479+In templates, you can use special template tags or filters to retrieve the same
480+information. To get information about a single language, use the
481+``{% get_language_info %}`` tag::
482+
483+    {% get_language_info for LANGUAGE_CODE as lang %}
484+    {% get_language_info for "pl" as lang %}
485+
486+You can then access the information::
487+
488+    Language code: {{ lang.language_code }}<br />
489+    Name of language: {{ lang.name_local }}<br />
490+    Name in English: {{ lang.name }}<br />
491+    Bi-directional: {{ lang.bidi }}
492+
493+You can also use the ``{% get_language_info_list %}`` template tag to retrieve
494+information for a list of languages (e.g. active languages as specified in
495+``settings.LANGUAGES``).  See `The set_language redirect view`_ for an example
496+of how to display a language selector using ``{% get_language_info_list %}``.
497+
498+In addition to ``settings.LANGUAGES`` style nested tuples,
499+``{% get_language_info_list %}`` supports simple lists of language codes.  If
500+you do this in your view::
501+
502+  return render_to_response('mytemplate.html',
503+                            {'available_languages': ['en', 'es', 'fr']},
504+                            RequestContext(request))
505+
506+you can iterate those languages in the template::
507+
508+  {% get_language_info_list available_languages %}
509+  {% for l in available_languages %} ... {% endfor %}
510+
511+There are also simple filters available for convenience:
512+
513+    * ``{{ LANGUAGE_CODE|language_name }}``  ("German")
514+    * ``{{ LANGUAGE_CODE|language_name_local }}`` ("Deutsch")
515+    * ``{{ LANGUAGE_CODE|bidi }}`` (False)     
516+
517+The source of the language information is the ``django.conf.language_info``
518+module.  If you add a new translation, you can run the
519+``django/bin/make-language-info.py`` script to insert the information for the
520+added language.
521+
522 Using translations in your own projects
523 =======================================
524 
525@@ -732,16 +790,20 @@
526     * If that's empty -- say, if a user's browser suppresses that header --
527       then the user will be redirected to ``/`` (the site root) as a fallback.
528 
529-Here's example HTML template code::
530+Here's example HTML template code (see `Local names of languages`_ for more
531+information)::
532 
533+    {% get_language_info_list for LANGUAGES as available_languages %}
534     <form action="/i18n/setlang/" method="post">
535-    <input name="next" type="hidden" value="/next/page/" />
536-    <select name="language">
537-    {% for lang in LANGUAGES %}
538-    <option value="{{ lang.0 }}">{{ lang.1 }}</option>
539-    {% endfor %}
540-    </select>
541-    <input type="submit" value="Go" />
542+      <input name="next" type="hidden" value="/next/page/" />
543+      <select name="language">
544+        {% for lang in available_languages %}
545+        <option value="{{ lang.language_code }}" {% ifequal lang.language_code LANGUAGE_CODE %}selected{% endifequal %}>
546+          {{ lang.name_local }}
547+        </option>
548+        {% endfor %}
549+      </select>
550+      <input type="submit" value="Go" />
551     </form>
552 
553 Translations and JavaScript