commit 166e899529784cb979ea9517066783c492a2f143 Author: Philipp Winter phw@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.