Repository: freedomofpress/securedrop

Issue:
Add <html lang="$some_lang"> to HTML templates
# Feature request

## Description

All pages should have the `<html lang="en-US">` or other appropriate language tag.

Reference: https://www.w3schools.com/Tags/ref_language_codes.asp

## User Stories

As a source/journalist, I want my browser to appropriately handle things like CSS text transforms.


Discussion:

Resolving patch:
diff --git a/securedrop/i18n.py b/securedrop/i18n.py
--- a/securedrop/i18n.py
+++ b/securedrop/i18n.py
@@ -22,7 +22,9 @@
 import collections
 import config
 import os
+import re
 
+LOCALE_SPLIT = re.compile('(-|_)')
 LOCALES = set(['en_US'])
 babel = None
 
@@ -129,3 +131,13 @@ def get_locale2name():
             locale = core.Locale.parse(l)
             locale2name[l] = locale.languages[locale.language]
     return locale2name
+
+
+def locale_to_rfc_5646(locale):
+    lower = locale.lower()
+    if 'hant' in lower:
+        return 'zh-Hant'
+    elif 'hans' in lower:
+        return 'zh-Hans'
+    else:
+        return LOCALE_SPLIT.split(locale)[0]
diff --git a/securedrop/journalist.py b/securedrop/journalist.py
--- a/securedrop/journalist.py
+++ b/securedrop/journalist.py
@@ -71,6 +71,7 @@ def setup_g():
 
     g.locale = i18n.get_locale()
     g.text_direction = i18n.get_text_direction(g.locale)
+    g.html_lang = i18n.locale_to_rfc_5646(g.locale)
     g.locales = i18n.get_locale2name()
 
     if request.method == 'POST':
diff --git a/securedrop/source.py b/securedrop/source.py
--- a/securedrop/source.py
+++ b/securedrop/source.py
@@ -95,7 +95,9 @@ def setup_g():
     """Store commonly used values in Flask's special g object"""
     g.locale = i18n.get_locale()
     g.text_direction = i18n.get_text_direction(g.locale)
+    g.html_lang = i18n.locale_to_rfc_5646(g.locale)
     g.locales = i18n.get_locale2name()
+
     # ignore_static here because `crypto_util.hash_codename` is scrypt (very
     # time consuming), and we don't need to waste time running if we're just
     # serving a static resource that won't need to access these common values.


Test patch:
diff --git a/securedrop/tests/test_i18n.py b/securedrop/tests/test_i18n.py
--- a/securedrop/tests/test_i18n.py
+++ b/securedrop/tests/test_i18n.py
@@ -19,6 +19,7 @@
 import argparse
 import logging
 import os
+import re
 
 from flask import request, session, render_template_string, render_template
 from flask_babel import gettext
@@ -32,10 +33,15 @@
 import pytest
 import source
 import version
+import utils
 
 
 class TestI18N(object):
 
+    @classmethod
+    def setup_class(cls):
+        utils.env.setup()
+
     def test_get_supported_locales(self):
         locales = ['en_US', 'fr_FR']
         assert locales == i18n._get_supported_locales(locales, None, None)
@@ -218,6 +224,29 @@ def test_verify_default_locale_en_us_if_not_defined_in_config(self):
         finally:
             config.DEFAULT_LOCALE = DEFAULT_LOCALE
 
+    def test_locale_to_rfc_5646(self):
+        assert i18n.locale_to_rfc_5646('en') == 'en'
+        assert i18n.locale_to_rfc_5646('en-US') == 'en'
+        assert i18n.locale_to_rfc_5646('en_US') == 'en'
+        assert i18n.locale_to_rfc_5646('en-us') == 'en'
+        assert i18n.locale_to_rfc_5646('zh-hant') == 'zh-Hant'
+
+    def test_html_lang_correct(self):
+        app = journalist.app.test_client()
+        resp = app.get('/', follow_redirects=True)
+        html = resp.data.decode('utf-8')
+        assert re.compile('<html .*lang="en".*>').search(html), html
+
+        app = source.app.test_client()
+        resp = app.get('/', follow_redirects=True)
+        html = resp.data.decode('utf-8')
+        assert re.compile('<html .*lang="en".*>').search(html), html
+
+        # check '/generate' too because '/' uses a different template
+        resp = app.get('/generate', follow_redirects=True)
+        html = resp.data.decode('utf-8')
+        assert re.compile('<html .*lang="en".*>').search(html), html
+
     @classmethod
     def teardown_class(cls):
         reload(journalist)