[tor-commits] [bridgedb/master] Add a language switcher to BridgeDB's web UI.

phw at torproject.org phw at torproject.org
Wed Oct 16 17:13:48 UTC 2019


commit 166e899529784cb979ea9517066783c492a2f143
Author: Philipp Winter <phw at nymity.ch>
Date:   Mon Sep 30 16:06:18 2019 -0700

    Add a language switcher to BridgeDB's web UI.
    
    So far, BridgeDB looked at the user's Accept-Language request header to
    decide what language to use in its web interface.  Not everybody likes
    that, so we should provide an option to override this behaviour.  This
    patch adds a language switcher to BridgeDB's web interface.  It sits at
    the top right and lets the user choose their language.
    
    Some implementation considerations:
    
    * The patch uses BridgeDB's "lang" HTTP GET argument to pass the chosen
      language from one page to another.  This allows us to avoid cookies.
    
    * We allow the user to pick any language that BridgeDB supports,
      regardless of how complete the translations are.
    
    * Each language in the language switcher is translated to the respective
      language, i.e., it says "español" instead of "spanish".
    
    This patch fixes <https://bugs.torproject.org/26543>.
---
 CHANGELOG                                          |  7 +++
 bridgedb/distributors/https/server.py              | 55 +++++++++++++++++++++-
 .../https/templates/assets/css/main.css            |  6 +++
 bridgedb/distributors/https/templates/base.html    | 17 ++++++-
 bridgedb/distributors/https/templates/bridges.html | 14 ++++--
 bridgedb/distributors/https/templates/captcha.html |  2 +-
 bridgedb/distributors/https/templates/howto.html   |  2 +-
 bridgedb/distributors/https/templates/index.html   | 12 ++++-
 bridgedb/distributors/https/templates/options.html |  9 +++-
 bridgedb/test/test_https_server.py                 | 13 +++++
 bridgedb/translations.py                           | 22 +++++++++
 11 files changed, 148 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index be4c6d2..4fe1afc 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,10 @@
+Changes in version A.B.C - YYYY-MM-DD
+
+        * FIXES https://bugs.torproject.org/26543
+        Implement a language switcher that allows users to override the locale
+        that BridgeDB automatically selects by inspecting the client's request
+        headers.
+
 Changes in version 0.8.3 - 2019-10-03
 
         * FIXES https://bugs.torproject.org/31903
diff --git a/bridgedb/distributors/https/server.py b/bridgedb/distributors/https/server.py
index 81bc353..0fb8014 100644
--- a/bridgedb/distributors/https/server.py
+++ b/bridgedb/distributors/https/server.py
@@ -28,6 +28,7 @@ import random
 import re
 import time
 import os
+import operator
 
 from functools import partial
 
@@ -37,6 +38,8 @@ import mako.exceptions
 from mako.template import Template
 from mako.lookup import TemplateLookup
 
+import babel.core
+
 from twisted.internet import defer
 from twisted.internet import reactor
 from twisted.internet import task
@@ -87,6 +90,9 @@ logging.debug("Set template root to %s" % TEMPLATE_DIR)
 #: Localisations which BridgeDB supports which should be rendered right-to-left.
 rtl_langs = ('ar', 'he', 'fa', 'gu_IN', 'ku')
 
+#: A list of supported language tuples. Use getSortedLangList() to read this variable.
+supported_langs = []
+
 # We use our metrics singleton to keep track of BridgeDB metrics such as
 # "number of failed HTTPS bridge requests."
 metrix = metrics.HTTPSMetrics()
@@ -156,6 +162,45 @@ def redirectMaliciousRequest(request):
     return request
 
 
+def getSortedLangList(rebuild=False):
+    """
+    Build and return a list of tuples that contains all of BridgeDB's supported
+    languages, e.g.: [("az", "Azərbaycan"), ("ca", "Català"), ..., ].
+
+    :param rebuild bool: Force a rebuild of ``supported_langs`` if the argument
+        is set to ``True``.  The default is ``False``.
+    :rtype: list
+    :returns: A list of tuples of the form (language-locale, language). The
+        list is sorted alphabetically by language.  We use this list to
+        provide a language switcher in BridgeDB's web interface.
+    """
+
+    # If we already compiled our languages, return them right away.
+    global supported_langs
+    if supported_langs and not rebuild:
+        return supported_langs
+    logging.debug("Building supported languages for language switcher.")
+
+    langDict = {}
+    for l in translations.getSupportedLangs():
+
+        # We don't support 'en_GB', and 'en' and 'en_US' are the same.  'zh_HK'
+        # is very similar to 'zh_TW' and we also lack translators for it, so we
+        # drop the locale: <https://bugs.torproject.org/26543#comment:17>
+        if l in ("en_GB", "en_US", "zh_HK"):
+            continue
+
+        try:
+            langDict[l] = "%s" % (babel.core.Locale.parse(l).display_name.capitalize())
+        except Exception as err:
+            logging.warning("Failed to create language switcher option for %s: %s" % (l, err))
+
+    # Sort languages alphabetically.
+    supported_langs = sorted(langDict.items(), key=operator.itemgetter(1))
+
+    return supported_langs
+
+
 class MaliciousRequest(Exception):
     """Raised when we received a possibly malicious request."""
 
