Repository: pallets/click

Problem statement:
Provide i18n with gettext
It would be useful to wrap every public message with gettext function to allow i18n for application.



Hints:
 + 1 

+1

For those interrested, I've made a gist showing how to catch click exceptions and translate them.
It's **quick and dirty** (use regexps), but right now it does the job:

https://gist.github.com/lapause/ed148b2b1481f83f39c5

To core developers: if you don't want to implement i18n, you could create one exception class by error type (±3 inheriting from UsageError, ±14 from BadParameter) and use attributes to store dynamic elements (parameter and file names/types.... By doing so it would allow developers to just trap exceptions of interrest to them and re-raise them with customized messages without having to mess with regexps depending of the exact messages used by the library.

To core developers (2): I'm pretty sure my monkey patching of the click.Command.main is not the best way to go, but http://click.pocoo.org/4/exceptions/ could really benefit from a real life example of how to invoke directly a command declared with multiple arguments, decorators...

1.  Agreed, I'd like to see this too. However, I suppose this would also require a much more fine-grained exception hierarchy, one class for each error message. Right now some parts of click raise such generic ones as `RuntimeError`.
2.  I think you're supposed to subclass `Command` so you can override `main` properly, and then pass the subclass like `@click.command(cls=TranslatingCommand)`.

+1

I'm playing around with click in Mailman 3 to replace our homegrown cli parsing code.  All of our help text is marked with `_()` and we use [flufl.i18n](http://flufli18n.readthedocs.io/en/latest/) to do the actual run time translations.  I may have more opinions as I get farther along, but for now I don't see any real problem with option help text.  E.g. this works fine:

```python
@click.option(
    '-v', '--verbose',
    is_flag=True, default=False,
    help=_('Display more debugging information to the log file.'))
```

It gets tricky in one place: use of the function's docstring for the help prologue.  Python only assigns unadorned strings to `__doc__`.  So there's no good way to mark the help prologue for translation, extraction and also use it in click.

Of course, I might be missing a lot as I've just started digging in. ;)
Should this be implemented in click itself or as part of click-contrib?

Also, `command()` and `group()` have `help` parameters so you could do
```python
@click.command(cls=TranslatingCommand, help=_('translatable string'))
def func():
    # No docstring here.
    pass
```
This issue appears to be asking for i18n on Click's internal messages, such as exception strings. There should be nothing preventing use of i18n on your own strings.
What I meant was should this be built into click or should there be a separate library?
What you're showing isn't related to what's being asked for. Nothing has to change for what you're showing. Internal message have to be wrapped for what's being asked for.
I'm asking if translating the strings should be built in.
So I think what needs to be done is to wrap messages that the user might see with `_` (`gettext`) and leave it at that for now.
I'd like to work on this.

Patch:
diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py
--- 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
--- 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
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -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
@@ -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):
@@ -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):
@@ -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()
 
@@ -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()
@@ -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):
@@ -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
@@ -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,
@@ -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):
@@ -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
@@ -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):
@@ -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:
@@ -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)
@@ -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.
@@ -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()
@@ -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}]"
diff --git a/src/click/decorators.py b/src/click/decorators.py
--- 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
@@ -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.
 
@@ -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
@@ -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)
 
@@ -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)
diff --git a/src/click/exceptions.py b/src/click/exceptions.py
--- a/src/click/exceptions.py
+++ b/src/click/exceptions.py
@@ -1,3 +1,6 @@
+from gettext import gettext as _
+from gettext import ngettext
+
 from ._compat import filename_to_ui
 from ._compat import get_text_stderr
 from .utils import echo
@@ -28,7 +31,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: {message}").format(message=self.format_message()), file=file)
 
 
 class UsageError(ClickException):
@@ -53,14 +56,18 @@ 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(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color)
-        echo(f"Error: {self.format_message()}", file=file, color=color)
+        echo(
+            _("Error: {message}").format(message=self.format_message()),
+            file=file,
+            color=color,
+        )
 
 
 class BadParameter(UsageError):
@@ -92,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):
@@ -123,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:
@@ -134,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 f"missing parameter: {param_name}"
+            return _("Missing parameter: {param_name}").format(param_name=param_name)
         else:
             return self.message
 
@@ -158,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):
@@ -205,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
--- 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
--- 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
--- 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
--- 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:
@@ -731,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
@@ -742,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
--- 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,
                 )


Test patch:
diff --git a/tests/test_arguments.py b/tests/test_arguments.py
--- 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
--- 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_imports.py b/tests/test_imports.py
--- 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:
diff --git a/tests/test_options.py b/tests/test_options.py
--- 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
--- 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"),
     ],