We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
make BCP47/RFC5646 `RequestLocaleInfo.language_tag` property available in locale selectors
## Description

In `securedrop/source_templates/locales.html`, the `lang` and `hreflang` attributes require the value of the `RequestLocaleInfo.language_tag` property, which is not available in the `g.locales` dictionary.

## Steps to Reproduce

After #6041, validate the HTML of a Source Interface page including the `locales.html` locale selector (i.e., in any of them).

## Expected Behavior

The page validates.

## Actual Behavior

Some locales' `lang` and `hreflang` attributes are invalid.

## Comments

The trick will be to make the `RequestLocaleInfo.language_tag` property available to the Jinja template, which currently has access only to the contents of the `g.locales` dictionary.

1. [x] `securedrop/i18n.py`: `map_locale_display_names()`: return map of `RequestLocaleInfo` objects
2. [x] `securedrop/{journalist,source}_templates/locales.html`: `s/g.locales[locale]/g.locales[locale].display_name` (pending #6041)
3. [x] `securedrop/{journalist,source}_templates/locales.html`: use `g.locales[locale].language_tag` in `lang` and `hreflang` attributes (pending #6041)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/i18n.py`
Content:
```
1 #
2 # SecureDrop whistleblower submission system
3 # Copyright (C) 2017 Loic Dachary <loic@dachary.org>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Affero General Public License for more details.
14 #
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18 import collections
19 
20 from typing import Dict, List
21 
22 from babel.core import (
23     Locale,
24     UnknownLocaleError,
25     get_locale_identifier,
26     negotiate_locale,
27     parse_locale,
28 )
29 from flask import Flask, g, request, session
30 from flask_babel import Babel
31 
32 from sdconfig import SDConfig
33 
34 
35 class RequestLocaleInfo:
36     """
37     Convenience wrapper around a babel.core.Locale.
38     """
39 
40     def __init__(self, locale: str):
41         self.locale = Locale.parse(locale)
42 
43     def __str__(self) -> str:
44         """
45         The Babel string representation of the locale.
46         """
47         return str(self.locale)
48 
49     @property
50     def text_direction(self) -> str:
51         """
52         The Babel text direction: ltr or rtl.
53 
54         Used primarily to set text direction in HTML via the "dir"
55         attribute.
56         """
57         return self.locale.text_direction
58 
59     @property
60     def language(self) -> str:
61         """
62         The Babel language name.
63 
64         Just the language, without subtag info like region or script.
65         """
66         return self.locale.language
67 
68     @property
69     def id(self) -> str:
70         """
71         The Babel string representation of the locale.
72 
73         This should match the name of the directory containing its
74         translations.
75         """
76         return str(self.locale)
77 
78     @property
79     def language_tag(self) -> str:
80         """
81         Returns a BCP47/RFC5646 language tag for the locale.
82 
83         Language tags are used in HTTP headers and the HTML lang
84         attribute.
85         """
86         return get_locale_identifier(parse_locale(str(self.locale)), sep="-")
87 
88 
89 def configure_babel(config: SDConfig, app: Flask) -> None:
90     """
91     Set up Flask-Babel according to the SecureDrop configuration.
92     """
93     # Tell Babel where to find our translations.
94     translations_directory = str(config.TRANSLATION_DIRS.absolute())
95     app.config["BABEL_TRANSLATION_DIRECTORIES"] = translations_directory
96 
97     # Create the app's Babel instance. Passing the app to the
98     # constructor causes the instance to attach itself to the app.
99     babel = Babel(app)
100 
101     # verify that Babel is only using the translations we told it about
102     if list(babel.translation_directories) != [translations_directory]:
103         raise ValueError(
104             "Babel translation directories ({}) do not match SecureDrop configuration ({})".format(
105                 babel.translation_directories, [translations_directory]
106             )
107         )
108 
109     # register the function used to determine the locale of a request
110     babel.localeselector(lambda: get_locale(config))
111 
112 
113 def validate_locale_configuration(config: SDConfig, app: Flask) -> None:
114     """
115     Ensure that the configured locales are valid and translated.
116     """
117     if config.DEFAULT_LOCALE not in config.SUPPORTED_LOCALES:
118         raise ValueError(
119             'The default locale "{}" is not included in the set of supported locales "{}"'.format(
120                 config.DEFAULT_LOCALE, config.SUPPORTED_LOCALES
121             )
122         )
123 
124     translations = app.babel_instance.list_translations()
125     for locale in config.SUPPORTED_LOCALES:
126         if locale == "en_US":
127             continue
128 
129         parsed = Locale.parse(locale)
130         if parsed not in translations:
131             raise ValueError(
132                 'Configured locale "{}" is not in the set of translated locales "{}"'.format(
133                     parsed, translations
134                 )
135             )
136 
137 
138 LOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, str]
139 
140 
141 def map_locale_display_names(config: SDConfig) -> None:
142     """
143     Create a map of locale identifiers to names for display.
144 
145     For most of our supported languages, we only provide one
146     translation, so including the full display name is not necessary
147     to distinguish them. For languages with more than one translation,
148     like Chinese, we do need the additional detail.
149     """
150     language_locale_counts = collections.defaultdict(int)  # type: Dict[str, int]
151     for l in sorted(config.SUPPORTED_LOCALES):
152         locale = Locale.parse(l)
153         language_locale_counts[locale.language_name] += 1
154 
155     locale_map = collections.OrderedDict()
156     for l in sorted(config.SUPPORTED_LOCALES):
157         locale = Locale.parse(l)
158         if language_locale_counts[locale.language_name] == 1:
159             name = locale.language_name
160         else:
161             name = locale.display_name
162         locale_map[str(locale)] = name
163 
164     global LOCALES
165     LOCALES = locale_map
166 
167 
168 def configure(config: SDConfig, app: Flask) -> None:
169     configure_babel(config, app)
170     validate_locale_configuration(config, app)
171     map_locale_display_names(config)
172 
173 
174 def get_locale(config: SDConfig) -> str:
175     """
176     Return the best supported locale for a request.
177 
178     Get the locale as follows, by order of precedence:
179     - l request argument or session['locale']
180     - browser suggested locale, from the Accept-Languages header
181     - config.DEFAULT_LOCALE
182     """
183     # Default to any locale set in the session.
184     locale = session.get("locale")
185 
186     # A valid locale specified in request.args takes precedence.
187     if request.args.get("l"):
188         negotiated = negotiate_locale([request.args["l"]], LOCALES.keys())
189         if negotiated:
190             locale = negotiated
191 
192     # If the locale is not in the session or request.args, negotiate
193     # the best supported option from the browser's accepted languages.
194     if not locale:
195         locale = negotiate_locale(get_accepted_languages(), LOCALES.keys())
196 
197     # Finally, fall back to the default locale if necessary.
198     return locale or config.DEFAULT_LOCALE
199 
200 
201 def get_accepted_languages() -> List[str]:
202     """
203     Convert a request's list of accepted languages into locale identifiers.
204     """
205     accept_languages = []
206     for l in request.accept_languages.values():
207         try:
208             parsed = Locale.parse(l, "-")
209             accept_languages.append(str(parsed))
210 
211             # We only have two Chinese translations, simplified
212             # and traditional, based on script and not
213             # region. Browsers tend to send identifiers with
214             # region, e.g. zh-CN or zh-TW. Babel can generally
215             # infer the script from those, so we can fabricate a
216             # fallback entry without region, in the hope that it
217             # will match one of our translations and the site will
218             # at least be more legible at first contact than the
219             # probable default locale of English.
220             if parsed.language == "zh" and parsed.script:
221                 accept_languages.append(
222                     str(Locale(language=parsed.language, script=parsed.script))
223                 )
224         except (ValueError, UnknownLocaleError):
225             pass
226     return accept_languages
227 
228 
229 def set_locale(config: SDConfig) -> None:
230     """
231     Update locale info in request and session.
232     """
233     locale = get_locale(config)
234     g.localeinfo = RequestLocaleInfo(locale)
235     session["locale"] = locale
236     g.locales = LOCALES
237
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/securedrop/i18n.py b/securedrop/i18n.py
--- a/securedrop/i18n.py
+++ b/securedrop/i18n.py
@@ -40,12 +40,27 @@
     def __init__(self, locale: str):
         self.locale = Locale.parse(locale)
 
+        # This attribute can be set to `True` to differentiate multiple
+        # locales currently available (supported) for the same language.
+        self.use_display_name = False
+
     def __str__(self) -> str:
         """
         The Babel string representation of the locale.
         """
         return str(self.locale)
 
+    @property
+    def display_name(self) -> str:
+        """
+        Give callers (i.e., templates) the `Locale` object's display name when
+        such resolution is warranted, otherwise the language name---as
+        determined by `map_locale_display_names()`.
+        """
+        if self.use_display_name:
+            return self.locale.display_name
+        return self.locale.language_name
+
     @property
     def text_direction(self) -> str:
         """
@@ -135,7 +150,7 @@
             )
 
 
-LOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, str]
+LOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, RequestLocaleInfo]
 
 
 def map_locale_display_names(config: SDConfig) -> None:
@@ -149,17 +164,15 @@
     """
     language_locale_counts = collections.defaultdict(int)  # type: Dict[str, int]
     for l in sorted(config.SUPPORTED_LOCALES):
-        locale = Locale.parse(l)
-        language_locale_counts[locale.language_name] += 1
+        locale = RequestLocaleInfo(l)
+        language_locale_counts[locale.language] += 1
 
     locale_map = collections.OrderedDict()
     for l in sorted(config.SUPPORTED_LOCALES):
-        locale = Locale.parse(l)
-        if language_locale_counts[locale.language_name] == 1:
-            name = locale.language_name
-        else:
-            name = locale.display_name
-        locale_map[str(locale)] = name
+        locale = RequestLocaleInfo(l)
+        if language_locale_counts[locale.language] > 1:
+            locale.use_display_name = True
+        locale_map[str(locale)] = locale
 
     global LOCALES
     LOCALES = locale_map

{"golden_diff": "diff --git a/securedrop/i18n.py b/securedrop/i18n.py\n--- a/securedrop/i18n.py\n+++ b/securedrop/i18n.py\n@@ -40,12 +40,27 @@\n     def __init__(self, locale: str):\n         self.locale = Locale.parse(locale)\n \n+        # This attribute can be set to `True` to differentiate multiple\n+        # locales currently available (supported) for the same language.\n+        self.use_display_name = False\n+\n     def __str__(self) -> str:\n         \"\"\"\n         The Babel string representation of the locale.\n         \"\"\"\n         return str(self.locale)\n \n+    @property\n+    def display_name(self) -> str:\n+        \"\"\"\n+        Give callers (i.e., templates) the `Locale` object's display name when\n+        such resolution is warranted, otherwise the language name---as\n+        determined by `map_locale_display_names()`.\n+        \"\"\"\n+        if self.use_display_name:\n+            return self.locale.display_name\n+        return self.locale.language_name\n+\n     @property\n     def text_direction(self) -> str:\n         \"\"\"\n@@ -135,7 +150,7 @@\n             )\n \n \n-LOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, str]\n+LOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, RequestLocaleInfo]\n \n \n def map_locale_display_names(config: SDConfig) -> None:\n@@ -149,17 +164,15 @@\n     \"\"\"\n     language_locale_counts = collections.defaultdict(int)  # type: Dict[str, int]\n     for l in sorted(config.SUPPORTED_LOCALES):\n-        locale = Locale.parse(l)\n-        language_locale_counts[locale.language_name] += 1\n+        locale = RequestLocaleInfo(l)\n+        language_locale_counts[locale.language] += 1\n \n     locale_map = collections.OrderedDict()\n     for l in sorted(config.SUPPORTED_LOCALES):\n-        locale = Locale.parse(l)\n-        if language_locale_counts[locale.language_name] == 1:\n-            name = locale.language_name\n-        else:\n-            name = locale.display_name\n-        locale_map[str(locale)] = name\n+        locale = RequestLocaleInfo(l)\n+        if language_locale_counts[locale.language] > 1:\n+            locale.use_display_name = True\n+        locale_map[str(locale)] = locale\n \n     global LOCALES\n     LOCALES = locale_map\n", "issue": "make BCP47/RFC5646 `RequestLocaleInfo.language_tag` property available in locale selectors\n## Description\r\n\r\nIn `securedrop/source_templates/locales.html`, the `lang` and `hreflang` attributes require the value of the `RequestLocaleInfo.language_tag` property, which is not available in the `g.locales` dictionary.\r\n\r\n## Steps to Reproduce\r\n\r\nAfter #6041, validate the HTML of a Source Interface page including the `locales.html` locale selector (i.e., in any of them).\r\n\r\n## Expected Behavior\r\n\r\nThe page validates.\r\n\r\n## Actual Behavior\r\n\r\nSome locales' `lang` and `hreflang` attributes are invalid.\r\n\r\n## Comments\r\n\r\nThe trick will be to make the `RequestLocaleInfo.language_tag` property available to the Jinja template, which currently has access only to the contents of the `g.locales` dictionary.\r\n\r\n1. [x] `securedrop/i18n.py`: `map_locale_display_names()`: return map of `RequestLocaleInfo` objects\r\n2. [x] `securedrop/{journalist,source}_templates/locales.html`: `s/g.locales[locale]/g.locales[locale].display_name` (pending #6041)\r\n3. [x] `securedrop/{journalist,source}_templates/locales.html`: use `g.locales[locale].language_tag` in `lang` and `hreflang` attributes (pending #6041)\n", "before_files": [{"content": "#\n# SecureDrop whistleblower submission system\n# Copyright (C) 2017 Loic Dachary <loic@dachary.org>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Affero General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU Affero General Public License for more details.\n#\n# You should have received a copy of the GNU Affero General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\nimport collections\n\nfrom typing import Dict, List\n\nfrom babel.core import (\n    Locale,\n    UnknownLocaleError,\n    get_locale_identifier,\n    negotiate_locale,\n    parse_locale,\n)\nfrom flask import Flask, g, request, session\nfrom flask_babel import Babel\n\nfrom sdconfig import SDConfig\n\n\nclass RequestLocaleInfo:\n    \"\"\"\n    Convenience wrapper around a babel.core.Locale.\n    \"\"\"\n\n    def __init__(self, locale: str):\n        self.locale = Locale.parse(locale)\n\n    def __str__(self) -> str:\n        \"\"\"\n        The Babel string representation of the locale.\n        \"\"\"\n        return str(self.locale)\n\n    @property\n    def text_direction(self) -> str:\n        \"\"\"\n        The Babel text direction: ltr or rtl.\n\n        Used primarily to set text direction in HTML via the \"dir\"\n        attribute.\n        \"\"\"\n        return self.locale.text_direction\n\n    @property\n    def language(self) -> str:\n        \"\"\"\n        The Babel language name.\n\n        Just the language, without subtag info like region or script.\n        \"\"\"\n        return self.locale.language\n\n    @property\n    def id(self) -> str:\n        \"\"\"\n        The Babel string representation of the locale.\n\n        This should match the name of the directory containing its\n        translations.\n        \"\"\"\n        return str(self.locale)\n\n    @property\n    def language_tag(self) -> str:\n        \"\"\"\n        Returns a BCP47/RFC5646 language tag for the locale.\n\n        Language tags are used in HTTP headers and the HTML lang\n        attribute.\n        \"\"\"\n        return get_locale_identifier(parse_locale(str(self.locale)), sep=\"-\")\n\n\ndef configure_babel(config: SDConfig, app: Flask) -> None:\n    \"\"\"\n    Set up Flask-Babel according to the SecureDrop configuration.\n    \"\"\"\n    # Tell Babel where to find our translations.\n    translations_directory = str(config.TRANSLATION_DIRS.absolute())\n    app.config[\"BABEL_TRANSLATION_DIRECTORIES\"] = translations_directory\n\n    # Create the app's Babel instance. Passing the app to the\n    # constructor causes the instance to attach itself to the app.\n    babel = Babel(app)\n\n    # verify that Babel is only using the translations we told it about\n    if list(babel.translation_directories) != [translations_directory]:\n        raise ValueError(\n            \"Babel translation directories ({}) do not match SecureDrop configuration ({})\".format(\n                babel.translation_directories, [translations_directory]\n            )\n        )\n\n    # register the function used to determine the locale of a request\n    babel.localeselector(lambda: get_locale(config))\n\n\ndef validate_locale_configuration(config: SDConfig, app: Flask) -> None:\n    \"\"\"\n    Ensure that the configured locales are valid and translated.\n    \"\"\"\n    if config.DEFAULT_LOCALE not in config.SUPPORTED_LOCALES:\n        raise ValueError(\n            'The default locale \"{}\" is not included in the set of supported locales \"{}\"'.format(\n                config.DEFAULT_LOCALE, config.SUPPORTED_LOCALES\n            )\n        )\n\n    translations = app.babel_instance.list_translations()\n    for locale in config.SUPPORTED_LOCALES:\n        if locale == \"en_US\":\n            continue\n\n        parsed = Locale.parse(locale)\n        if parsed not in translations:\n            raise ValueError(\n                'Configured locale \"{}\" is not in the set of translated locales \"{}\"'.format(\n                    parsed, translations\n                )\n            )\n\n\nLOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, str]\n\n\ndef map_locale_display_names(config: SDConfig) -> None:\n    \"\"\"\n    Create a map of locale identifiers to names for display.\n\n    For most of our supported languages, we only provide one\n    translation, so including the full display name is not necessary\n    to distinguish them. For languages with more than one translation,\n    like Chinese, we do need the additional detail.\n    \"\"\"\n    language_locale_counts = collections.defaultdict(int)  # type: Dict[str, int]\n    for l in sorted(config.SUPPORTED_LOCALES):\n        locale = Locale.parse(l)\n        language_locale_counts[locale.language_name] += 1\n\n    locale_map = collections.OrderedDict()\n    for l in sorted(config.SUPPORTED_LOCALES):\n        locale = Locale.parse(l)\n        if language_locale_counts[locale.language_name] == 1:\n            name = locale.language_name\n        else:\n            name = locale.display_name\n        locale_map[str(locale)] = name\n\n    global LOCALES\n    LOCALES = locale_map\n\n\ndef configure(config: SDConfig, app: Flask) -> None:\n    configure_babel(config, app)\n    validate_locale_configuration(config, app)\n    map_locale_display_names(config)\n\n\ndef get_locale(config: SDConfig) -> str:\n    \"\"\"\n    Return the best supported locale for a request.\n\n    Get the locale as follows, by order of precedence:\n    - l request argument or session['locale']\n    - browser suggested locale, from the Accept-Languages header\n    - config.DEFAULT_LOCALE\n    \"\"\"\n    # Default to any locale set in the session.\n    locale = session.get(\"locale\")\n\n    # A valid locale specified in request.args takes precedence.\n    if request.args.get(\"l\"):\n        negotiated = negotiate_locale([request.args[\"l\"]], LOCALES.keys())\n        if negotiated:\n            locale = negotiated\n\n    # If the locale is not in the session or request.args, negotiate\n    # the best supported option from the browser's accepted languages.\n    if not locale:\n        locale = negotiate_locale(get_accepted_languages(), LOCALES.keys())\n\n    # Finally, fall back to the default locale if necessary.\n    return locale or config.DEFAULT_LOCALE\n\n\ndef get_accepted_languages() -> List[str]:\n    \"\"\"\n    Convert a request's list of accepted languages into locale identifiers.\n    \"\"\"\n    accept_languages = []\n    for l in request.accept_languages.values():\n        try:\n            parsed = Locale.parse(l, \"-\")\n            accept_languages.append(str(parsed))\n\n            # We only have two Chinese translations, simplified\n            # and traditional, based on script and not\n            # region. Browsers tend to send identifiers with\n            # region, e.g. zh-CN or zh-TW. Babel can generally\n            # infer the script from those, so we can fabricate a\n            # fallback entry without region, in the hope that it\n            # will match one of our translations and the site will\n            # at least be more legible at first contact than the\n            # probable default locale of English.\n            if parsed.language == \"zh\" and parsed.script:\n                accept_languages.append(\n                    str(Locale(language=parsed.language, script=parsed.script))\n                )\n        except (ValueError, UnknownLocaleError):\n            pass\n    return accept_languages\n\n\ndef set_locale(config: SDConfig) -> None:\n    \"\"\"\n    Update locale info in request and session.\n    \"\"\"\n    locale = get_locale(config)\n    g.localeinfo = RequestLocaleInfo(locale)\n    session[\"locale\"] = locale\n    g.locales = LOCALES\n", "path": "securedrop/i18n.py"}], "after_files": [{"content": "#\n# SecureDrop whistleblower submission system\n# Copyright (C) 2017 Loic Dachary <loic@dachary.org>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Affero General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU Affero General Public License for more details.\n#\n# You should have received a copy of the GNU Affero General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\nimport collections\n\nfrom typing import Dict, List\n\nfrom babel.core import (\n    Locale,\n    UnknownLocaleError,\n    get_locale_identifier,\n    negotiate_locale,\n    parse_locale,\n)\nfrom flask import Flask, g, request, session\nfrom flask_babel import Babel\n\nfrom sdconfig import SDConfig\n\n\nclass RequestLocaleInfo:\n    \"\"\"\n    Convenience wrapper around a babel.core.Locale.\n    \"\"\"\n\n    def __init__(self, locale: str):\n        self.locale = Locale.parse(locale)\n\n        # This attribute can be set to `True` to differentiate multiple\n        # locales currently available (supported) for the same language.\n        self.use_display_name = False\n\n    def __str__(self) -> str:\n        \"\"\"\n        The Babel string representation of the locale.\n        \"\"\"\n        return str(self.locale)\n\n    @property\n    def display_name(self) -> str:\n        \"\"\"\n        Give callers (i.e., templates) the `Locale` object's display name when\n        such resolution is warranted, otherwise the language name---as\n        determined by `map_locale_display_names()`.\n        \"\"\"\n        if self.use_display_name:\n            return self.locale.display_name\n        return self.locale.language_name\n\n    @property\n    def text_direction(self) -> str:\n        \"\"\"\n        The Babel text direction: ltr or rtl.\n\n        Used primarily to set text direction in HTML via the \"dir\"\n        attribute.\n        \"\"\"\n        return self.locale.text_direction\n\n    @property\n    def language(self) -> str:\n        \"\"\"\n        The Babel language name.\n\n        Just the language, without subtag info like region or script.\n        \"\"\"\n        return self.locale.language\n\n    @property\n    def id(self) -> str:\n        \"\"\"\n        The Babel string representation of the locale.\n\n        This should match the name of the directory containing its\n        translations.\n        \"\"\"\n        return str(self.locale)\n\n    @property\n    def language_tag(self) -> str:\n        \"\"\"\n        Returns a BCP47/RFC5646 language tag for the locale.\n\n        Language tags are used in HTTP headers and the HTML lang\n        attribute.\n        \"\"\"\n        return get_locale_identifier(parse_locale(str(self.locale)), sep=\"-\")\n\n\ndef configure_babel(config: SDConfig, app: Flask) -> None:\n    \"\"\"\n    Set up Flask-Babel according to the SecureDrop configuration.\n    \"\"\"\n    # Tell Babel where to find our translations.\n    translations_directory = str(config.TRANSLATION_DIRS.absolute())\n    app.config[\"BABEL_TRANSLATION_DIRECTORIES\"] = translations_directory\n\n    # Create the app's Babel instance. Passing the app to the\n    # constructor causes the instance to attach itself to the app.\n    babel = Babel(app)\n\n    # verify that Babel is only using the translations we told it about\n    if list(babel.translation_directories) != [translations_directory]:\n        raise ValueError(\n            \"Babel translation directories ({}) do not match SecureDrop configuration ({})\".format(\n                babel.translation_directories, [translations_directory]\n            )\n        )\n\n    # register the function used to determine the locale of a request\n    babel.localeselector(lambda: get_locale(config))\n\n\ndef validate_locale_configuration(config: SDConfig, app: Flask) -> None:\n    \"\"\"\n    Ensure that the configured locales are valid and translated.\n    \"\"\"\n    if config.DEFAULT_LOCALE not in config.SUPPORTED_LOCALES:\n        raise ValueError(\n            'The default locale \"{}\" is not included in the set of supported locales \"{}\"'.format(\n                config.DEFAULT_LOCALE, config.SUPPORTED_LOCALES\n            )\n        )\n\n    translations = app.babel_instance.list_translations()\n    for locale in config.SUPPORTED_LOCALES:\n        if locale == \"en_US\":\n            continue\n\n        parsed = Locale.parse(locale)\n        if parsed not in translations:\n            raise ValueError(\n                'Configured locale \"{}\" is not in the set of translated locales \"{}\"'.format(\n                    parsed, translations\n                )\n            )\n\n\nLOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, RequestLocaleInfo]\n\n\ndef map_locale_display_names(config: SDConfig) -> None:\n    \"\"\"\n    Create a map of locale identifiers to names for display.\n\n    For most of our supported languages, we only provide one\n    translation, so including the full display name is not necessary\n    to distinguish them. For languages with more than one translation,\n    like Chinese, we do need the additional detail.\n    \"\"\"\n    language_locale_counts = collections.defaultdict(int)  # type: Dict[str, int]\n    for l in sorted(config.SUPPORTED_LOCALES):\n        locale = RequestLocaleInfo(l)\n        language_locale_counts[locale.language] += 1\n\n    locale_map = collections.OrderedDict()\n    for l in sorted(config.SUPPORTED_LOCALES):\n        locale = RequestLocaleInfo(l)\n        if language_locale_counts[locale.language] > 1:\n            locale.use_display_name = True\n        locale_map[str(locale)] = locale\n\n    global LOCALES\n    LOCALES = locale_map\n\n\ndef configure(config: SDConfig, app: Flask) -> None:\n    configure_babel(config, app)\n    validate_locale_configuration(config, app)\n    map_locale_display_names(config)\n\n\ndef get_locale(config: SDConfig) -> str:\n    \"\"\"\n    Return the best supported locale for a request.\n\n    Get the locale as follows, by order of precedence:\n    - l request argument or session['locale']\n    - browser suggested locale, from the Accept-Languages header\n    - config.DEFAULT_LOCALE\n    \"\"\"\n    # Default to any locale set in the session.\n    locale = session.get(\"locale\")\n\n    # A valid locale specified in request.args takes precedence.\n    if request.args.get(\"l\"):\n        negotiated = negotiate_locale([request.args[\"l\"]], LOCALES.keys())\n        if negotiated:\n            locale = negotiated\n\n    # If the locale is not in the session or request.args, negotiate\n    # the best supported option from the browser's accepted languages.\n    if not locale:\n        locale = negotiate_locale(get_accepted_languages(), LOCALES.keys())\n\n    # Finally, fall back to the default locale if necessary.\n    return locale or config.DEFAULT_LOCALE\n\n\ndef get_accepted_languages() -> List[str]:\n    \"\"\"\n    Convert a request's list of accepted languages into locale identifiers.\n    \"\"\"\n    accept_languages = []\n    for l in request.accept_languages.values():\n        try:\n            parsed = Locale.parse(l, \"-\")\n            accept_languages.append(str(parsed))\n\n            # We only have two Chinese translations, simplified\n            # and traditional, based on script and not\n            # region. Browsers tend to send identifiers with\n            # region, e.g. zh-CN or zh-TW. Babel can generally\n            # infer the script from those, so we can fabricate a\n            # fallback entry without region, in the hope that it\n            # will match one of our translations and the site will\n            # at least be more legible at first contact than the\n            # probable default locale of English.\n            if parsed.language == \"zh\" and parsed.script:\n                accept_languages.append(\n                    str(Locale(language=parsed.language, script=parsed.script))\n                )\n        except (ValueError, UnknownLocaleError):\n            pass\n    return accept_languages\n\n\ndef set_locale(config: SDConfig) -> None:\n    \"\"\"\n    Update locale info in request and session.\n    \"\"\"\n    locale = get_locale(config)\n    g.localeinfo = RequestLocaleInfo(locale)\n    session[\"locale\"] = locale\n    g.locales = LOCALES\n", "path": "securedrop/i18n.py"}]}