From e316138a0555e87369726456c9832b59e648b5bf Mon Sep 17 00:00:00 2001 From: Ianis Vasilev Date: Sat, 1 Mar 2025 17:31:06 +0200 Subject: [PATCH] Add localization * Use a custom module that wraps gettext and allows setting the locale. * Add a directory with .po files, and include the corresponding .mo files to avoid requiring everybody to compile them. * Add Russian and Bulgarian localization. --- CHANGES.rst | 4 + docs/contributing.md | 11 ++ locales/bg_BG/LC_MESSAGES/click.po | 180 +++++++++++++++++++ locales/compile.sh | 9 + locales/en_US/LC_MESSAGES/click.po | 6 + locales/ru_RU/LC_MESSAGES/click.po | 180 +++++++++++++++++++ pyproject.toml | 1 + src/click/__init__.py | 3 + src/click/_termui_impl.py | 2 +- src/click/core.py | 4 +- src/click/decorators.py | 2 +- src/click/exceptions.py | 4 +- src/click/formatting.py | 2 +- src/click/locales.py | 59 ++++++ src/click/locales/bg_BG/LC_MESSAGES/click.mo | Bin 0 -> 5991 bytes src/click/locales/en_US/LC_MESSAGES/click.mo | Bin 0 -> 193 bytes src/click/locales/ru_RU/LC_MESSAGES/click.mo | Bin 0 -> 6069 bytes src/click/parser.py | 4 +- src/click/shell_completion.py | 2 +- src/click/termui.py | 4 +- src/click/testing.py | 14 ++ src/click/types.py | 4 +- tests/test_locales.py | 75 ++++++++ 23 files changed, 556 insertions(+), 14 deletions(-) create mode 100644 locales/bg_BG/LC_MESSAGES/click.po create mode 100755 locales/compile.sh create mode 100644 locales/en_US/LC_MESSAGES/click.po create mode 100644 locales/ru_RU/LC_MESSAGES/click.po create mode 100644 src/click/locales.py create mode 100644 src/click/locales/bg_BG/LC_MESSAGES/click.mo create mode 100644 src/click/locales/en_US/LC_MESSAGES/click.mo create mode 100644 src/click/locales/ru_RU/LC_MESSAGES/click.mo create mode 100644 tests/test_locales.py diff --git a/CHANGES.rst b/CHANGES.rst index 77f4b4b4b1..7adf27d35a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,10 @@ Version 8.4.0 Unreleased +- Allow customizing locales via ``click.set_click_locale`` and ``click.reset_click_locale`` + and querying the active locale via ``click.get_click_locale``. + Click now uses its own gettext domain. :issue:`2706` + - :class:`ParamType` typing improvements. :pr:`3371` - :class:`ParamType` is now a generic abstract base class, diff --git a/docs/contributing.md b/docs/contributing.md index 6ce30ef90b..71dac0b01f 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -42,3 +42,14 @@ external package for it. ## Formatting Wrap lines in Markdown files at 80 characters. + +## Translations + +If you want to work on translations: + +* Install gettext. Windows requires a unix-like environment - see [MinGW gettext](https://gnuwin32.sourceforge.net/packages/gettext.htm). + +* Compile the translations. + ```shell-session + $ bash ./locales/compile.sh + ``` diff --git a/locales/bg_BG/LC_MESSAGES/click.po b/locales/bg_BG/LC_MESSAGES/click.po new file mode 100644 index 0000000000..b37d4d3f22 --- /dev/null +++ b/locales/bg_BG/LC_MESSAGES/click.po @@ -0,0 +1,180 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bg_BG" +"Plural-Forms: nplurals=2; plural=n == 1 ? 0 : 1;" + +msgid "{editor}: Editing failed" +msgstr "{editor}: Редактирането се провалили" + +msgid "{editor}: Editing failed: {e}" +msgstr "{editor}: Редактирането се провалили: {e}" + +msgid "Aborted!" +msgstr "Авария!" + +msgid "Show this message and exit." +msgstr "Покажи това съобщение и излез." + +msgid "Usage:" +msgstr "Използване:" + +msgid "(Deprecated) {text}" +msgstr "(Депрекиран) {text}" + +msgid "Options" +msgstr "Опции" + +msgid "Got unexpected extra argument ({args})" +msgid_plural "Got unexpected extra arguments ({args})" +msgstr[0] "Получен е неочакван допълнителен аргумент ({args})" +msgstr[1] "Получени са неочаквани допълнителни аргументи ({args})" + +msgid "DeprecationWarning: The command {name!r} is deprecated." +msgstr "DeprecationWarning: Командата {name!r} е депрекирана." + +msgid "Commands" +msgstr "Команди" + +msgid "Missing command." +msgstr "Липсва команда." + +msgid "No such command {name!r}." +msgstr "Няма команда {name!r}." + +msgid "Argument {name!r} takes {nargs} values but 1 was given." +msgid_plural "Argument {name!r} takes {nargs} values but {len} were given." +msgstr[0] "Аргументът {name!r} приема {nargs} стойности, но е дадена 1." +msgstr[1] "Аргументът {name!r} приема {nargs} стойности, но са дадени {len}." + +msgid "env var: {var}" +msgstr "променлива на средата: {var}" + +msgid "default: {default}" +msgstr "по подразбиране: {default}" + +msgid "required" +msgstr "задължително" + +msgid "%(prog)s, version %(version)s" +msgstr "%(prog)s, версия %(version)s" + +msgid "Show the version and exit." +msgstr "Покажи версията и излез." + +msgid "Error: {message}" +msgstr "Грешка: {message}" + +msgid "Try '{command} {option}' for help." +msgstr "Опитайте '{command} {option}' за помощ." + +msgid "Invalid value: {message}" +msgstr "Невалидна стойност." + +msgid "Invalid value for {param_hint}: {message}" +msgstr "Невалидна стойност за {param_hint}: {message}" + +msgid "Missing argument." +msgstr "Липсващ аргумент." + +msgid "Missing option." +msgstr "Липсваща опция." + +msgid "Missing parameter." +msgstr "Липсващ параметър." + +msgid "Missing {param_type}." +msgstr "Липсва {param_type}." + +msgid "Missing parameter: {param_name}" +msgstr "Липсващ параметър: {param_name}" + +msgid "No such option: {name}" +msgstr "Няма такава опция: {name}." + +msgid "Did you mean {possibility}?" +msgid_plural "(Possible options: {possibilities})" +msgstr[0] "Имали сте предвид {possibility}?" +msgstr[1] "(Възможни варианти {possibilities})" + +msgid "unknown error" +msgstr "неизвестна грешка" + +msgid "Could not open file {filename!r}: {message}" +msgstr "Не можах да отворя файла {filename!r}: {message}" + +msgid "Argument {name!r} takes {nargs} values." +msgstr "Аргументът {name!r} приема {nargs} стойности." + +msgid "Option {name!r} does not take a value." +msgstr "Опцията {name!r} не приема стойности." + +msgid "Option {name!r} requires an argument." +msgid_plural "Option {name!r} requires {nargs} arguments." +msgstr[0] "Опцията {name!r} изисква аргумент." +msgstr[1] "Опцията {name!r} изисква {nargs} аргумента." + +msgid "Repeat for confirmation" +msgstr "Повторете за потвърждение" + +msgid "Error: The value you entered was invalid." +msgstr "Грешка: Въведената стойност е невалидна." + +msgid "Error: The two entered values do not match." +msgstr "Грешка: Двете въведени стойности не съвпадат." + +msgid "Error: invalid input." +msgstr "Грешка: невалидна въведена стойност." + +msgid "Press any key to continue..." +msgstr "Натиснете произволен клавиш, за да продължите..." + +msgid "Choose from:\n\t{choices}" +msgstr "Изберете сред:\n\t{choices}" + +msgid "{value!r} is not {choice}." +msgid_plural "{value!r} is not one of {choices}." +msgstr[0] "{value!r} не съвпада с {choice}." +msgstr[1] "{value!r} не е сред {choices}." + +msgid "{value!r} does not match the format {format}." +msgid_plural "{value!r} is not one of {choices}." +msgstr[0] "{value!r} няма формат {format}." +msgstr[1] "{value!r} не е сред форматите {formats}." + +msgid "{value!r} is not a valid {number_type}." +msgstr "{value!r} не е валидно {number_type}." + +msgid "{value!r} is not a valid {range}." +msgstr "{value!r} не в диапазона {range}." + +msgid "{value!r} is not a valid boolean." +msgstr "{value!r} не е валидна булева стойност." + +msgid "{value!r} is not a valid UUID." +msgstr "{value!r} не е валидно UUID." + +msgid "file" +msgstr "файл" + +msgid "directory" +msgstr "директория" + +msgid "path" +msgstr "път" + +msgid "{name} {filename!r} does not exist." +msgstr "{name} {filename!r} не съществува." + +msgid "{name} {filename!r} is a file." +msgstr "{name} {filename!r} е файл." + +msgid "{name} {filename!r} is a directory." +msgstr "{name} {filename!r} е директория." + +msgid "{name} {filename!r} is not writable." +msgstr "не е позволено записването в {name} {filename!r}." + +msgid "{name} {filename!r} is not readable." +msgstr "не е позволено четенето от {name} {filename!r}." diff --git a/locales/compile.sh b/locales/compile.sh new file mode 100755 index 0000000000..4f17aa29dd --- /dev/null +++ b/locales/compile.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +for file in locales/*/LC_MESSAGES/click.po; do + locale=$(basename $(dirname $(dirname $file))) + output_dir=src/click/$(dirname $file) + mkdir --parents $output_dir + echo "Building .mo file for ${locale}" + msgfmt --statistics --check-format $file -o $output_dir/click.mo +done diff --git a/locales/en_US/LC_MESSAGES/click.po b/locales/en_US/LC_MESSAGES/click.po new file mode 100644 index 0000000000..b1e9468e20 --- /dev/null +++ b/locales/en_US/LC_MESSAGES/click.po @@ -0,0 +1,6 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_US" +"Plural-Forms: nplurals=2; plural=n == 1 ? 0 : 1;" diff --git a/locales/ru_RU/LC_MESSAGES/click.po b/locales/ru_RU/LC_MESSAGES/click.po new file mode 100644 index 0000000000..5522ab872b --- /dev/null +++ b/locales/ru_RU/LC_MESSAGES/click.po @@ -0,0 +1,180 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru_RU" +"Plural-Forms: nplurals=2; plural=n == 1 ? 0 : 1;" + +msgid "{editor}: Editing failed" +msgstr "{editor}: Редактирование провалилось" + +msgid "{editor}: Editing failed: {e}" +msgstr "{editor}: Редактирование провалилось: {e}" + +msgid "Aborted!" +msgstr "Авария!" + +msgid "Show this message and exit." +msgstr "Показать это сообщение и выйти." + +msgid "Usage:" +msgstr "Использование:" + +msgid "(Deprecated) {text}" +msgstr "(Депрекированый) {text}" + +msgid "Options" +msgstr "Опции" + +msgid "Got unexpected extra argument ({args})" +msgid_plural "Got unexpected extra arguments ({args})" +msgstr[0] "Получен неожиданный дополнительный аргумент ({args})" +msgstr[1] "Получены неожиданные дополнительные аргументы ({args})" + +msgid "DeprecationWarning: The command {name!r} is deprecated." +msgstr "DeprecationWarning: Команда {name!r} является депрекированой." + +msgid "Commands" +msgstr "Команды" + +msgid "Missing command." +msgstr "Отсутствует команда." + +msgid "No such command {name!r}." +msgstr "Нет команды {name!r}." + +msgid "Argument {name!r} takes {nargs} values but 1 was given." +msgid_plural "Argument {name!r} takes {nargs} values but {len} were given." +msgstr[0] "Аргумент {name!r} принимает {nargs} значений, но дано 1." +msgstr[1] "Аргумент {name!r} принимает {nargs} значений, но даны {len}." + +msgid "env var: {var}" +msgstr "переменная среды: {var}" + +msgid "default: {default}" +msgstr "по умолчанию: {default}" + +msgid "required" +msgstr "обязательно" + +msgid "%(prog)s, version %(version)s" +msgstr "%(prog)s, версия %(version)s" + +msgid "Show the version and exit." +msgstr "Показать версию и выйти." + +msgid "Error: {message}" +msgstr "Ошибка: {message}" + +msgid "Try '{command} {option}' for help." +msgstr "Попробуйте '{command} {option}' для помощи." + +msgid "Invalid value: {message}" +msgstr "Невалидное значение." + +msgid "Invalid value for {param_hint}: {message}" +msgstr "Невалидное значение для {param_hint}: {message}" + +msgid "Missing argument." +msgstr "Отсутствующий аргумент." + +msgid "Missing option." +msgstr "Отсутствующая опция." + +msgid "Missing parameter." +msgstr "Отсутствующий параметр." + +msgid "Missing {param_type}." +msgstr "Отсутствует {param_type}." + +msgid "Missing parameter: {param_name}" +msgstr "Отсутствующий параметр: {param_name}" + +msgid "No such option: {name}" +msgstr "Нет такой опции: {name}." + +msgid "Did you mean {possibility}?" +msgid_plural "(Possible options: {possibilities})" +msgstr[0] "Вы имели ввиду {possibility}?" +msgstr[1] "(Возможные варианты {possibilities})" + +msgid "unknown error" +msgstr "неизвестная ошибка" + +msgid "Could not open file {filename!r}: {message}" +msgstr "Не смог открыть файл {filename!r}: {message}" + +msgid "Argument {name!r} takes {nargs} values." +msgstr "Аргумент {name!r} принимает {nargs} значений." + +msgid "Option {name!r} does not take a value." +msgstr "Опция {name!r} не принимает значений." + +msgid "Option {name!r} requires an argument." +msgid_plural "Option {name!r} requires {nargs} arguments." +msgstr[0] "Опция {name!r} требует аргумент." +msgstr[1] "Опция {name!r} требует {nargs} аргументов." + +msgid "Repeat for confirmation" +msgstr "Повторите для подтверждения" + +msgid "Error: The value you entered was invalid." +msgstr "Ошибка: Введённое значение является невалидным." + +msgid "Error: The two entered values do not match." +msgstr "Ошибка: Два введённых значения не совпадают." + +msgid "Error: invalid input." +msgstr "Ошибка: невалидный ввод." + +msgid "Press any key to continue..." +msgstr "Для продолжения нажмите любую кнопку..." + +msgid "Choose from:\n\t{choices}" +msgstr "Выберите среди:\n\t{choices}" + +msgid "{value!r} is not {choice}." +msgid_plural "{value!r} is not one of {choices}." +msgstr[0] "{value!r} не совпадает с {choice}." +msgstr[1] "{value!r} не среди {choices}." + +msgid "{value!r} does not match the format {format}." +msgid_plural "{value!r} is not one of {choices}." +msgstr[0] "{value!r} не в формате {format}." +msgstr[1] "{value!r} ни в одном из форматов {formats}." + +msgid "{value!r} is not a valid {number_type}." +msgstr "{value!r} является невалидным {number_type}." + +msgid "{value!r} is not a valid {range}." +msgstr "{value!r} не в диапазоне {range}." + +msgid "{value!r} is not a valid boolean." +msgstr "{value!r} является невалидным булевым значением." + +msgid "{value!r} is not a valid UUID." +msgstr "{value!r} является невалидным UUID." + +msgid "file" +msgstr "файл" + +msgid "directory" +msgstr "директория" + +msgid "path" +msgstr "путь" + +msgid "{name} {filename!r} does not exist." +msgstr "{name} {filename!r} не существует." + +msgid "{name} {filename!r} is a file." +msgstr "{name} {filename!r} файл." + +msgid "{name} {filename!r} is a directory." +msgstr "{name} {filename!r} директория." + +msgid "{name} {filename!r} is not writable." +msgstr "не позволено записывать в {name} {filename!r}." + +msgid "{name} {filename!r} is not readable." +msgstr "не позволено читать {name} {filename!r}." diff --git a/pyproject.toml b/pyproject.toml index 5a0e37d048..56da00ef1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ name = "click" include = [ "docs/", "tests/", + "locales/", "CHANGES.rst", "uv.lock" ] diff --git a/src/click/__init__.py b/src/click/__init__.py index 64be7e0c3c..a476d5474a 100644 --- a/src/click/__init__.py +++ b/src/click/__init__.py @@ -39,6 +39,9 @@ from .formatting import HelpFormatter as HelpFormatter from .formatting import wrap_text as wrap_text from .globals import get_current_context as get_current_context +from .locales import get_click_locale as get_click_locale +from .locales import reset_click_locale as reset_click_locale +from .locales import set_click_locale as set_click_locale from .termui import clear as clear from .termui import confirm as confirm from .termui import echo_via_pager as echo_via_pager diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index 219fbaa1f7..ca215b2e81 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -15,7 +15,6 @@ import sys import time import typing as t -from gettext import gettext as _ from io import StringIO from pathlib import Path from types import TracebackType @@ -28,6 +27,7 @@ from ._compat import term_len from ._compat import WIN from .exceptions import ClickException +from .locales import gettext as _ from .utils import echo V = t.TypeVar("V") diff --git a/src/click/core.py b/src/click/core.py index c5cab15c07..a66ff95418 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -15,8 +15,6 @@ from contextlib import contextmanager from contextlib import ExitStack from functools import update_wrapper -from gettext import gettext as _ -from gettext import ngettext from itertools import repeat from types import TracebackType @@ -35,6 +33,8 @@ from .formatting import join_options from .globals import pop_context from .globals import push_context +from .locales import gettext as _ +from .locales import ngettext from .parser import _OptionParser from .parser import _split_opt from .termui import confirm diff --git a/src/click/decorators.py b/src/click/decorators.py index 14aee42ea4..3737f98656 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -3,7 +3,6 @@ import inspect import typing as t from functools import update_wrapper -from gettext import gettext as _ from .core import Argument from .core import Command @@ -12,6 +11,7 @@ from .core import Option from .core import Parameter from .globals import get_current_context +from .locales import gettext as _ from .utils import echo if t.TYPE_CHECKING: diff --git a/src/click/exceptions.py b/src/click/exceptions.py index 4914d9cfe2..6436eb5aee 100644 --- a/src/click/exceptions.py +++ b/src/click/exceptions.py @@ -2,11 +2,11 @@ import collections.abc as cabc import typing as t -from gettext import gettext as _ -from gettext import ngettext from ._compat import get_text_stderr from .globals import resolve_color_default +from .locales import gettext as _ +from .locales import ngettext from .utils import echo from .utils import format_filename diff --git a/src/click/formatting.py b/src/click/formatting.py index de2ca47117..b9ba767fe3 100644 --- a/src/click/formatting.py +++ b/src/click/formatting.py @@ -2,9 +2,9 @@ import collections.abc as cabc from contextlib import contextmanager -from gettext import gettext as _ from ._compat import term_len +from .locales import gettext as _ from .parser import _split_opt # Can force a width. This is used by the test system diff --git a/src/click/locales.py b/src/click/locales.py new file mode 100644 index 0000000000..9c65ecbff1 --- /dev/null +++ b/src/click/locales.py @@ -0,0 +1,59 @@ +import os +from gettext import NullTranslations +from gettext import translation as build_translations_object + +_translations: NullTranslations = NullTranslations() +_active_locale: str | None = None + + +LOCALE_ROOT_PATH = os.path.join(os.path.dirname(__file__), "locales") + + +def reset_click_locale() -> None: + """Reset the active locale.""" + + global _active_locale + global _translations + _active_locale = None + _translations = NullTranslations() + + +def set_click_locale(target_locale: str) -> None: + """Set the locale. If the locale is unrecognized, a NotImplementedError is raised. + + :param target_locale: The name of the locale, e.g. en_US + """ + global _active_locale + global _translations + + try: + _translations = build_translations_object( + "click", LOCALE_ROOT_PATH, [target_locale] + ) + except FileNotFoundError: + raise NotImplementedError(f"Unrecognized locale {target_locale}") from None + else: + _active_locale = target_locale + + +def get_click_locale() -> str | None: + """Get the active locale.""" + return _active_locale + + +def gettext(message: str) -> str: + """Wrapper around the gettext.gettext function that respects click's active locale. + + :param message: The message id to translate. + """ + return _translations.gettext(message) + + +def ngettext(msgid1: str, msgid2: str, n: int) -> str: + """Wrapper around the gettext.ngettext function that respects click's active locale. + + :param msgid1: Singular form of the message id. + :param msgid2: Plural form of the message id. + :n: The number used to determine the form. + """ + return _translations.ngettext(msgid1, msgid2, n) diff --git a/src/click/locales/bg_BG/LC_MESSAGES/click.mo b/src/click/locales/bg_BG/LC_MESSAGES/click.mo new file mode 100644 index 0000000000000000000000000000000000000000..6b8baab224008c0ede2c7a211df52c444a8824f6 GIT binary patch literal 5991 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 B`L+N6 literal 0 HcmV?d00001 diff --git a/src/click/locales/en_US/LC_MESSAGES/click.mo b/src/click/locales/en_US/LC_MESSAGES/click.mo new file mode 100644 index 0000000000000000000000000000000000000000..5837205c0c7d81bc8e2fd9bb04d38d0a8c474f56 GIT binary patch literal 193 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 str: if hide_input: echo(_("Error: The value you entered was invalid."), err=err) else: - echo(_("Error: {e.message}").format(e=e), err=err) + echo(_("Error: {message}").format(message=e.message), err=err) continue if not confirmation_prompt: return result diff --git a/src/click/testing.py b/src/click/testing.py index 04e7f1d925..f0eb334e02 100644 --- a/src/click/testing.py +++ b/src/click/testing.py @@ -13,6 +13,7 @@ from . import _compat from . import formatting +from . import locales from . import termui from . import utils from ._compat import _find_binary_reader @@ -667,3 +668,16 @@ def isolated_filesystem( shutil.rmtree(dt) except OSError: pass + + +@contextlib.contextmanager +def ContextualizedLocale(target_locale: str) -> cabc.Iterator[None]: + old_locale = locales.get_click_locale() + locales.set_click_locale(target_locale) + + yield + + if old_locale is None: + locales.reset_click_locale() + else: + locales.set_click_locale(old_locale) diff --git a/src/click/types.py b/src/click/types.py index 556f20f2b8..7b49f5972a 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -9,12 +9,12 @@ import typing as t import uuid from datetime import datetime -from gettext import gettext as _ -from gettext import ngettext from ._compat import _get_argv_encoding from ._compat import open_stream from .exceptions import BadParameter +from .locales import gettext as _ +from .locales import ngettext from .utils import format_filename from .utils import LazyFile from .utils import safecall diff --git a/tests/test_locales.py b/tests/test_locales.py new file mode 100644 index 0000000000..b891bfc46b --- /dev/null +++ b/tests/test_locales.py @@ -0,0 +1,75 @@ +from textwrap import dedent + +import pytest + +import click +from click.locales import set_click_locale +from click.testing import CliRunner +from click.testing import ContextualizedLocale + + +def test_test_unrecognized_locale(runner: CliRunner) -> None: + @click.command() + def cli() -> None: + pass + + with pytest.raises(NotImplementedError): + set_click_locale("unrecognized") + + +def test_help_translation_to_russian(runner: CliRunner) -> None: + @click.command() + def cli() -> None: + pass + + with ContextualizedLocale("ru_RU"): + result = runner.invoke(cli, ["--help"]) + + assert result.stdout == dedent("""\ + Использование: cli [OPTIONS] + + Опции: + --help Показать это сообщение и выйти. + """) + + +def test_help_translation_to_bulgarian(runner: CliRunner) -> None: + @click.command() + def cli() -> None: + pass + + with ContextualizedLocale("bg_BG"): + result = runner.invoke(cli, ["--help"]) + + assert result.stdout == dedent("""\ + Използване: cli [OPTIONS] + + Опции: + --help Покажи това съобщение и излез. + """) + + +def test_dynamic_change_of_language(runner: CliRunner) -> None: + @click.command() + def cli() -> None: + pass + + with ContextualizedLocale("en_US"): + result = runner.invoke(cli, ["--help"]) + + assert result.stdout == dedent("""\ + Usage: cli [OPTIONS] + + Options: + --help Show this message and exit. + """) + + with ContextualizedLocale("ru_RU"): + result = runner.invoke(cli, ["--help"]) + + assert result.stdout == dedent("""\ + Использование: cli [OPTIONS] + + Опции: + --help Показать это сообщение и выйти. + """)