We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
`map_locale_display_names()` does not set `use_display_name` for the first of multiple locales for a language
## Description

For languages like `pt` and `zh` for which we now have multiple translated locales, `map_locale_display_names()` sets `use_display_name` for only the second and following locales.  Prior to an overzealous refactoring in #6406, it would be set for all locales for a multi-locale language.

## Steps to Reproduce

Reported by @deeplow in <https://forum.securedrop.org/t/can-pt-br-become-portugues-brasil-instead-of-just-portugues/1455>.  *Test cases TK.*

## Expected Behavior

* `pt_BR` = `Português (Brasil)`
* `pt_PT` = `Português (Portugal)`

## Actual Behavior

* `pt_BR` = `Português`
* `pt_PT` = `Português (Portugal)`


--- 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 from typing import List, Set
20 
21 from babel.core import (
22     Locale,
23     UnknownLocaleError,
24     get_locale_identifier,
25     negotiate_locale,
26     parse_locale,
27 )
28 from flask import Flask, g, request, session
29 from flask_babel import Babel
30 from sdconfig import FALLBACK_LOCALE, SDConfig
31 
32 
33 class RequestLocaleInfo:
34     """
35     Convenience wrapper around a babel.core.Locale.
36     """
37 
38     def __init__(self, locale: str):
39         self.locale = Locale.parse(locale)
40 
41         # This attribute can be set to `True` to differentiate multiple
42         # locales currently available (supported) for the same language.
43         self.use_display_name = False
44 
45     def __str__(self) -> str:
46         """
47         The Babel string representation of the locale.
48         """
49         return str(self.locale)
50 
51     @property
52     def display_name(self) -> str:
53         """
54         Give callers (i.e., templates) the `Locale` object's display name when
55         such resolution is warranted, otherwise the language name---as
56         determined by `map_locale_display_names()`.
57         """
58         if self.use_display_name:
59             return self.locale.display_name
60         return self.locale.language_name
61 
62     @property
63     def text_direction(self) -> str:
64         """
65         The Babel text direction: ltr or rtl.
66 
67         Used primarily to set text direction in HTML via the "dir"
68         attribute.
69         """
70         return self.locale.text_direction
71 
72     @property
73     def language(self) -> str:
74         """
75         The Babel language name.
76 
77         Just the language, without subtag info like region or script.
78         """
79         return self.locale.language
80 
81     @property
82     def id(self) -> str:
83         """
84         The Babel string representation of the locale.
85 
86         This should match the name of the directory containing its
87         translations.
88         """
89         return str(self.locale)
90 
91     @property
92     def language_tag(self) -> str:
93         """
94         Returns a BCP47/RFC5646 language tag for the locale.
95 
96         Language tags are used in HTTP headers and the HTML lang
97         attribute.
98         """
99         return get_locale_identifier(parse_locale(str(self.locale)), sep="-")
100 
101 
102 def configure_babel(config: SDConfig, app: Flask) -> Babel:
103     """
104     Set up Flask-Babel according to the SecureDrop configuration.
105     """
106     # Tell Babel where to find our translations.
107     translations_directory = str(config.TRANSLATION_DIRS.absolute())
108     app.config["BABEL_TRANSLATION_DIRECTORIES"] = translations_directory
109 
110     # Create the app's Babel instance. Passing the app to the
111     # constructor causes the instance to attach itself to the app.
112     babel = Babel(app)
113 
114     # verify that Babel is only using the translations we told it about
115     if list(babel.translation_directories) != [translations_directory]:
116         raise ValueError(
117             "Babel translation directories ({}) do not match SecureDrop configuration ({})".format(
118                 babel.translation_directories, [translations_directory]
119             )
120         )
121 
122     # register the function used to determine the locale of a request
123     babel.localeselector(lambda: get_locale(config))
124     return babel
125 
126 
127 def parse_locale_set(codes: List[str]) -> Set[Locale]:
128     return {Locale.parse(code) for code in codes}
129 
130 
131 def validate_locale_configuration(config: SDConfig, babel: Babel) -> None:
132     """
133     Check that configured locales are available in the filesystem and therefore usable by
134     Babel.  Warn about configured locales that are not usable, unless we're left with
135     no usable default or fallback locale, in which case raise an exception.
136     """
137     # These locales are available and loadable from the filesystem.
138     available = set(babel.list_translations())
139     available.add(Locale.parse(FALLBACK_LOCALE))
140 
141     # These locales were configured via "securedrop-admin sdconfig", meaning
142     # they were present on the Admin Workstation at "securedrop-admin" runtime.
143     configured = parse_locale_set(config.SUPPORTED_LOCALES)
144 
145     # The intersection of these sets is the set of locales usable by Babel.
146     usable = available & configured
147 
148     missing = configured - usable
149     if missing:
150         babel.app.logger.error(
151             f"Configured locales {missing} are not in the set of usable locales {usable}"
152         )
153 
154     defaults = parse_locale_set([config.DEFAULT_LOCALE, FALLBACK_LOCALE])
155     if not defaults & usable:
156         raise ValueError(
157             f"None of the default locales {defaults} are in the set of usable locales {usable}"
158         )
159 
160     global USABLE_LOCALES
161     USABLE_LOCALES = usable
162 
163 
164 # TODO(#6420): avoid relying on and manipulating on this global state
165 LOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, RequestLocaleInfo]
166 USABLE_LOCALES = set()  # type: Set[Locale]
167 
168 
169 def map_locale_display_names(config: SDConfig) -> None:
170     """
171     Create a map of locale identifiers to names for display.
172 
173     For most of our supported languages, we only provide one
174     translation, so including the full display name is not necessary
175     to distinguish them. For languages with more than one translation,
176     like Chinese, we do need the additional detail.
177     """
178     seen: Set[str] = set()
179     locale_map = collections.OrderedDict()
180     for l in sorted(config.SUPPORTED_LOCALES):
181         if Locale.parse(l) not in USABLE_LOCALES:
182             continue
183 
184         locale = RequestLocaleInfo(l)
185         if locale.language in seen:
186             # Disambiguate translations for this language.
187             locale.use_display_name = True
188         else:
189             seen.add(locale.language)
190 
191         locale_map[str(locale)] = locale
192 
193     global LOCALES
194     LOCALES = locale_map
195 
196 
197 def configure(config: SDConfig, app: Flask) -> None:
198     babel = configure_babel(config, app)
199     validate_locale_configuration(config, babel)
200     map_locale_display_names(config)
201 
202 
203 def get_locale(config: SDConfig) -> str:
204     """
205     Return the best supported locale for a request.
206 
207     Get the locale as follows, by order of precedence:
208     - l request argument or session['locale']
209     - browser suggested locale, from the Accept-Languages header
210     - config.DEFAULT_LOCALE
211     - config.FALLBACK_LOCALE
212     """
213     preferences = []
214     if session.get("locale"):
215         preferences.append(session.get("locale"))
216     if request.args.get("l"):
217         preferences.insert(0, request.args.get("l"))
218     if not preferences:
219         preferences.extend(get_accepted_languages())
220     preferences.append(config.DEFAULT_LOCALE)
221     preferences.append(FALLBACK_LOCALE)
222 
223     negotiated = negotiate_locale(preferences, LOCALES.keys())
224 
225     if not negotiated:
226         raise ValueError("No usable locale")
227 
228     return negotiated
229 
230 
231 def get_accepted_languages() -> List[str]:
232     """
233     Convert a request's list of accepted languages into locale identifiers.
234     """
235     accept_languages = []
236     for l in request.accept_languages.values():
237         try:
238             parsed = Locale.parse(l, "-")
239             accept_languages.append(str(parsed))
240 
241             # We only have two Chinese translations, simplified
242             # and traditional, based on script and not
243             # region. Browsers tend to send identifiers with
244             # region, e.g. zh-CN or zh-TW. Babel can generally
245             # infer the script from those, so we can fabricate a
246             # fallback entry without region, in the hope that it
247             # will match one of our translations and the site will
248             # at least be more legible at first contact than the
249             # probable default locale of English.
250             if parsed.language == "zh" and parsed.script:
251                 accept_languages.append(str(Locale(language=parsed.language, script=parsed.script)))
252         except (ValueError, UnknownLocaleError):
253             pass
254     return accept_languages
255 
256 
257 def set_locale(config: SDConfig) -> None:
258     """
259     Update locale info in request and session.
260     """
261     locale = get_locale(config)
262     g.localeinfo = RequestLocaleInfo(locale)  # pylint: disable=assigning-non-slot
263     session["locale"] = locale
264     g.locales = LOCALES  # pylint: disable=assigning-non-slot
265
```
--- 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
@@ -16,7 +16,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 import collections
-from typing import List, Set
+from typing import Dict, List, Set
 
 from babel.core import (
     Locale,
@@ -175,18 +175,21 @@
     to distinguish them. For languages with more than one translation,
     like Chinese, we do need the additional detail.
     """
-    seen: Set[str] = set()
+
+    language_locale_counts = collections.defaultdict(int)  # type: Dict[str, int]
+    for l in sorted(config.SUPPORTED_LOCALES):
+        locale = RequestLocaleInfo(l)
+        language_locale_counts[locale.language] += 1
+
     locale_map = collections.OrderedDict()
     for l in sorted(config.SUPPORTED_LOCALES):
         if Locale.parse(l) not in USABLE_LOCALES:
             continue
 
         locale = RequestLocaleInfo(l)
-        if locale.language in seen:
+        if language_locale_counts[locale.language] > 1:
             # Disambiguate translations for this language.
             locale.use_display_name = True
-        else:
-            seen.add(locale.language)
 
         locale_map[str(locale)] = locale

{"golden_diff": "diff --git a/securedrop/i18n.py b/securedrop/i18n.py\n--- a/securedrop/i18n.py\n+++ b/securedrop/i18n.py\n@@ -16,7 +16,7 @@\n # along with this program.  If not, see <http://www.gnu.org/licenses/>.\n #\n import collections\n-from typing import List, Set\n+from typing import Dict, List, Set\n \n from babel.core import (\n     Locale,\n@@ -175,18 +175,21 @@\n     to distinguish them. For languages with more than one translation,\n     like Chinese, we do need the additional detail.\n     \"\"\"\n-    seen: Set[str] = set()\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         if Locale.parse(l) not in USABLE_LOCALES:\n             continue\n \n         locale = RequestLocaleInfo(l)\n-        if locale.language in seen:\n+        if language_locale_counts[locale.language] > 1:\n             # Disambiguate translations for this language.\n             locale.use_display_name = True\n-        else:\n-            seen.add(locale.language)\n \n         locale_map[str(locale)] = locale\n", "issue": "`map_locale_display_names()` does not set `use_display_name` for the first of multiple locales for a language\n## Description\r\n\r\nFor languages like `pt` and `zh` for which we now have multiple translated locales, `map_locale_display_names()` sets `use_display_name` for only the second and following locales.  Prior to an overzealous refactoring in #6406, it would be set for all locales for a multi-locale language.\r\n\r\n## Steps to Reproduce\r\n\r\nReported by @deeplow in <https://forum.securedrop.org/t/can-pt-br-become-portugues-brasil-instead-of-just-portugues/1455>.  *Test cases TK.*\r\n\r\n## Expected Behavior\r\n\r\n* `pt_BR` = `Portugu\u00eas (Brasil)`\r\n* `pt_PT` = `Portugu\u00eas (Portugal)`\r\n\r\n## Actual Behavior\r\n\r\n* `pt_BR` = `Portugu\u00eas`\r\n* `pt_PT` = `Portugu\u00eas (Portugal)`\r\n\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\nfrom typing import List, Set\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\nfrom sdconfig import FALLBACK_LOCALE, 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) -> Babel:\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    return babel\n\n\ndef parse_locale_set(codes: List[str]) -> Set[Locale]:\n    return {Locale.parse(code) for code in codes}\n\n\ndef validate_locale_configuration(config: SDConfig, babel: Babel) -> None:\n    \"\"\"\n    Check that configured locales are available in the filesystem and therefore usable by\n    Babel.  Warn about configured locales that are not usable, unless we're left with\n    no usable default or fallback locale, in which case raise an exception.\n    \"\"\"\n    # These locales are available and loadable from the filesystem.\n    available = set(babel.list_translations())\n    available.add(Locale.parse(FALLBACK_LOCALE))\n\n    # These locales were configured via \"securedrop-admin sdconfig\", meaning\n    # they were present on the Admin Workstation at \"securedrop-admin\" runtime.\n    configured = parse_locale_set(config.SUPPORTED_LOCALES)\n\n    # The intersection of these sets is the set of locales usable by Babel.\n    usable = available & configured\n\n    missing = configured - usable\n    if missing:\n        babel.app.logger.error(\n            f\"Configured locales {missing} are not in the set of usable locales {usable}\"\n        )\n\n    defaults = parse_locale_set([config.DEFAULT_LOCALE, FALLBACK_LOCALE])\n    if not defaults & usable:\n        raise ValueError(\n            f\"None of the default locales {defaults} are in the set of usable locales {usable}\"\n        )\n\n    global USABLE_LOCALES\n    USABLE_LOCALES = usable\n\n\n# TODO(#6420): avoid relying on and manipulating on this global state\nLOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, RequestLocaleInfo]\nUSABLE_LOCALES = set()  # type: Set[Locale]\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    seen: Set[str] = set()\n    locale_map = collections.OrderedDict()\n    for l in sorted(config.SUPPORTED_LOCALES):\n        if Locale.parse(l) not in USABLE_LOCALES:\n            continue\n\n        locale = RequestLocaleInfo(l)\n        if locale.language in seen:\n            # Disambiguate translations for this language.\n            locale.use_display_name = True\n        else:\n            seen.add(locale.language)\n\n        locale_map[str(locale)] = locale\n\n    global LOCALES\n    LOCALES = locale_map\n\n\ndef configure(config: SDConfig, app: Flask) -> None:\n    babel = configure_babel(config, app)\n    validate_locale_configuration(config, babel)\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    - config.FALLBACK_LOCALE\n    \"\"\"\n    preferences = []\n    if session.get(\"locale\"):\n        preferences.append(session.get(\"locale\"))\n    if request.args.get(\"l\"):\n        preferences.insert(0, request.args.get(\"l\"))\n    if not preferences:\n        preferences.extend(get_accepted_languages())\n    preferences.append(config.DEFAULT_LOCALE)\n    preferences.append(FALLBACK_LOCALE)\n\n    negotiated = negotiate_locale(preferences, LOCALES.keys())\n\n    if not negotiated:\n        raise ValueError(\"No usable locale\")\n\n    return negotiated\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(str(Locale(language=parsed.language, script=parsed.script)))\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)  # pylint: disable=assigning-non-slot\n    session[\"locale\"] = locale\n    g.locales = LOCALES  # pylint: disable=assigning-non-slot\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\nfrom typing import Dict, List, Set\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\nfrom sdconfig import FALLBACK_LOCALE, 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) -> Babel:\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    return babel\n\n\ndef parse_locale_set(codes: List[str]) -> Set[Locale]:\n    return {Locale.parse(code) for code in codes}\n\n\ndef validate_locale_configuration(config: SDConfig, babel: Babel) -> None:\n    \"\"\"\n    Check that configured locales are available in the filesystem and therefore usable by\n    Babel.  Warn about configured locales that are not usable, unless we're left with\n    no usable default or fallback locale, in which case raise an exception.\n    \"\"\"\n    # These locales are available and loadable from the filesystem.\n    available = set(babel.list_translations())\n    available.add(Locale.parse(FALLBACK_LOCALE))\n\n    # These locales were configured via \"securedrop-admin sdconfig\", meaning\n    # they were present on the Admin Workstation at \"securedrop-admin\" runtime.\n    configured = parse_locale_set(config.SUPPORTED_LOCALES)\n\n    # The intersection of these sets is the set of locales usable by Babel.\n    usable = available & configured\n\n    missing = configured - usable\n    if missing:\n        babel.app.logger.error(\n            f\"Configured locales {missing} are not in the set of usable locales {usable}\"\n        )\n\n    defaults = parse_locale_set([config.DEFAULT_LOCALE, FALLBACK_LOCALE])\n    if not defaults & usable:\n        raise ValueError(\n            f\"None of the default locales {defaults} are in the set of usable locales {usable}\"\n        )\n\n    global USABLE_LOCALES\n    USABLE_LOCALES = usable\n\n\n# TODO(#6420): avoid relying on and manipulating on this global state\nLOCALES = collections.OrderedDict()  # type: collections.OrderedDict[str, RequestLocaleInfo]\nUSABLE_LOCALES = set()  # type: Set[Locale]\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\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        if Locale.parse(l) not in USABLE_LOCALES:\n            continue\n\n        locale = RequestLocaleInfo(l)\n        if language_locale_counts[locale.language] > 1:\n            # Disambiguate translations for this language.\n            locale.use_display_name = True\n\n        locale_map[str(locale)] = locale\n\n    global LOCALES\n    LOCALES = locale_map\n\n\ndef configure(config: SDConfig, app: Flask) -> None:\n    babel = configure_babel(config, app)\n    validate_locale_configuration(config, babel)\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    - config.FALLBACK_LOCALE\n    \"\"\"\n    preferences = []\n    if session.get(\"locale\"):\n        preferences.append(session.get(\"locale\"))\n    if request.args.get(\"l\"):\n        preferences.insert(0, request.args.get(\"l\"))\n    if not preferences:\n        preferences.extend(get_accepted_languages())\n    preferences.append(config.DEFAULT_LOCALE)\n    preferences.append(FALLBACK_LOCALE)\n\n    negotiated = negotiate_locale(preferences, LOCALES.keys())\n\n    if not negotiated:\n        raise ValueError(\"No usable locale\")\n\n    return negotiated\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(str(Locale(language=parsed.language, script=parsed.script)))\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)  # pylint: disable=assigning-non-slot\n    session[\"locale\"] = locale\n    g.locales = LOCALES  # pylint: disable=assigning-non-slot\n", "path": "securedrop/i18n.py"}]}