From 03e2fee8a99f08976aef0d38cb3fa372a94c928a Mon Sep 17 00:00:00 2001 From: Saif807380 Date: Thu, 1 Apr 2021 17:11:37 +0530 Subject: [PATCH 1/2] mark cli messages for translation --- CHANGES.rst | 2 +- src/click/core.py | 9 ++++++--- src/click/decorators.py | 10 +++++++--- src/click/exceptions.py | 18 ++++++++++++++---- src/click/termui.py | 11 ++++++----- tests/test_imports.py | 1 + 6 files changed, 35 insertions(+), 16 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 907e21ae21..172770e776 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -197,7 +197,7 @@ Unreleased :issue:`1791` - When taking arguments from ``sys.argv`` on Windows, glob patterns, user dir, and env vars are expanded. :issue:`1096` - +- Wrapped public messages with ``_(gettext)`` for i18n support. :issue:`303` Version 7.1.2 ------------- diff --git a/src/click/core.py b/src/click/core.py index 80b1075a56..1b91af9cda 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -6,6 +6,7 @@ from contextlib import contextmanager from contextlib import ExitStack from functools import update_wrapper +from gettext import gettext as _ from itertools import repeat from ._unicodefun import _verify_python_env @@ -995,7 +996,7 @@ def main( except Abort: if not standalone_mode: raise - echo("Aborted!", file=sys.stderr) + echo(_("Aborted!"), file=sys.stderr) sys.exit(1) def _main_shell_completion(self, ctx_args, prog_name, complete_var=None): @@ -1170,7 +1171,7 @@ def show_help(ctx, param, value): is_eager=True, expose_value=False, callback=show_help, - help="Show this message and exit.", + help=_("Show this message and exit."), ) def make_parser(self, ctx): @@ -1280,7 +1281,9 @@ def invoke(self, ctx): if self.deprecated: echo( style( - f"DeprecationWarning: The command {self.name!r} is deprecated.", + _( + "DeprecationWarning: The command {self.name!r} is deprecated." + ).format(self=self), fg="red", ), err=True, diff --git a/src/click/decorators.py b/src/click/decorators.py index 43c8fa2974..13fb156806 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -1,6 +1,7 @@ import inspect import typing as t from functools import update_wrapper +from gettext import gettext as _ from .core import Argument from .core import Command @@ -280,7 +281,7 @@ def version_option( *param_decls, package_name=None, prog_name=None, - message="%(prog)s, version %(version)s", + message=None, **kwargs, ): """Add a ``--version`` option which immediately prints the version @@ -315,6 +316,9 @@ def version_option( .. versionchanged:: 8.0 Use :mod:`importlib.metadata` instead of ``pkg_resources``. """ + if message is None: + message = _("%(prog)s, version %(version)s") + if version is None and package_name is None: frame = inspect.currentframe() f_globals = frame.f_back.f_globals if frame is not None else None @@ -381,7 +385,7 @@ def callback(ctx, param, value): kwargs.setdefault("is_flag", True) kwargs.setdefault("expose_value", False) kwargs.setdefault("is_eager", True) - kwargs.setdefault("help", "Show the version and exit.") + kwargs.setdefault("help", _("Show the version and exit.")) kwargs["callback"] = callback return option(*param_decls, **kwargs) @@ -412,6 +416,6 @@ def callback(ctx, param, value): kwargs.setdefault("is_flag", True) kwargs.setdefault("expose_value", False) kwargs.setdefault("is_eager", True) - kwargs.setdefault("help", "Show this message and exit.") + kwargs.setdefault("help", _("Show this message and exit.")) kwargs["callback"] = callback return option(*param_decls, **kwargs) diff --git a/src/click/exceptions.py b/src/click/exceptions.py index 9623cd8126..81a1469fff 100644 --- a/src/click/exceptions.py +++ b/src/click/exceptions.py @@ -1,3 +1,5 @@ +from gettext import gettext as _ + from ._compat import filename_to_ui from ._compat import get_text_stderr from .utils import echo @@ -28,7 +30,7 @@ def __str__(self): def show(self, file=None): if file is None: file = get_text_stderr() - echo(f"Error: {self.format_message()}", file=file) + echo(_("Error: {self.format_message()}").format(self=self), file=file) class UsageError(ClickException): @@ -59,8 +61,16 @@ def show(self, file=None): ) if self.ctx is not None: color = self.ctx.color - echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color) - echo(f"Error: {self.format_message()}", file=file, color=color) + echo( + _("{usage}\n{hint}").format(usage=self.ctx.get_usage(), hint=hint), + file=file, + color=color, + ) + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=color, + ) class BadParameter(UsageError): @@ -144,7 +154,7 @@ def format_message(self): def __str__(self): if self.message is None: param_name = self.param.name if self.param else None - return f"missing parameter: {param_name}" + return _("missing parameter: {param_name}").format(param_name=param_name) else: return self.message diff --git a/src/click/termui.py b/src/click/termui.py index 8342fdeafb..72b03f818c 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -4,6 +4,7 @@ import os import sys import typing as t +from gettext import gettext as _ from ._compat import is_bytes from ._compat import isatty @@ -145,7 +146,7 @@ def prompt_func(text): if confirmation_prompt: if confirmation_prompt is True: - confirmation_prompt = "Repeat for confirmation" + confirmation_prompt = _("Repeat for confirmation") confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix) @@ -161,9 +162,9 @@ def prompt_func(text): result = value_proc(value) except UsageError as e: if hide_input: - echo("Error: the value you entered was invalid", err=err) + echo(_("Error: the value you entered was invalid"), err=err) else: - echo(f"Error: {e.message}", err=err) # noqa: B306 + echo(_("Error: {e.message}").format(e=e), err=err) # noqa: B306 continue if not confirmation_prompt: return result @@ -173,7 +174,7 @@ def prompt_func(text): break if value == value2: return result - echo("Error: the two entered values do not match", err=err) + echo(_("Error: the two entered values do not match"), err=err) def confirm( @@ -222,7 +223,7 @@ def confirm( elif default is not None and value == "": rv = default else: - echo("Error: invalid input", err=err) + echo(_("Error: invalid input"), err=err) continue break if abort and not rv: diff --git a/tests/test_imports.py b/tests/test_imports.py index dd26972ab6..ec32fcafd9 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -47,6 +47,7 @@ def tracking_import(module, locals=None, globals=None, fromlist=None, "enum", "typing", "types", + "gettext", } if WIN: From 8d49e146ab8c2312e7917bb7c3f8abf01b8b55bf Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 3 Apr 2021 12:34:17 -0700 Subject: [PATCH 2/2] mark more messages for translation --- CHANGES.rst | 4 +- src/click/_termui_impl.py | 9 +++- src/click/_unicodefun.py | 67 ++++++++++++++++------------- src/click/core.py | 43 ++++++++++--------- src/click/decorators.py | 3 +- src/click/exceptions.py | 69 ++++++++++++++++++------------ src/click/formatting.py | 9 +++- src/click/parser.py | 18 ++++++-- src/click/shell_completion.py | 9 ++-- src/click/termui.py | 13 ++++-- src/click/types.py | 79 ++++++++++++++++++++++++++--------- tests/test_arguments.py | 4 +- tests/test_basic.py | 2 +- tests/test_options.py | 4 +- tests/test_termui.py | 2 +- 15 files changed, 218 insertions(+), 117 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 172770e776..13facd9b1b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -197,7 +197,9 @@ Unreleased :issue:`1791` - When taking arguments from ``sys.argv`` on Windows, glob patterns, user dir, and env vars are expanded. :issue:`1096` -- Wrapped public messages with ``_(gettext)`` for i18n support. :issue:`303` +- Marked messages shown by the CLI with ``gettext()`` to allow + applications to translate Click's built-in strings. :issue:`303` + Version 7.1.2 ------------- diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index 3c7d57f22c..0e9860bd8f 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -8,6 +8,7 @@ import os import sys import time +from gettext import gettext as _ from ._compat import _default_text_stdout from ._compat import CYGWIN @@ -489,9 +490,13 @@ def edit_file(self, filename): c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True) exit_code = c.wait() if exit_code != 0: - raise ClickException(f"{editor}: Editing failed!") + raise ClickException( + _("{editor}: Editing failed").format(editor=editor) + ) except OSError as e: - raise ClickException(f"{editor}: Editing failed: {e}") + raise ClickException( + _("{editor}: Editing failed: {e}").format(editor=editor, e=e) + ) def edit(self, text): import tempfile diff --git a/src/click/_unicodefun.py b/src/click/_unicodefun.py index 53ec9d267b..aa11024277 100644 --- a/src/click/_unicodefun.py +++ b/src/click/_unicodefun.py @@ -1,5 +1,6 @@ import codecs import os +from gettext import gettext as _ def _verify_python_env(): @@ -13,7 +14,15 @@ def _verify_python_env(): if fs_enc != "ascii": return - extra = "" + extra = [ + _( + "Click will abort further execution because Python was" + " configured to use ASCII as encoding for the environment." + " Consult https://click.palletsprojects.com/unicode-support/" + " for mitigation steps." + ) + ] + if os.name == "posix": import subprocess @@ -37,27 +46,32 @@ def _verify_python_env(): if locale.lower() in ("c.utf8", "c.utf-8"): has_c_utf8 = True - extra += "\n\n" if not good_locales: - extra += ( - "Additional information: on this system no suitable" - " UTF-8 locales were discovered. This most likely" - " requires resolving by reconfiguring the locale" - " system." + extra.append( + _( + "Additional information: on this system no suitable" + " UTF-8 locales were discovered. This most likely" + " requires resolving by reconfiguring the locale" + " system." + ) ) elif has_c_utf8: - extra += ( - "This system supports the C.UTF-8 locale which is" - " recommended. You might be able to resolve your issue" - " by exporting the following environment variables:\n\n" - " export LC_ALL=C.UTF-8\n" - " export LANG=C.UTF-8" + extra.append( + _( + "This system supports the C.UTF-8 locale which is" + " recommended. You might be able to resolve your" + " issue by exporting the following environment" + " variables:" + ) ) + extra.append(" export LC_ALL=C.UTF-8\n export LANG=C.UTF-8") else: - extra += ( - "This system lists some UTF-8 supporting locales that" - " you can pick from. The following suitable locales" - f" were discovered: {', '.join(sorted(good_locales))}" + extra.append( + _( + "This system lists some UTF-8 supporting locales" + " that you can pick from. The following suitable" + " locales were discovered: {locales}" + ).format(locales=", ".join(sorted(good_locales))) ) bad_locale = None @@ -67,16 +81,13 @@ def _verify_python_env(): if locale is not None: break if bad_locale is not None: - extra += ( - "\n\nClick discovered that you exported a UTF-8 locale" - " but the locale system could not pick up from it" - " because it does not exist. The exported locale is" - f" {bad_locale!r} but it is not supported" + extra.append( + _( + "Click discovered that you exported a UTF-8 locale" + " but the locale system could not pick up from it" + " because it does not exist. The exported locale is" + " {locale!r} but it is not supported." + ).format(locale=bad_locale) ) - raise RuntimeError( - "Click will abort further execution because Python was" - " configured to use ASCII as encoding for the environment." - " Consult https://click.palletsprojects.com/unicode-support/" - f" for mitigation steps.{extra}" - ) + raise RuntimeError("\n\n".join(extra)) diff --git a/src/click/core.py b/src/click/core.py index 1b91af9cda..a5e7a79b4f 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -7,6 +7,7 @@ from contextlib import ExitStack from functools import update_wrapper from gettext import gettext as _ +from gettext import ngettext from itertools import repeat from ._unicodefun import _verify_python_env @@ -1200,7 +1201,7 @@ def get_short_help_str(self, limit=45): text = make_default_short_help(self.help, limit) if self.deprecated: - text = f"(Deprecated) {text}" + text = _("(Deprecated) {text}").format(text=text) return text.strip() @@ -1226,7 +1227,7 @@ def format_help_text(self, ctx, formatter): text = self.help or "" if self.deprecated: - text = f"(Deprecated) {text}" + text = _("(Deprecated) {text}").format(text=text) if text: formatter.write_paragraph() @@ -1243,7 +1244,7 @@ def format_options(self, ctx, formatter): opts.append(rv) if opts: - with formatter.section("Options"): + with formatter.section(_("Options")): formatter.write_dl(opts) def format_epilog(self, ctx, formatter): @@ -1266,9 +1267,11 @@ def parse_args(self, ctx, args): if args and not ctx.allow_extra_args and not ctx.resilient_parsing: ctx.fail( - "Got unexpected extra" - f" argument{'s' if len(args) != 1 else ''}" - f" ({' '.join(map(make_str, args))})" + ngettext( + "Got unexpected extra argument ({args})", + "Got unexpected extra arguments ({args})", + len(args), + ).format(args=" ".join(map(str, args))) ) ctx.args = args @@ -1281,9 +1284,9 @@ def invoke(self, ctx): if self.deprecated: echo( style( - _( - "DeprecationWarning: The command {self.name!r} is deprecated." - ).format(self=self), + _("DeprecationWarning: The command {name!r} is deprecated.").format( + name=self.name + ), fg="red", ), err=True, @@ -1477,7 +1480,7 @@ def format_commands(self, ctx, formatter): rows.append((subcommand, help)) if rows: - with formatter.section("Commands"): + with formatter.section(_("Commands")): formatter.write_dl(rows) def parse_args(self, ctx, args): @@ -1509,7 +1512,7 @@ def _process_result(value): with ctx: super().invoke(ctx) return _process_result([] if self.chain else None) - ctx.fail("Missing command.") + ctx.fail(_("Missing command.")) # Fetch args back out args = ctx.protected_args + ctx.args @@ -1583,7 +1586,7 @@ def resolve_command(self, ctx, args): if cmd is None and not ctx.resilient_parsing: if split_opt(cmd_name)[0]: self.parse_args(ctx, ctx.args) - ctx.fail(f"No such command '{original_cmd_name}'.") + ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name)) return cmd.name if cmd else None, cmd, args[1:] def get_command(self, ctx, cmd_name): @@ -2061,10 +2064,12 @@ def process_value(self, ctx, value): else len(value) != self.nargs ) ): - were = "was" if len(value) == 1 else "were" ctx.fail( - f"Argument {self.name!r} takes {self.nargs} values but" - f" {len(value)} {were} given." + ngettext( + "Argument {name!r} takes {nargs} values but 1 was given.", + "Argument {name!r} takes {nargs} values but {len} were given.", + len(value), + ).format(name=self.name, nargs=self.nargs, len=len(value)) ) if self.callback is not None: @@ -2424,7 +2429,7 @@ def _write_opts(opts): if isinstance(envvar, (list, tuple)) else envvar ) - extra.append(f"env var: {var_str}") + extra.append(_("env var: {var}").format(var=var_str)) default_value = self.get_default(ctx, call=False) show_default_is_str = isinstance(self.show_default, str) @@ -2437,7 +2442,7 @@ def _write_opts(opts): elif isinstance(default_value, (list, tuple)): default_string = ", ".join(str(d) for d in default_value) elif callable(default_value): - default_string = "(dynamic)" + default_string = _("(dynamic)") elif self.is_bool_flag and self.secondary_opts: # For boolean flags that have distinct True/False opts, # use the opt without prefix instead of the value. @@ -2447,7 +2452,7 @@ def _write_opts(opts): else: default_string = default_value - extra.append(f"default: {default_string}") + extra.append(_("default: {default}").format(default=default_string)) if isinstance(self.type, _NumberRangeBase): range_str = self.type._describe_range() @@ -2456,7 +2461,7 @@ def _write_opts(opts): extra.append(range_str) if self.required: - extra.append("required") + extra.append(_("required")) if extra: extra_str = ";".join(extra) help = f"{help} [{extra_str}]" if help else f"[{extra_str}]" diff --git a/src/click/decorators.py b/src/click/decorators.py index 13fb156806..a447084c27 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -305,7 +305,8 @@ def version_option( :param prog_name: The name of the CLI to show in the message. If not provided, it will be detected from the command. :param message: The message to show. The values ``%(prog)s``, - ``%(package)s``, and ``%(version)s`` are available. + ``%(package)s``, and ``%(version)s`` are available. Defaults to + ``"%(prog)s, version %(version)s"``. :param kwargs: Extra arguments are passed to :func:`option`. :raise RuntimeError: ``version`` could not be detected. diff --git a/src/click/exceptions.py b/src/click/exceptions.py index 81a1469fff..ab9b31299e 100644 --- a/src/click/exceptions.py +++ b/src/click/exceptions.py @@ -1,4 +1,5 @@ from gettext import gettext as _ +from gettext import ngettext from ._compat import filename_to_ui from ._compat import get_text_stderr @@ -30,7 +31,7 @@ def __str__(self): def show(self, file=None): if file is None: file = get_text_stderr() - echo(_("Error: {self.format_message()}").format(self=self), file=file) + echo(_("Error: {message}").format(message=self.format_message()), file=file) class UsageError(ClickException): @@ -55,17 +56,13 @@ def show(self, file=None): color = None hint = "" if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None: - hint = ( - f"Try '{self.ctx.command_path}" - f" {self.ctx.help_option_names[0]}' for help.\n" + hint = _("Try '{command} {option}' for help.").format( + command=self.ctx.command_path, option=self.ctx.help_option_names[0] ) + hint = f"{hint}\n" if self.ctx is not None: color = self.ctx.color - echo( - _("{usage}\n{hint}").format(usage=self.ctx.get_usage(), hint=hint), - file=file, - color=color, - ) + echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color) echo( _("Error: {message}").format(message=self.format_message()), file=file, @@ -102,10 +99,11 @@ def format_message(self): elif self.param is not None: param_hint = self.param.get_error_hint(self.ctx) else: - return f"Invalid value: {self.message}" - param_hint = _join_param_hints(param_hint) + return _("Invalid value: {message}").format(message=self.message) - return f"Invalid value for {param_hint}: {self.message}" + return _("Invalid value for {param_hint}: {message}").format( + param_hint=_join_param_hints(param_hint), message=self.message + ) class MissingParameter(BadParameter): @@ -133,7 +131,9 @@ def format_message(self): param_hint = self.param.get_error_hint(self.ctx) else: param_hint = None + param_hint = _join_param_hints(param_hint) + param_hint = f" {param_hint}" if param_hint else "" param_type = self.param_type if param_type is None and self.param is not None: @@ -144,17 +144,28 @@ def format_message(self): msg_extra = self.param.type.get_missing_message(self.param) if msg_extra: if msg: - msg += f". {msg_extra}" + msg += f". {msg_extra}" else: msg = msg_extra - hint_str = f" {param_hint}" if param_hint else "" - return f"Missing {param_type}{hint_str}.{' ' if msg else ''}{msg or ''}" + msg = f" {msg}" if msg else "" + + # Translate param_type for known types. + if param_type == "argument": + missing = _("Missing argument") + elif param_type == "option": + missing = _("Missing option") + elif param_type == "parameter": + missing = _("Missing parameter") + else: + missing = _("Missing {param_type}").format(param_type=param_type) + + return f"{missing}{param_hint}.{msg}" def __str__(self): if self.message is None: param_name = self.param.name if self.param else None - return _("missing parameter: {param_name}").format(param_name=param_name) + return _("Missing parameter: {param_name}").format(param_name=param_name) else: return self.message @@ -168,21 +179,23 @@ class NoSuchOption(UsageError): def __init__(self, option_name, message=None, possibilities=None, ctx=None): if message is None: - message = f"no such option: {option_name}" + message = _("No such option: {name}").format(name=option_name) super().__init__(message, ctx) self.option_name = option_name self.possibilities = possibilities def format_message(self): - bits = [self.message] - if self.possibilities: - if len(self.possibilities) == 1: - bits.append(f"Did you mean {self.possibilities[0]}?") - else: - possibilities = sorted(self.possibilities) - bits.append(f"(Possible options: {', '.join(possibilities)})") - return " ".join(bits) + if not self.possibilities: + return self.message + + possibility_str = ", ".join(sorted(self.possibilities)) + suggest = ngettext( + "Did you mean {possibility}?", + "(Possible options: {possibilities})", + len(self.possibilities), + ).format(possibility=possibility_str, possibilities=possibility_str) + return f"{self.message} {suggest}" class BadOptionUsage(UsageError): @@ -215,14 +228,16 @@ class FileError(ClickException): def __init__(self, filename, hint=None): ui_filename = filename_to_ui(filename) if hint is None: - hint = "unknown error" + hint = _("unknown error") super().__init__(hint) self.ui_filename = ui_filename self.filename = filename def format_message(self): - return f"Could not open file {self.ui_filename}: {self.message}" + return _("Could not open file {filename!r}: {message}").format( + filename=self.ui_filename, message=self.message + ) class Abort(RuntimeError): diff --git a/src/click/formatting.py b/src/click/formatting.py index 9cb5a19485..72e25c9765 100644 --- a/src/click/formatting.py +++ b/src/click/formatting.py @@ -1,5 +1,6 @@ import typing as t from contextlib import contextmanager +from gettext import gettext as _ from ._compat import term_len from .parser import split_opt @@ -129,13 +130,17 @@ def dedent(self): """Decreases the indentation.""" self.current_indent -= self.indent_increment - def write_usage(self, prog, args="", prefix="Usage: "): + def write_usage(self, prog, args="", prefix=None): """Writes a usage line into the buffer. :param prog: the program name. :param args: whitespace separated list of arguments. - :param prefix: the prefix for the first line. + :param prefix: The prefix for the first line. Defaults to + ``"Usage: "``. """ + if prefix is None: + prefix = f"{_('Usage:')} " + usage_prefix = f"{prefix:>{self.current_indent}}{prog} " text_width = self.width - self.current_indent diff --git a/src/click/parser.py b/src/click/parser.py index d730e01066..4eede0c1a6 100644 --- a/src/click/parser.py +++ b/src/click/parser.py @@ -22,6 +22,8 @@ # Copyright 2001-2006 Gregory P. Ward # Copyright 2002-2006 Python Software Foundation from collections import deque +from gettext import gettext as _ +from gettext import ngettext from .exceptions import BadArgumentUsage from .exceptions import BadOptionUsage @@ -194,7 +196,9 @@ def process(self, value, state): value = None elif holes != 0: raise BadArgumentUsage( - f"argument {self.dest} takes {self.nargs} values" + _("Argument {name!r} takes {nargs} values.").format( + name=self.dest, nargs=self.nargs + ) ) if self.nargs == -1 and self.obj.envvar is not None: @@ -359,7 +363,9 @@ def _match_long_opt(self, opt, explicit_value, state): value = self._get_value_from_state(opt, option, state) elif explicit_value is not None: - raise BadOptionUsage(opt, f"{opt} option does not take a value") + raise BadOptionUsage( + opt, _("Option {name!r} does not take a value.").format(name=opt) + ) else: value = None @@ -414,9 +420,13 @@ def _get_value_from_state(self, option_name, option, state): # Option allows omitting the value. value = _flag_needs_value else: - n_str = "an argument" if nargs == 1 else f"{nargs} arguments" raise BadOptionUsage( - option_name, f"{option_name} option requires {n_str}." + option_name, + ngettext( + "Option {name!r} requires an argument.", + "Option {name!r} requires {nargs} arguments.", + nargs, + ).format(name=option_name, nargs=nargs), ) elif nargs == 1: next_rarg = state.rargs[0] diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py index d87e0c84de..ae498ac246 100644 --- a/src/click/shell_completion.py +++ b/src/click/shell_completion.py @@ -1,6 +1,7 @@ import os import re import typing as t +from gettext import gettext as _ from .core import Argument from .core import MultiCommand @@ -289,12 +290,14 @@ def _check_version(self): if major < "4" or major == "4" and minor < "4": raise RuntimeError( - "Shell completion is not supported for Bash" - " versions older than 4.4." + _( + "Shell completion is not supported for Bash" + " versions older than 4.4." + ) ) else: raise RuntimeError( - "Couldn't detect Bash version, shell completion is not supported." + _("Couldn't detect Bash version, shell completion is not supported.") ) def source(self): diff --git a/src/click/termui.py b/src/click/termui.py index 72b03f818c..3a7e0850e8 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -162,7 +162,7 @@ def prompt_func(text): result = value_proc(value) except UsageError as e: if hide_input: - echo(_("Error: the value you entered was invalid"), err=err) + echo(_("Error: The value you entered was invalid."), err=err) else: echo(_("Error: {e.message}").format(e=e), err=err) # noqa: B306 continue @@ -174,7 +174,7 @@ def prompt_func(text): break if value == value2: return result - echo(_("Error: the two entered values do not match"), err=err) + echo(_("Error: The two entered values do not match."), err=err) def confirm( @@ -732,7 +732,7 @@ def raw_terminal(): return f() -def pause(info="Press any key to continue ...", err=False): +def pause(info=None, err=False): """This command stops execution and waits for the user to press any key to continue. This is similar to the Windows batch "pause" command. If the program is not run through a terminal, this command @@ -743,12 +743,17 @@ def pause(info="Press any key to continue ...", err=False): .. versionadded:: 4.0 Added the `err` parameter. - :param info: the info string to print before pausing. + :param info: The message to print before pausing. Defaults to + ``"Press any key to continue..."``. :param err: if set to message goes to ``stderr`` instead of ``stdout``, the same as with echo. """ if not isatty(sys.stdin) or not isatty(sys.stdout): return + + if info is None: + info = _("Press any key to continue...") + try: if info: echo(info, nl=False, err=err) diff --git a/src/click/types.py b/src/click/types.py index 6cf611fa7e..543833a244 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -2,6 +2,8 @@ import stat import typing as t from datetime import datetime +from gettext import gettext as _ +from gettext import ngettext from ._compat import _get_argv_encoding from ._compat import filename_to_ui @@ -228,8 +230,7 @@ def get_metavar(self, param): return f"[{choices_str}]" def get_missing_message(self, param): - choice_str = ",\n\t".join(self.choices) - return f"Choose from:\n\t{choice_str}" + return _("Choose from:\n\t{choices}").format(choices=",\n\t".join(self.choices)) def convert(self, value, param, ctx): # Match through normalization and case sensitivity @@ -256,9 +257,16 @@ def convert(self, value, param, ctx): if normed_value in normed_choices: return normed_choices[normed_value] - one_of = "one of " if len(self.choices) > 1 else "" - choices_str = ", ".join(repr(c) for c in self.choices) - self.fail(f"{value!r} is not {one_of}{choices_str}.", param, ctx) + choices_str = ", ".join(map(repr, self.choices)) + self.fail( + ngettext( + "{value!r} is not {choice}.", + "{value!r} is not one of {choices}.", + len(self.choices), + ).format(value=value, choice=choices_str, choices=choices_str), + param, + ctx, + ) def __repr__(self): return f"Choice({list(self.choices)})" @@ -335,10 +343,15 @@ def convert(self, value, param, ctx): if converted is not None: return converted - plural = "s" if len(self.formats) > 1 else "" - formats_str = ", ".join(repr(f) for f in self.formats) + formats_str = ", ".join(map(repr, self.formats)) self.fail( - f"{value!r} does not match the format{plural} {formats_str}.", param, ctx + ngettext( + "{value!r} does not match the format {format}.", + "{value!r} does not match the formats {formats}.", + len(self.formats), + ).format(value=value, format=formats_str, formats=formats_str), + param, + ctx, ) def __repr__(self): @@ -352,7 +365,13 @@ def convert(self, value, param, ctx): try: return self._number_class(value) except ValueError: - self.fail(f"{value!r} is not a valid {self.name}.", param, ctx) + self.fail( + _("{value!r} is not a valid {number_type}.").format( + value=value, number_type=self.name + ), + param, + ctx, + ) class _NumberRangeBase(_NumberParamTypeBase): @@ -393,7 +412,13 @@ def convert(self, value, param, ctx): return self._clamp(self.max, -1, self.max_open) if lt_min or gt_max: - self.fail(f"{rv} is not in the range {self._describe_range()}.", param, ctx) + self.fail( + _("{value} is not in the range {range}.").format( + value=rv, range=self._describe_range() + ), + param, + ctx, + ) return rv @@ -517,7 +542,9 @@ def convert(self, value, param, ctx): if norm in {"0", "false", "f", "no", "n", "off"}: return False - self.fail(f"{value!r} is not a valid boolean.", param, ctx) + self.fail( + _("{value!r} is not a valid boolean.").format(value=value), param, ctx + ) def __repr__(self): return "BOOL" @@ -537,7 +564,9 @@ def convert(self, value, param, ctx): try: return uuid.UUID(value) except ValueError: - self.fail(f"{value!r} is not a valid UUID.", param, ctx) + self.fail( + _("{value!r} is not a valid UUID.").format(value=value), param, ctx + ) def __repr__(self): return "UUID" @@ -698,11 +727,11 @@ def __init__( self.type = path_type if self.file_okay and not self.dir_okay: - self.name = "file" + self.name = _("file") elif self.dir_okay and not self.file_okay: - self.name = "directory" + self.name = _("directory") else: - self.name = "path" + self.name = _("path") def to_info_dict(self): info_dict = super().to_info_dict() @@ -746,32 +775,42 @@ def convert(self, value, param, ctx): if not self.exists: return self.coerce_path_result(rv) self.fail( - f"{self.name.title()} {filename_to_ui(value)!r} does not exist.", + _("{name} {filename!r} does not exist.").format( + name=self.name.title(), filename=filename_to_ui(value) + ), param, ctx, ) if not self.file_okay and stat.S_ISREG(st.st_mode): self.fail( - f"{self.name.title()} {filename_to_ui(value)!r} is a file.", + _("{name} {filename!r} is a file.").format( + name=self.name.title(), filename=filename_to_ui(value) + ), param, ctx, ) if not self.dir_okay and stat.S_ISDIR(st.st_mode): self.fail( - f"{self.name.title()} {filename_to_ui(value)!r} is a directory.", + _("{name} {filename!r} is a directory.").format( + name=self.name.title(), filename=filename_to_ui(value) + ), param, ctx, ) if self.writable and not os.access(value, os.W_OK): self.fail( - f"{self.name.title()} {filename_to_ui(value)!r} is not writable.", + _("{name} {filename!r} is not writable.").format( + name=self.name.title(), filename=filename_to_ui(value) + ), param, ctx, ) if self.readable and not os.access(value, os.R_OK): self.fail( - f"{self.name.title()} {filename_to_ui(value)!r} is not readable.", + _("{name} {filename!r} is not readable.").format( + name=self.name.title(), filename=filename_to_ui(value) + ), param, ctx, ) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 607a636fed..e1a6de90f5 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -230,7 +230,7 @@ def test_missing_argument_string_cast(): with pytest.raises(click.MissingParameter) as excinfo: click.Argument(["a"], required=True).process_value(ctx, None) - assert str(excinfo.value) == "missing parameter: a" + assert str(excinfo.value) == "Missing parameter: a" def test_implicit_non_required(runner): @@ -301,7 +301,7 @@ def cmd(a): result = runner.invoke(cmd, ["3"]) assert result.exception is not None - assert "argument a takes 2 values" in result.output + assert "Argument 'a' takes 2 values." in result.output def test_multiple_param_decls_not_allowed(runner): diff --git a/tests/test_basic.py b/tests/test_basic.py index 47356277b6..c35e69ad1a 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -120,7 +120,7 @@ def cli(foo): result = runner.invoke(cli, ["--foo"]) assert result.exception - assert "--foo option requires an argument" in result.output + assert "Option '--foo' requires an argument." in result.output result = runner.invoke(cli, ["--foo="]) assert not result.exception diff --git a/tests/test_options.py b/tests/test_options.py index 65109d69a4..009f80e2a1 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -83,7 +83,7 @@ def cli(): result = runner.invoke(cli, [unknown_flag]) assert result.exception - assert f"no such option: {unknown_flag}" in result.output + assert f"No such option: {unknown_flag}" in result.output @pytest.mark.parametrize( @@ -441,7 +441,7 @@ def test_missing_option_string_cast(): with pytest.raises(click.MissingParameter) as excinfo: click.Option(["-a"], required=True).process_value(ctx, None) - assert str(excinfo.value) == "missing parameter: a" + assert str(excinfo.value) == "Missing parameter: a" def test_missing_choice(runner): diff --git a/tests/test_termui.py b/tests/test_termui.py index 05e3bf8fb0..ab459f052f 100644 --- a/tests/test_termui.py +++ b/tests/test_termui.py @@ -376,7 +376,7 @@ def test_fast_edit(runner): ("prompt_required", "required", "args", "expect"), [ (True, False, None, "prompt"), - (True, False, ["-v"], "-v option requires an argument"), + (True, False, ["-v"], "Option '-v' requires an argument."), (False, True, None, "prompt"), (False, True, ["-v"], "prompt"), ],