@@ -345,7 +390,11 @@ class TranslatedTemplateResource(CustomErrorHandlingResource, CSPResource):
             langs = translations.getLocaleFromHTTPRequest(request)
             rtl = translations.usingRTLLang(langs)
             template = lookup.get_template(self.template)
-            rendered = template.render(strings, rtl=rtl, lang=langs[0])
+            rendered = template.render(strings,
+                                       getSortedLangList(),
+                                       rtl=rtl,
+                                       lang=langs[0],
+                                       langOverride=translations.isLangOverridden(request))
         except Exception as err:  # pragma: no cover
             rendered = replaceErrorPage(request, err)
         request.setHeader("Content-Type", "text/html; charset=utf-8")
@@ -469,8 +518,10 @@ class CaptchaProtectedResource(CustomErrorHandlingResource, CSPResource):
             imgstr = 'data:image/jpeg;base64,%s' % base64.b64encode(image)
             template = lookup.get_template('captcha.html')
             rendered = template.render(strings,
+                                       getSortedLangList(),
                                        rtl=rtl,
                                        lang=langs[0],
+                                       langOverride=translations.isLangOverridden(request),
                                        imgstr=imgstr,
                                        challenge_field=challenge)
         except Exception as err:
@@ -994,8 +1045,10 @@ class BridgesResource(CustomErrorHandlingResource, CSPResource):
                 rtl = translations.usingRTLLang(langs)
                 template = lookup.get_template('bridges.html')
                 rendered = template.render(strings,
+                                           getSortedLangList(),
                                            rtl=rtl,
                                            lang=langs[0],
+                                           langOverride=translations.isLangOverridden(request),
                                            answer=bridgeLines,
                                            qrcode=qrcode)
             except Exception as err:
diff --git a/bridgedb/distributors/https/templates/assets/css/main.css b/bridgedb/distributors/https/templates/assets/css/main.css
index 2b06a40..72a3205 100644
--- a/bridgedb/distributors/https/templates/assets/css/main.css
+++ b/bridgedb/distributors/https/templates/assets/css/main.css
@@ -439,3 +439,9 @@ div.bridge-lines.-webkit-scrollbar-thumb.horizontal{
 }
 @media (min-width: 1px) and (max-width: 480px), handheld {
 }
+
+.dropdown:hover .dropdown-menu {
+  display: block;
+  height: 350px;
+  overflow: auto;
+}
diff --git a/bridgedb/distributors/https/templates/base.html b/bridgedb/distributors/https/templates/base.html
index 423b43c..00997b2 100644
--- a/bridgedb/distributors/https/templates/base.html
+++ b/bridgedb/distributors/https/templates/base.html
@@ -1,7 +1,7 @@
 ## -*- coding: utf-8 -*-
 
 <%namespace name="base" file="base.html" inheritable="True"/>
-<%page args="strings, rtl=False, lang='en', **kwargs"/>
+<%page args="strings, langs, rtl=False, lang='en', langOverride=False, **kwargs"/>
 
 <!DOCTYPE html>
 <html lang="${lang}">
@@ -37,6 +37,19 @@
               <a  class="navbar-brand" href="../">BridgeDB</a>
             </div>
             <ul class="nav navbar-nav pull-right">
+              <li class="dropdown">
+                <a href="#" class="dropdown-toggle" role="button">
+                  ${_("Language")}<span class="caret"></span>
+                </a>
+                <ul class="dropdown-menu">
+                  % for tuple in langs:
+                  <li>
+                    <a href="?lang=${tuple[0]}">${tuple[1]} (${tuple[0]})</a>
+                  </li>
+                  % endfor
+                </ul>
+              </li>
+
               <li>
                 <a href="https://www.torproject.org">The Tor Project</a>
               </li>
