diff --git a/securedrop/i18n.py b/securedrop/i18n.py index 4533fac04b..2290556e34 100644 --- a/securedrop/i18n.py +++ b/securedrop/i18n.py @@ -29,8 +29,7 @@ from flask import Flask, g, request, session from flask_babel import Babel -from sdconfig import SDConfig - +from sdconfig import SDConfig, FALLBACK_LOCALE class RequestLocaleInfo: """ @@ -128,28 +127,30 @@ def configure_babel(config: SDConfig, app: Flask) -> Babel: def validate_locale_configuration(config: SDConfig, babel: Babel) -> None: """ - Ensure that the configured locales are valid and translated. + Check that configured locales are available in the filesystem and therefore usable by + Babel. Warn about configured locales that are not usable, unless we're left with + no usable default or fallback locale, in which case raise an exception. """ - if config.DEFAULT_LOCALE not in config.SUPPORTED_LOCALES: - raise ValueError( - 'The default locale "{}" is not included in the set of supported locales "{}"'.format( - config.DEFAULT_LOCALE, config.SUPPORTED_LOCALES - ) - ) - - translations = babel.list_translations() - for locale in config.SUPPORTED_LOCALES: - if locale == "en_US": - continue - + available = set(babel.list_translations()) # available in the filesystem + available.add(FALLBACK_LOCALE) + configured = set(config.SUPPORTED_LOCALES) # configured by the administrator + usable = available & configured # usable by Babel + + # This loop could be replaced with a set operation, but we'd have to iterate over the result + # anyway in order to log it. + for locale in configured: parsed = Locale.parse(locale) - if parsed not in translations: - raise ValueError( - 'Configured locale "{}" is not in the set of translated locales "{}"'.format( - parsed, translations - ) + if parsed not in usable: + babel.app.logger.error( + f'Configured locale "{parsed}" is not in the set of usable locales {usable}' ) + defaults = set([config.DEFAULT_LOCALE, FALLBACK_LOCALE]) & usable + if len(defaults) == 0: + raise ValueError( + f'None of the default locales {defaults} is in the set of usable locales {usable}' + ) + LOCALES = collections.OrderedDict() # type: collections.OrderedDict[str, RequestLocaleInfo] diff --git a/securedrop/sdconfig.py b/securedrop/sdconfig.py index 64db5813d3..3c01b990cb 100644 --- a/securedrop/sdconfig.py +++ b/securedrop/sdconfig.py @@ -8,6 +8,9 @@ from typing import Set +FALLBACK_LOCALE = "en_US" + + class SDConfig: def __init__(self) -> None: try: @@ -120,7 +123,7 @@ def __init__(self) -> None: # Config entries used by i18n.py # Use en_US as the default locale if the key is not defined in _config self.DEFAULT_LOCALE = getattr( - _config, "DEFAULT_LOCALE", "en_US" + _config, "DEFAULT_LOCALE", FALLBACK_LOCALE, ) # type: str supported_locales = set(getattr( _config, "SUPPORTED_LOCALES", [self.DEFAULT_LOCALE] diff --git a/securedrop/tests/test_i18n.py b/securedrop/tests/test_i18n.py index 1552e619a5..2caff23bb4 100644 --- a/securedrop/tests/test_i18n.py +++ b/securedrop/tests/test_i18n.py @@ -225,11 +225,43 @@ def test_i18n(journalist_app, config): verify_i18n(app) -def test_supported_locales(config): +def test_no_usable_default_locale(config): + ''' + The apps fail if neither the default nor the fallback locale is usable. + ''' fake_config = SDConfig() + fake_config.DEFAULT_LOCALE = 'eo' # Esperanto + fake_config.SUPPORTED_LOCALES = ['eo'] + fake_config.TRANSLATION_DIRS = Path(config.TEMP_DIR) + + with pytest.raises(ValueError, match='in the set of usable locales'): + journalist_app_module.create_app(fake_config) + + with pytest.raises(ValueError, match='in the set of usable locales'): + source_app.create_app(fake_config) + + +def test_unusable_default_but_usable_fallback_locale(config, caplog): + ''' + The apps start even if the default locale is unusable, as along as the fallback locale is + usable, but log an error for OSSEC to pick up. + ''' + fake_config = SDConfig() + fake_config.DEFAULT_LOCALE = 'eo' # Esperanto + fake_config.SUPPORTED_LOCALES = ['eo', 'en_US'] + fake_config.TRANSLATION_DIRS = Path(config.TEMP_DIR) + + for app in (journalist_app_module.create_app(fake_config), + source_app.create_app(fake_config)): + with app.app_context(): + assert 'Configured locale "eo" is not in the set of usable locales' in caplog.text + - # Check that an invalid locale raises an error during app - # configuration. +def test_invalid_locales(config): + ''' + An invalid locale raises an error during app configuration. + ''' + fake_config = SDConfig() fake_config.SUPPORTED_LOCALES = ['en_US', 'yy_ZZ'] fake_config.TRANSLATION_DIRS = Path(config.TEMP_DIR) @@ -239,16 +271,21 @@ def test_supported_locales(config): with pytest.raises(UnknownLocaleError): source_app.create_app(fake_config) - # Check that a valid but unsupported locale raises an error during - # app configuration. + +def test_valid_but_unusable_locales(config, caplog): + ''' + The apps start with one or more unusable, but still valid, locales, but log an error for + OSSEC to pick up. + ''' + fake_config = SDConfig() + fake_config.SUPPORTED_LOCALES = ['en_US', 'wae_CH'] fake_config.TRANSLATION_DIRS = Path(config.TEMP_DIR) - with pytest.raises(ValueError, match="not in the set of translated locales"): - journalist_app_module.create_app(fake_config) - - with pytest.raises(ValueError, match="not in the set of translated locales"): - source_app.create_app(fake_config) + for app in (journalist_app_module.create_app(fake_config), + source_app.create_app(fake_config)): + with app.app_context(): + assert 'Configured locale "wae_CH" is not in the set of usable locales' in caplog.text def test_language_tags():