diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index ae577bfa..1780b245 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -146,6 +146,21 @@ def apply_options_in_url(logger, request_options: RequestOptionsType, value: str def is_truthy(value: str): return value.lower() in ["yes", "y", "1", "true"] + @staticmethod + def _resolve_locale(args, logger=None): + # tabcmd Classic accepts --language and --country to control + # export locale. tabcmd 2 exposes both as global flags but only --language + # was reaching the REST API. Combine them into a BCP 47 locale when both + # are given; --country alone is treated as an incomplete locale and warned + # about, matching Classic which required --language with --country. + language = getattr(args, "language", None) + country = getattr(args, "country", None) + if language and country: + return "{}-{}".format(language, country) + if country and not language and logger is not None: + logger.warning(_("export.locale.country_without_language").format(country)) + return language + @staticmethod def apply_png_options(logger, request_options: TSC.ImageRequestOptions, args): # these are only used in export, not get @@ -157,8 +172,9 @@ def apply_png_options(logger, request_options: TSC.ImageRequestOptions, args): request_options.image_resolution = None else: request_options.image_resolution = TSC.ImageRequestOptions.Resolution.High.lower() - if args.language: - request_options.language = args.language + locale = DatasourcesAndWorkbooks._resolve_locale(args, logger) + if locale: + request_options.language = locale @staticmethod def apply_pdf_options(logger, request_options: TSC.PDFRequestOptions, args): @@ -170,13 +186,15 @@ def apply_pdf_options(logger, request_options: TSC.PDFRequestOptions, args): request_options.viz_height = int(args.height) if args.width: request_options.viz_width = int(args.width) - if args.language: - request_options.language = args.language + locale = DatasourcesAndWorkbooks._resolve_locale(args, logger) + if locale: + request_options.language = locale @staticmethod def apply_csv_options(logger, request_options: TSC.CSVRequestOptions, args): - if args.language: - request_options.language = args.language + locale = DatasourcesAndWorkbooks._resolve_locale(args, logger) + if locale: + request_options.language = locale @staticmethod def save_to_data_file(logger, output, filename): diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 2a4183e7..c2e10d30 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -103,8 +103,10 @@ def parent_parser_with_global_options(): parser.add_argument( "--country", - choices=["de", "en", "es", "fr", "it", "ja", "ko", "pt", "sv", "zh"], - type=str.lower, # coerce input to lowercase to act case insensitive + # ISO 3166-1 alpha-2 country code (case-insensitive). Combined with --language + # to form a locale (e.g. --language en --country GB -> "en-GB"). Left + # unconstrained on choices since Classic accepts any 2-letter country code. + type=str.upper, help=_("export.options.country"), ) diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index 7eda4bff..ad1dfefd 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -51,6 +51,7 @@ encryptextracts.status=Scheduling extracts on site {0} to be encrypted... export.errors.white_space_workbook_view=The name of the workbook or view to export cannot include spaces. Use the normalized name of the workbook or view as it appears in the URL. export.errors.requires_workbook_view_param=The ''{0}'' command requires a / parameter, and there must be at least one slash (/) in this parameter export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid +export.locale.country_without_language=--country {0} was ignored: --country requires --language (e.g. --language en --country GB). Using the site''s default locale. export.options.country=If not using user''s default locale, the country abbreviation for locale (find in IANA Language Subtag Registry). Must use with --language export.options.csv=Export data in CSV format (default) export.options.fullpdf=Export visual views in PDF format (if workbook was published with tabs) diff --git a/tests/commands/test_datasources_and_workbooks_command.py b/tests/commands/test_datasources_and_workbooks_command.py index 8fa60dde..f93e2b16 100644 --- a/tests/commands/test_datasources_and_workbooks_command.py +++ b/tests/commands/test_datasources_and_workbooks_command.py @@ -185,10 +185,62 @@ def test_apply_csv_options(self): def test_apply_csv_options_with_language(self): mock_args.language = "de" + mock_args.country = None request_options = tsc.CSVRequestOptions() DatasourcesAndWorkbooks.apply_csv_options(mock_logger, request_options, mock_args) assert request_options.language == "de" + def test_apply_csv_options_with_language_and_country(self): + # tabcmd Classic combined --language + --country into a locale (issue #109). + local_args = argparse.Namespace(language="en", country="GB") + request_options = tsc.CSVRequestOptions() + DatasourcesAndWorkbooks.apply_csv_options(mock_logger, request_options, local_args) + assert request_options.language == "en-GB" + + def test_apply_png_options_with_language_and_country(self): + local_args = argparse.Namespace(language="fr", country="CA", height="600", width="800", resolution=None) + request_options = tsc.ImageRequestOptions() + DatasourcesAndWorkbooks.apply_png_options(mock_logger, request_options, local_args) + assert request_options.language == "fr-CA" + + def test_apply_pdf_options_with_language_and_country(self): + local_args = argparse.Namespace( + language="pt", + country="BR", + height=800, + width=600, + pagelayout=tsc.PDFRequestOptions.Orientation.Portrait.__str__(), + pagesize=tsc.PDFRequestOptions.PageType.Folio.__str__(), + ) + request_options = tsc.PDFRequestOptions() + DatasourcesAndWorkbooks.apply_pdf_options(mock_logger, request_options, local_args) + assert request_options.language == "pt-BR" + + def test_resolve_locale_country_without_language_is_dropped(self): + # --country alone is not a valid locale; drop it and don't send to server. + local_args = argparse.Namespace(language=None, country="US") + assert DatasourcesAndWorkbooks._resolve_locale(local_args) is None + + def test_resolve_locale_country_without_language_warns(self): + # The user's --country was silently dropped in prior revisions; make sure + # they get a warning. In tests the gettext catalog isn't loaded so `_()` + # returns the raw key -- assert on the key rather than the English text. + local_args = argparse.Namespace(language=None, country="US") + warn_logger = mock.MagicMock() + assert DatasourcesAndWorkbooks._resolve_locale(local_args, warn_logger) is None + warn_logger.warning.assert_called_once() + assert "country_without_language" in warn_logger.warning.call_args[0][0] + + def test_resolve_locale_language_only(self): + local_args = argparse.Namespace(language="ja", country=None) + assert DatasourcesAndWorkbooks._resolve_locale(local_args) == "ja" + + def test_resolve_locale_language_only_does_not_warn(self): + local_args = argparse.Namespace(language="ja", country=None) + warn_logger = mock.MagicMock() + DatasourcesAndWorkbooks._resolve_locale(local_args, warn_logger) + warn_logger.warning.assert_not_called() + @mock.patch("tableauserverclient.Server") class MockedServerTests(unittest.TestCase):