@@ -44,7 +57,7 @@
           </div>
         </div>
 
-${next.body(strings, rtl=rtl, lang=lang, **kwargs)}
+${next.body(strings, langs, rtl=rtl, lang=lang, langOverride=langOverride, **kwargs)}
 
         <div class="faq">
           <div class="row-fluid marketing">
diff --git a/bridgedb/distributors/https/templates/bridges.html b/bridgedb/distributors/https/templates/bridges.html
index 076f930..a7503f5 100644
--- a/bridgedb/distributors/https/templates/bridges.html
+++ b/bridgedb/distributors/https/templates/bridges.html
@@ -1,7 +1,7 @@
 ## -*- coding: utf-8 -*-
 
 <%inherit file="base.html"/>
-<%page args="strings, rtl=False, lang='en', answer=0, qrcode=0, **kwargs"/>
+<%page args="strings, langs, rtl=False, lang='en', langOverride=False, answer=0, qrcode=0, **kwargs"/>
 
 </div>
 
@@ -129,9 +129,15 @@ ${_("""Uh oh, spaghettios!""")}
     </p>
     <p>
       ${_("""There currently aren't any bridges available...""")}
-      ${_(""" Perhaps you should try %s going back %s and choosing a""" \
-          """ different bridge type!""") % \
-         ("""<a class="alert-link" href="options">""", """</a>""")}
+      % if langOverride:
+        ${_(""" Perhaps you should try %s going back %s and choosing a""" \
+            """ different bridge type!""") % \
+           ("""<a class="alert-link" href="options?lang="""+lang+""">""", """</a>""")}
+      % else:
+        ${_(""" Perhaps you should try %s going back %s and choosing a""" \
+            """ different bridge type!""") % \
+           ("""<a class="alert-link" href="options">""", """</a>""")}
+      % endif
     </p>
   </div>
 </div>
diff --git a/bridgedb/distributors/https/templates/captcha.html b/bridgedb/distributors/https/templates/captcha.html
index 33c1d45..1faed49 100644
--- a/bridgedb/distributors/https/templates/captcha.html
+++ b/bridgedb/distributors/https/templates/captcha.html
@@ -1,7 +1,7 @@
 ## -*- coding: utf-8 -*-
 
 <%inherit file="base.html"/>
-<%page args="strings, rtl=False, lang='en', imgstr=0, captcha_challenge=0, **kwargs"/>
+<%page args="strings, langs, rtl=False, lang='en', langOverride=False, imgstr=0, captcha_challenge=0, **kwargs"/>
 
 <div class="container-narrow" id="captcha-submission-container">
   <div class="container-fluid container-fluid-inner-5">
diff --git a/bridgedb/distributors/https/templates/howto.html b/bridgedb/distributors/https/templates/howto.html
index 24e4980..70fca6a 100644
--- a/bridgedb/distributors/https/templates/howto.html
+++ b/bridgedb/distributors/https/templates/howto.html
@@ -1,7 +1,7 @@
 ## -*- coding: utf-8 -*-
 
 <%inherit file="base.html"/>
-<%page args="strings, rtl=False, lang='en', **kwargs"/>
+<%page args="strings, langs, rtl=False, lang='en', langOverride=False, **kwargs"/>
 
 <br />
 
diff --git a/bridgedb/distributors/https/templates/index.html b/bridgedb/distributors/https/templates/index.html
index 269b2ae..2752c5b 100644
--- a/bridgedb/distributors/https/templates/index.html
+++ b/bridgedb/distributors/https/templates/index.html
@@ -1,7 +1,7 @@
 ## -*- coding: utf-8 -*-
 
 <%inherit file="base.html"/>
-<%page args="strings, rtl=False, lang='en', **kwargs"/>
+<%page args="strings, langs, rtl=False, lang='en', langOverride=False, **kwargs"/>
 
 <div class="main-steps">
 <div class="step row" id="step-1">
@@ -24,7 +24,11 @@
       <span class="step-title">
         ${_("Step %s2%s") % ("""<u>""", """</u>""")}</span>
       <span class="step-text">
+      % if langOverride:
+        ${_("Get %s bridges %s") % ("""<a href="/options?lang="""+lang+"""" accesskey="2">""", "</a>")}</span>
+      % else:
         ${_("Get %s bridges %s") % ("""<a href="/options" accesskey="2">""", "</a>")}</span>
+      % endif
     </span>
   </div>
 </div>
