Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ Unreleased
:issue:`1791`
- When taking arguments from ``sys.argv`` on Windows, glob patterns,
user dir, and env vars are expanded. :issue:`1096`
- Marked messages shown by the CLI with ``gettext()`` to allow
applications to translate Click's built-in strings. :issue:`303`


Version 7.1.2
Expand Down
9 changes: 7 additions & 2 deletions src/click/_termui_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
67 changes: 39 additions & 28 deletions src/click/_unicodefun.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import codecs
import os
from gettext import gettext as _


def _verify_python_env():
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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))
46 changes: 27 additions & 19 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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 ._unicodefun import _verify_python_env
Expand Down Expand Up @@ -995,7 +997,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):
Expand Down Expand Up @@ -1170,7 +1172,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):
Expand Down Expand Up @@ -1199,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()

Expand All @@ -1225,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()
Expand All @@ -1242,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):
Expand All @@ -1265,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
Expand All @@ -1280,7 +1284,9 @@ def invoke(self, ctx):
if self.deprecated:
echo(
style(
f"DeprecationWarning: The command {self.name!r} is deprecated.",
_("DeprecationWarning: The command {name!r} is deprecated.").format(
name=self.name
),
fg="red",
),
err=True,
Expand Down Expand Up @@ -1474,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):
Expand Down Expand Up @@ -1506,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
Expand Down Expand Up @@ -1580,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):
Expand Down Expand Up @@ -2058,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:
Expand Down Expand Up @@ -2421,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)
Expand All @@ -2434,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.
Expand All @@ -2444,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()
Expand All @@ -2453,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}]"
Expand Down
13 changes: 9 additions & 4 deletions src/click/decorators.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -304,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.

Expand All @@ -315,6 +317,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
Expand Down Expand Up @@ -381,7 +386,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)

Expand Down Expand Up @@ -412,6 +417,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)
Loading