Skip to content

Commit

Permalink
refactor(get_locale): resolve locale from single list of preferences
Browse files Browse the repository at this point in the history
As of 9c11950, get_locale() handled user/agent-provided locale
preferences and deferred to resolve_fallback_locale() if none
could be satisfied.  Here we build a single list of locales,
beginning with user/agent preferences and ending with the server-side
{DEFAULT_FALLBACK}_LOCALEs, and call babel.core.negotiate_locale()
once to negotiate one (if possible).
  • Loading branch information
cfm committed May 3, 2022
1 parent eb2a042 commit 20c7212
Show file tree
Hide file tree
Showing 2 changed files with 15 additions and 66 deletions.
51 changes: 14 additions & 37 deletions securedrop/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,28 +202,6 @@ def configure(config: SDConfig, app: Flask) -> None:
map_locale_display_names(config)


def resolve_fallback_locale(config: SDConfig) -> str:
"""
Return a *usable* fallback locale. Namely:
1. Don't fall back to the configured `DEFAULT_LOCALE` if it isn't available.
2. Don't fall back to the hard-coded `FALLBACK_LOCALE` (`en_US`) if it isn't configured.
NB. If neither the default nor the fallback locale is usable, then we should have crashed
already in `validate_locale_configuration()`.
"""

if Locale.parse(config.DEFAULT_LOCALE) in USABLE_LOCALES:
return config.DEFAULT_LOCALE

elif Locale.parse(FALLBACK_LOCALE) in USABLE_LOCALES:
return FALLBACK_LOCALE

else:
raise ValueError('No usable fallback locale')


def get_locale(config: SDConfig) -> str:
"""
Return the best supported locale for a request.
Expand All @@ -232,25 +210,24 @@ def get_locale(config: SDConfig) -> str:
- l request argument or session['locale']
- browser suggested locale, from the Accept-Languages header
- config.DEFAULT_LOCALE
- config.FALLBACK_LOCALE
"""
# Default to any usable locale set in the session.
locale = session.get("locale")
if locale:
locale = negotiate_locale([locale], LOCALES.keys())

# A usable locale specified in request.args takes precedence.
preferences = []
if session.get("locale"):
preferences.append(session.get("locale"))
if request.args.get("l"):
negotiated = negotiate_locale([request.args["l"]], LOCALES.keys())
if negotiated:
locale = negotiated
preferences.insert(0, request.args.get("l"))
if not preferences:
preferences.extend(get_accepted_languages())
preferences.append(config.DEFAULT_LOCALE)
preferences.append(FALLBACK_LOCALE)

negotiated = negotiate_locale(preferences, LOCALES.keys())

# If the locale is not in the session or request.args, negotiate
# the best supported option from the browser's accepted languages.
if not locale:
locale = negotiate_locale(get_accepted_languages(), LOCALES.keys())
if not negotiated:
raise ValueError("No usable locale")

# Finally, if we can't negotiate a requested locale, resolve a fallback.
return locale or resolve_fallback_locale(config)
return negotiated


def get_accepted_languages() -> List[str]:
Expand Down
30 changes: 1 addition & 29 deletions securedrop/tests/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from flask import request
from flask import session
from flask_babel import gettext
from i18n import parse_locale_set, resolve_fallback_locale
from i18n import parse_locale_set
from sdconfig import FALLBACK_LOCALE, SDConfig
from sh import pybabel
from sh import sed
Expand Down Expand Up @@ -233,32 +233,6 @@ def test_parse_locale_set():
assert parse_locale_set([FALLBACK_LOCALE]) == set([Locale.parse(FALLBACK_LOCALE)])


def test_resolve_fallback_locale(config):
"""
Only a usable default or fallback locale is returned.
"""
i18n.USABLE_LOCALES = parse_locale_set([FALLBACK_LOCALE, 'es_ES'])
fake_config = SDConfig()

# The default locale is neither configured nor available.
fake_config.DEFAULT_LOCALE = NEVER_LOCALE
assert resolve_fallback_locale(fake_config) == FALLBACK_LOCALE

# The default locale is configured but not available.
fake_config.SUPPORTED_LOCALES = [FALLBACK_LOCALE, NEVER_LOCALE]
assert resolve_fallback_locale(fake_config) == FALLBACK_LOCALE

# The default locale is available but not configured.
fake_config.SUPPORTED_LOCALES = [FALLBACK_LOCALE]
fake_config.DEFAULT_LOCALE = NEVER_LOCALE
assert resolve_fallback_locale(fake_config) == FALLBACK_LOCALE

# Happy path: a non-fallback default locale is both available and configured.
fake_config.SUPPORTED_LOCALES = [FALLBACK_LOCALE, 'es_ES']
fake_config.DEFAULT_LOCALE = 'es_ES'
assert resolve_fallback_locale(fake_config) == 'es_ES'


def test_no_usable_fallback_locale(journalist_app, config):
"""
The apps fail if neither the default nor the fallback locale is usable.
Expand All @@ -269,8 +243,6 @@ def test_no_usable_fallback_locale(journalist_app, config):
fake_config.TRANSLATION_DIRS = Path(config.TEMP_DIR)

i18n.USABLE_LOCALES = set()
with pytest.raises(ValueError, match='No usable fallback locale'):
resolve_fallback_locale(fake_config)

with pytest.raises(ValueError, match='in the set of usable locales'):
journalist_app_module.create_app(fake_config)
Expand Down

0 comments on commit 20c7212

Please sign in to comment.