@@ -35,9 +39,15 @@
       <span class="step-title">
         ${_("Step %s3%s") % ("""<u>""", """</u>""")}</span>
       <span class="step-text">
+      % if langOverride:
+        ${_("""Now %s add the bridges to Tor Browser %s""") % \
+           ("""<a href="/howto?lang="""+lang+"""" accesskey="3">""",
+            """</a>""")}</span>
+      % else:
         ${_("""Now %s add the bridges to Tor Browser %s""") % \
            ("""<a href="/howto" accesskey="3">""",
             """</a>""")}</span>
+      % endif
     </span>
   </div>
 </div>
diff --git a/bridgedb/distributors/https/templates/options.html b/bridgedb/distributors/https/templates/options.html
index 040d523..b9ae948 100644
--- a/bridgedb/distributors/https/templates/options.html
+++ b/bridgedb/distributors/https/templates/options.html
@@ -1,7 +1,7 @@
 ## -*- coding: utf-8 -*-
 
 <%inherit file="base.html"/>
-<%page args="strings, rtl=False, lang='en', **kwargs"/>
+<%page args="strings, langs, rtl=False, lang='en', langOverride=False, **kwargs"/>
 
 <div class="container-fluid container-fluid-outer-96">
   <!--<div class="container-fluid step-semi-transparent">-->
@@ -26,7 +26,11 @@
       <div class="container-fluid container-fluid-outer">
         <div class="container-fluid-inner-5">
           <p class="bs-component">
+          % if langOverride:
+            <a href="./bridges?lang=${lang}">
+          % else:
             <a href="./bridges">
+          % endif
               <button class="btn btn-success btn-lg btn-block"
                       id="just-give-me-bridges-btn"
                       type="button"
@@ -54,6 +58,9 @@
     <!-- BEGIN bridge options selection form -->
     <form class="form-horizontal" id="advancedOptions" action="bridges" method="GET">
       <fieldset>
+        % if langOverride:
+          <input type="hidden" id="lang" name="lang" value="${lang}">
+        % endif
         <div class="container-fluid" id="instructions">
           <legend id="advanced-options-legend">
             <br />
diff --git a/bridgedb/test/test_https_server.py b/bridgedb/test/test_https_server.py
index d68b880..945ea06 100644
--- a/bridgedb/test/test_https_server.py
+++ b/bridgedb/test/test_https_server.py
@@ -27,6 +27,7 @@ from twisted.trial import unittest
 from twisted.web.resource import Resource
 from twisted.web.test import requesthelper
 
+from bridgedb import translations
 from bridgedb.distributors.https import server
 from bridgedb.schedule import ScheduledInterval
 
@@ -43,6 +44,18 @@ logging.disable(50)
 #server.logging.getLogger().setLevel(10)
 
 
+class GetSortedLangListTests(unittest.TestCase):
+    """Tests for :func:`bridgedb.distributors.https.server.getSortedLangList`."""
+
+    def test_getSortedLangList(self):
+        """getSortedLangList should return a list of tuples containing sorted
+        locales and languages."""
+        origFunc = translations.getSupportedLangs
+        translations.getSupportedLangs = lambda: ["en", "de"]
+        l = server.getSortedLangList(rebuild=True)
+        self.assertEqual(l, [("de", u"Deutsch"), ("en", u"English")])
+        translations.getSupportedLangs = origFunc
+
 class ReplaceErrorPageTests(unittest.TestCase):
     """Tests for :func:`bridgedb.distributors.https.server.replaceErrorPage`."""
 
diff --git a/bridgedb/translations.py b/bridgedb/translations.py
index 7429b60..447e808 100644
--- a/bridgedb/translations.py
+++ b/bridgedb/translations.py
@@ -20,6 +20,28 @@ from bridgedb.parse import headers
 TRANSLATIONS_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'i18n')
 
 
+def isLangOverridden(request):
+    """
+    Return True if the `lang' HTTP GET argument is set in the given request.
+
+    :type request: :api:`twisted.web.server.Request`
+    :param request: An incoming request from a client.
+    :rtype: bool
+    :returns: ``True`` if the given request has a `lang` argument and ``False``
+        otherwise.
+    """
+
+    return request.args.get("lang", [None])[0] is not None
+
+def getSupportedLangs():
+    """Return all supported languages.
+
+    :rtype: set
+    :returns: A set of language locales, e.g.: set(['el', 'eo', ..., ]).
+    """
+
+    return _langs.get_langs()
+
 def getFirstSupportedLang(langs):
     """Return the first language in **langs** that we support.
 





More information about the tor-commits mailing list