Extract translatable log messages into separate module
Marco Ricci

Marco Ricci commited on 2024-12-25 21:41:22
Zeige 3 geänderte Dateien mit 801 Einfügungen und 0 Löschungen.


Extract all log messages into a separate module, so that they may be
translated in a future version.  We anticipate the use of such a message
in a `logging` call, so we provide a wrapper object that defers the
string interpolation until its serialization is called, and caches the
result.  (This is all as per the recommendation from the standard
`logging` module.)  Implemented this way, it is easy to run easy
transformations on the specific log message template, e.g. substituting
a translated message template, or trimming the filename placeholder if
the filename is `None`.

As an added benefit, this organization also makes it somewhat easy to
ensure message consistency across different use sites and to generate
diagnostics lists for inclusion in the manpage.

While we intend to use the gettext toolset to implement the translation
later, we do *not* currently use the standard `_` gettext alias or
otherwise mark up the strings in an `xgettext`-compatible manner.  Both
the GNU version of `xgettext` and the Python work-alike `xgettext.py`
suck: the GNU version does not completely understand Python syntax and
cannot sensibly parse `black`-/`ruff`-formatted code using implicit
string concatenation or trailing commas in function argument lists, and
`xgettext.py` is so outdated (even in current Pythons) that it does not
support `xgettext`'s `--add-comment` option for extracting extra
comments for the translators.  So instead of attempting to cram the
strings and comments into some formatter-resistant and
`xgettext`-compatible shape (a very futile endeavor so far), store
enough information that we could generate the `.po` files ourselves with
very little additional effort.
... ...
@@ -601,6 +601,189 @@ key or to type in a master passphrase.
601 601
 .Sh DIAGNOSTICS
602 602
 .
603 603
 .Ex -std "derivepassphrase vault"
604
+.Pp
605
+.
606
+.Ss Fatal error messages on standard error
607
+.
608
+.Pq Li %s Ns No " indicates a variable part of the message."
609
+.
610
+.Bl -diag
611
+.
612
+.It %s is mutually exclusive with %s.
613
+The two indicated options must not be used at the same time.
614
+.
615
+.It %s requires a SERVICE or \-\-config.
616
+Using the indicated passphrase generation option requires the
617
+.Ar SERVICE
618
+argument or the
619
+.Fl \-config
620
+option.
621
+.
622
+.It %s requires a SERVICE.
623
+Using the indicated option requires the
624
+.Ar SERVICE
625
+argument.
626
+.
627
+.It %s does not take a SERVICE argument.
628
+The indicated option must not be specified together with the
629
+.Ar SERVICE
630
+argument.
631
+.
632
+.It Cannot load vault settings: %s.
633
+There was a fatal problem loading the stored vault configuration data.
634
+Further details are contained in the variable part of the message.
635
+.
636
+.It Cannot store vault settings: %s.
637
+There was a fatal problem saving the vault configuration data.
638
+Further details are contained in the variable part of the message.
639
+.
640
+.It Cannot import vault settings: %s.
641
+There was a fatal problem loading the imported vault configuration data.
642
+Further details are contained in the variable part of the message.
643
+.
644
+.It Cannot export vault settings: %s.
645
+There was a fatal problem saving the exported vault configuration data.
646
+Further details are contained in the variable part of the message.
647
+.
648
+.It Cannot load user config: %s.
649
+There was a fatal problem loading the central user configuration file.
650
+Further details are contained in the variable part of the message.
651
+.
652
+.It The user configuration file is invalid.
653
+(Exactly what it says.)
654
+.
655
+.It No usable SSH keys were found
656
+The running SSH agent does not contain any suitable SSH keys.
657
+.
658
+.It No valid SSH key selected
659
+We requested that an SSH key be selected, but we got an invalid selection.
660
+.
661
+.It The requested SSH key is not loaded into the agent.
662
+The running SSH agent does not contain the necessary SSH key.
663
+.
664
+.It Cannot find any running SSH agent because SSH_AUTH_SOCK is not set.
665
+We require a running SSH agent, but cannot locate its communication channel,
666
+which is normally indicated by the
667
+.Ev SSH_AUTH_SOCK
668
+environment variable.
669
+.
670
+.It Cannot connect to an SSH agent because this Python version does not support UNIX domain sockets.
671
+This Python installation does not support the communication mechanism
672
+necessary to talk to SSH agents.
673
+.
674
+.It Cannot connect to the SSH agent: %s.
675
+We cannot connect to the SSH agent indicated by the
676
+.Ev SSH_AUTH_SOCK
677
+environment variable.
678
+Further details are contained in the variable part of the message.
679
+.
680
+.It The SSH agent failed to complete the request
681
+The SSH agent \(em while responsive in principle \(em failed to or refused
682
+to supply a list of loaded keys.
683
+.
684
+.It Error communicating with the SSH agent
685
+There was a system error communicating with the SSH agent.
686
+.
687
+.It Not saving any new notes: the user aborted the request.
688
+(Exactly what it says.)
689
+.
690
+.It Cannot update %s settings without actual settings.
691
+Using
692
+.Fl \-config
693
+requires at least one of the
694
+.Fl \-phrase , \-key , \-length , No etc.\&
695
+options.
696
+.
697
+.It Attempted to unset and set %s at the same time.
698
+While handling
699
+.Fl \-config ,
700
+the same configuration setting was passed as an option and as an argument to
701
+.Fl \-unset .
702
+.
703
+.It Generating a passphrase requires a SERVICE.
704
+(Exactly what it says.)
705
+.
706
+.It No passphrase or key was given in the configuration.
707
+.Nm derivepassphrase vault
708
+does not know whether to use a master SSH key or a master passphrase.
709
+.
710
+.It No passphrase was given: the user aborted the request.
711
+(Exactly what it says.)
712
+.
713
+.It No SSH key was selected: the user aborted the request.
714
+(Exactly what it says.)
715
+.
716
+.El
717
+.Pp
718
+.
719
+.Ss Non-fatal warning and info messages on standard error
720
+.
721
+.Pq Li %s Ns No " indicates a variable part of the message."
722
+.
723
+.Bl -diag
724
+.
725
+.It The %s passphrase is not %s-normalized.
726
+The indicated passphrase \(em as a Unicode string \(em is not properly
727
+normalized according to the preferred Unicode normalization form
728
+.Pq as specified in the central configuration file .
729
+It is therefore possible that the passphrase \(em as a byte string \(em is
730
+not the same byte string as you expect it to be
731
+.Pq even though it Em looks No correct ,
732
+and that the derived passphrases thus do not match their expected values
733
+either.
734
+Please double-check.
735
+.
736
+.It An empty SERVICE is not supported by vault(1).
737
+.Xr vault 1
738
+does not support the empty string as a value for
739
+.Ar SERVICE ;
740
+it will treat the
741
+.Ar SERVICE
742
+as missing.
743
+For compatibility,
744
+.Nm derivepassphrase vault
745
+will do the same.
746
+In particular, if the empty service is imported in a configuration via
747
+.Fl \-import ,
748
+then this service cannot be accessed via the
749
+.Nm derivepassphrase vault
750
+command-line.
751
+.
752
+.It Replacing invalid value %s for key %s with %s.
753
+When importing a configuration, the indicated invalid value has been
754
+replaced with the indicated replacement value.
755
+.Pq The Do interpretation Dc of the configuration doesn't change .
756
+.
757
+.It Removing ineffective setting %s = %s.
758
+When importing a configuration, the indicated ineffective setting has been
759
+removed.
760
+.Pq The Do interpretation Dc of the configuration doesn't change .
761
+.
762
+.It Setting a %s passphrase is ineffective because a key is also set.
763
+The configuration (global or key-specific) contains both a stored master
764
+passphrase and an SSH key.
765
+The master passphrase will not take effect.
766
+.
767
+.It A subcommand will be required in v1.0.
768
+.Bo
769
+Since v0.2.0, until v1.0.
770
+.Bc
771
+This command now requires a subcommand.
772
+For compatibility, it currently defaults to
773
+.Dq vault .
774
+.
775
+.It Using deprecated v0.1-style config file %s, instead of v0.2-style %s.
776
+.Bo
777
+Since v0.2.0, until v1.0.
778
+.Bc
779
+A configuration file has been renamed.
780
+.Nm derivepassphrase vault
781
+will attempt to rename the file itself
782
+.Pq Qq Li Successfully migrated to %s. ,
783
+or complain if it cannot rename it
784
+.Pq Qq Li Failed to migrate to %s: %s .
785
+.
786
+.El
604 787
 .
605 788
 .Sh COMPATIBILITY
606 789
 .
... ...
@@ -240,6 +240,159 @@ This is a property specific to the key type, and sometimes the agent used:
240 240
 
241 241
 The derivepassphrase vault utility exits 0 on success, and >0 if an error occurs.
242 242
 
243
+### Fatal error messsages on standard error
244
+
245
+(`%s` indicates a variable part of the message.)
246
+
247
+??? failure "`%s is mutually exclusive with %s.`"
248
+
249
+    The two indicated options must not be used at the same time.
250
+
251
+??? failure "`%s requires a SERVICE or --config.`"
252
+
253
+    Using the indicated passphrase generation option requires the <var>SERVICE</var> argument or the `--config` option.
254
+
255
+??? failure "`%s requires a SERVICE.`"
256
+
257
+    Using the indicated option requires the <var>SERVICE</var> argument.
258
+
259
+??? failure "`%s does not take a SERVICE argument.`"
260
+
261
+    The indicated option must not be specified together with the <var>SERVICE</var> argument.
262
+
263
+??? failure "`Cannot load vault settings: %s.`"
264
+
265
+    There was a fatal problem loading the stored vault configuration data.
266
+    Further details are contained in the variable part of the message.
267
+
268
+??? failure "`Cannot store vault settings: %s.`"
269
+
270
+    There was a fatal problem saving the vault configuration data.
271
+    Further details are contained in the variable part of the message.
272
+
273
+??? failure "`Cannot import vault settings: %s.`"
274
+
275
+    There was a fatal problem loading the imported vault configuration data.
276
+    Further details are contained in the variable part of the message.
277
+
278
+??? failure "`Cannot export vault settings: %s.`"
279
+
280
+    There was a fatal problem saving the exported vault configuration data.
281
+    Further details are contained in the variable part of the message.
282
+
283
+??? failure "`Cannot load user config: %s.`"
284
+
285
+    There was a fatal problem loading the central user configuration file.
286
+    Further details are contained in the variable part of the message.
287
+
288
+??? failure "`The user configuration file is invalid.`"
289
+
290
+    (Exactly what it says.)
291
+
292
+??? failure "`No usable SSH keys were found`"
293
+
294
+    The running SSH agent does not contain any suitable SSH keys.
295
+
296
+??? failure "`No valid SSH key selected`"
297
+
298
+    We requested that an SSH key be selected, but we got an invalid selection.
299
+
300
+??? failure "`The requested SSH key is not loaded into the agent.`"
301
+
302
+    The running SSH agent does not contain the necessary SSH key.
303
+
304
+??? failure "`Cannot find any running SSH agent because SSH_AUTH_SOCK is not set.`"
305
+
306
+    We require a running SSH agent, but cannot locate its communication channel, which is normally indicated by the `SSH_AUTH_SOCK` environment variable.
307
+
308
+??? failure "`Cannot connect to an SSH agent because this Python version does not support UNIX domain sockets.`"
309
+
310
+    This Python installation does not support the communication mechanism necessary to talk to SSH agents.
311
+
312
+??? failure "`Cannot connect to the SSH agent: %s.`"
313
+
314
+    We cannot connect to the SSH agent indicated by the `SSH_AUTH_SOCK` environment variable.
315
+    Further details are contained in the variable part of the message.
316
+
317
+??? failure "`The SSH agent failed to complete the request`"
318
+
319
+    The SSH agent---while responsive in principle---failed to or refused to supply a list of loaded keys.
320
+
321
+??? failure "`Error communicating with the SSH agent`"
322
+
323
+    There was a system error communicating with the SSH agent.
324
+
325
+??? failure "`Not saving any new notes: the user aborted the request.`"
326
+
327
+    (Exactly what it says.)
328
+
329
+??? failure "`Cannot update %s settings without actual settings.`"
330
+
331
+    Using `--config` requires at least one of the `--phrase`, `--key`, `--length`, etc. options.
332
+
333
+??? failure "`Attempted to unset and set %s at the same time.`"
334
+
335
+    While handling `--config`, the same configuration setting was passed as an option and as an argument to `--unset`.
336
+
337
+??? failure "`Generating a passphrase requires a SERVICE.`"
338
+
339
+    (Exactly what it says.)
340
+
341
+??? failure "`No passphrase or key was given in the configuration.`"
342
+
343
+    <b>derivepassphrase vault</b> does not know whether to use a master SSH key or a master passphrase.
344
+
345
+??? failure "`No passphrase was given: the user aborted the request.`"
346
+
347
+    (Exactly what it says.)
348
+
349
+??? failure "`No SSH key was selected: the user aborted the request.`"
350
+
351
+    (Exactly what it says.)
352
+
353
+### Non-fatal warning and info messages on standard error
354
+
355
+(`%s` indicates a variable part of the message.)
356
+
357
+??? warning "`The %s passphrase is not %s-normalized.`"
358
+
359
+    The indicated passphrase---as a Unicode string---is not properly normalized according to the preferred Unicode normalization form (as specified in the central configuration file).
360
+    It is therefore possible that the passphrase---as a byte string---is not the same byte string as you expect it to be (even though it *looks* correct), and that the derived passphrases thus do not match their expected values either.
361
+    Please double-check.
362
+
363
+??? warning "`An empty SERVICE is not supported by vault(1).`"
364
+
365
+    <i>vault</i>(1) does not support the empty string as a value for <var>SERVICE</var>; it will treat the <var>SERVICE</var> as missing.
366
+    For compatibility, <b>derivepassphrase vault</b> will do the same.
367
+    In particular, if the empty service is imported in a configuration via `--import`, then this service cannot be accessed via the <b>derivepassphrase vault</b> command-line.
368
+
369
+??? warning "`Replacing invalid value %s for key %s with %s.`"
370
+
371
+    When importing a configuration, the indicated invalid value has been replaced with the indicated replacement value.
372
+    (The "interpretation" of the configuration doesn’t change).
373
+
374
+??? warning "`Removing ineffective setting %s = %s.`"
375
+
376
+    When importing a configuration, the indicated ineffective setting has been removed.
377
+    (The "interpretation" of the configuration doesn’t change).
378
+
379
+??? warning "`Setting a %s passphrase is ineffective because a key is also set.`"
380
+
381
+    The configuration (global or key-specific) contains both a stored master passphrase and an SSH key.
382
+    The master passphrase will not take effect.
383
+
384
+??? warning "`A subcommand will be required in v1.0.`"
385
+
386
+    [Since v0.2.0, until v1.0.]
387
+    This command now requires a subcommand.
388
+    For compatibility, it currently defaults to "vault".
389
+
390
+??? warning "`Using deprecated v0.1-style config file %s, instead of v0.2-style %s.`"
391
+
392
+    [Since v0.2.0, until v1.0.]
393
+    A configuration file has been renamed.
394
+    <b>derivepassphrase vault</b> will attempt to rename the file itself (`Successfully migrated to %s.`), or complain if it cannot rename it (`Failed to migrate to %s: %s`).
395
+
243 396
 ## COMPATIBILITY
244 397
 
245 398
 ### With other software
... ...
@@ -0,0 +1,465 @@
1
+# SPDX-FileCopyrightText: 2024 Marco Ricci <software@the13thletter.info>
2
+#
3
+# SPDX-Licence-Identifier: MIT
4
+
5
+"""Internal module.  Do not use.  Contains error strings and functions."""
6
+
7
+from __future__ import annotations
8
+
9
+import enum
10
+import gettext
11
+import inspect
12
+import types
13
+from typing import TYPE_CHECKING, NamedTuple
14
+
15
+import derivepassphrase as dpp
16
+
17
+if TYPE_CHECKING:
18
+    from collections.abc import Iterable, Mapping
19
+
20
+    from typing_extensions import Any, Self
21
+
22
+__author__ = dpp.__author__
23
+__version__ = dpp.__version__
24
+
25
+__all__ = ('PROG_NAME',)
26
+
27
+PROG_NAME = 'derivepassphrase'
28
+translation = gettext.translation(PROG_NAME, fallback=True)
29
+
30
+
31
+class TranslatableString(NamedTuple):
32
+    singular: str
33
+    plural: str
34
+    l10n_context: str
35
+    translator_comments: str
36
+    flags: frozenset[str]
37
+
38
+
39
+def _prepare_translatable(
40
+    msg: str,
41
+    comments: str = '',
42
+    context: str = '',
43
+    plural_msg: str = '',
44
+    *,
45
+    flags: Iterable[str] = (),
46
+) -> TranslatableString:
47
+    msg = inspect.cleandoc(msg)
48
+    plural_msg = inspect.cleandoc(plural_msg)
49
+    context = context.strip()
50
+    comments = inspect.cleandoc(comments)
51
+    flags = (
52
+        frozenset(f.strip() for f in flags)
53
+        if not isinstance(flags, str)
54
+        else frozenset({flags})
55
+    )
56
+    assert (
57
+        '{' not in msg
58
+        or bool(flags & {'python-brace-format', 'no-python-brace-format'})
59
+    ), f'Missing flag for how to deal with brace in {msg!r}'
60
+    assert (
61
+        '%' not in msg
62
+        or bool(flags & {'python-format', 'no-python-format'})
63
+    ), f'Missing flag for how to deal with percent character in {msg!r}'
64
+    return TranslatableString(msg, plural_msg, context, comments, flags)
65
+
66
+
67
+class LogObject:
68
+
69
+    def __init__(
70
+        self,
71
+        template: TranslatableString,
72
+        args_dict: Mapping[str, Any] = types.MappingProxyType({}),
73
+        /,
74
+        **kwargs: Any,  # noqa: ANN401
75
+    ) -> None:
76
+        self.template = template
77
+        self.kwargs = {**args_dict, **kwargs}
78
+        self._rendered: str | None = None
79
+
80
+    def __str__(self) -> str:
81
+        if self._rendered is None:
82
+            context = self.template.l10n_context
83
+            template = self.template.singular
84
+            if context is not None:
85
+                template = translation.pgettext(context, template)
86
+            else:
87
+                template = translation.gettext(template)
88
+            self._rendered = template.format(**self.kwargs)
89
+        return self._rendered
90
+
91
+    def maybe_without_filename(self) -> Self:
92
+        if (
93
+            self.kwargs.get('filename') is None
94
+            and ': {filename!r}' in self.template.singular
95
+        ):
96
+            singular = ''.join(
97
+                self.template.singular.split(': {filename!r}', 1)
98
+            )
99
+            plural = (
100
+                ''.join(self.template.plural.split(': {filename!r}', 1))
101
+                if self.template.plural
102
+                else self.template.plural
103
+            )
104
+            return self.__class__(
105
+                self.template._replace(singular=singular, plural=plural),
106
+                self.kwargs,
107
+            )
108
+        return self
109
+
110
+    @classmethod
111
+    def InfoMsg(  # noqa: N802
112
+        cls,
113
+        msg_template: InfoMsgTemplate,
114
+        args_dict: Mapping[str, Any] = types.MappingProxyType({}),
115
+        /,
116
+        **kwargs: Any,  # noqa: ANN401
117
+    ) -> Self:
118
+        return cls(msg_template.value, {**args_dict, **kwargs})
119
+
120
+    @classmethod
121
+    def WarnMsg(  # noqa: N802
122
+        cls,
123
+        msg_template: WarnMsgTemplate,
124
+        args_dict: Mapping[str, Any] = types.MappingProxyType({}),
125
+        /,
126
+        **kwargs: Any,  # noqa: ANN401
127
+    ) -> Self:
128
+        return cls(msg_template.value, {**args_dict, **kwargs})
129
+
130
+    @classmethod
131
+    def ErrMsg(  # noqa: N802
132
+        cls,
133
+        msg_template: ErrMsgTemplate,
134
+        args_dict: Mapping[str, Any] = types.MappingProxyType({}),
135
+        /,
136
+        **kwargs: Any,  # noqa: ANN401
137
+    ) -> Self:
138
+        return cls(msg_template.value, {**args_dict, **kwargs})
139
+
140
+
141
+class InfoMsgTemplate(enum.Enum):
142
+    CANNOT_LOAD_AS_VAULT_CONFIG = _prepare_translatable(
143
+        comments=r"""
144
+        TRANSLATORS: "fmt" is a string such as "v0.2" or "storeroom",
145
+        indicating the format which we tried to load the vault
146
+        configuration as.
147
+        """,
148
+        msg='Cannot load {path!r} as a {fmt!s} vault configuration.',
149
+        context='info message',
150
+        flags='python-brace-format',
151
+    )
152
+    PIP_INSTALL_EXTRA = _prepare_translatable(
153
+        comments=r"""
154
+        TRANSLATORS: This message immediately follows an error message
155
+        about a missing library that needs to be installed.  The Python
156
+        Package Index (PyPI) supports declaring sets of optional
157
+        dependencies as "extras", so users installing from PyPI can
158
+        request reinstallation with a named "extra" being enabled.  This
159
+        would then let the installer take care of the missing libraries
160
+        automatically, hence this suggestion to PyPI users.
161
+        """,
162
+        msg='(For users installing from PyPI, see the {extra_name!r} extra.)',
163
+        context='info message',
164
+        flags='python-brace-format',
165
+    )
166
+    SUCCESSFULLY_MIGRATED = _prepare_translatable(
167
+        comments=r"""
168
+        TRANSLATORS: This info message immediately follows the "Using
169
+        deprecated v0.1-style ..." deprecation warning.
170
+        """,
171
+        msg='Successfully migrated to {path!r}.',
172
+        context='info message',
173
+        flags='python-brace-format',
174
+    )
175
+
176
+
177
+class WarnMsgTemplate(enum.Enum):
178
+    EMPTY_SERVICE_NOT_SUPPORTED = _prepare_translatable(
179
+        'An empty SERVICE is not supported by vault(1).  '
180
+        'For compatibility, this will be treated as if SERVICE was not '
181
+        'supplied, i.e., it will error out, or operate on global settings.',
182
+        comments='',
183
+        context='warning message',
184
+    )
185
+    EMPTY_SERVICE_SETTINGS_INACCESSIBLE = _prepare_translatable(
186
+        f'An empty SERVICE is not supported by vault(1).  '
187
+        f'The empty-string service settings will be '
188
+        f'inaccessible and ineffective.  '
189
+        f'To ensure that vault(1) and {PROG_NAME!s} see the settings, '
190
+        f'move them into the "global" section.',
191
+        comments='',
192
+        context='warning message',
193
+    )
194
+    FAILED_TO_MIGRATE_CONFIG = _prepare_translatable(
195
+        comments=r"""
196
+        TRANSLATORS: The error message is usually supplied by the
197
+        operating system, e.g. ENOENT/"No such file or directory".
198
+        """,
199
+        msg='Failed to migrate to {path!r}: {error!s}: {filename!r}.',
200
+        context='warning message',
201
+        flags='python-brace-format',
202
+    )
203
+    GLOBAL_PASSPHRASE_INEFFECTIVE = _prepare_translatable(
204
+        'Setting a global passphrase is ineffective '
205
+        'because a key is also set.',
206
+        comments='',
207
+        context='warning message',
208
+    )
209
+    PASSPHRASE_NOT_NORMALIZED = _prepare_translatable(
210
+        comments=r"""
211
+        TRANSLATORS: The key is a (vault) configuration key, in JSONPath
212
+        syntax, typically "$.global" for the global passphrase or
213
+        "$.services.service_name" or "$.services["service with spaces"]"
214
+        for the services "service_name" and "service with spaces",
215
+        respectively.  The form is one of the four Unicode normalization
216
+        forms: NFC, NFD, NFKC, NFKD.
217
+
218
+        The asterisks are not special.  Please feel free to substitute
219
+        any other appropriate way to mark up emphasis of the word
220
+        "displays".
221
+        """,
222
+        msg='The {key!s} passphrase is not {form!s}-normalized.  '
223
+        'Its serialization as a byte string may not be what you '
224
+        'expect it to be, even if it *displays* correctly.  '
225
+        'Please make sure to double-check any derived '
226
+        'passphrases for unexpected results.',
227
+        context='warning message',
228
+        flags='python-brace-format',
229
+    )
230
+    SERVICE_PASSPHRASE_INEFFECTIVE = _prepare_translatable(
231
+        comments=r"""
232
+        TRANSLATORS: The key that is set need not necessarily be set at
233
+        the service level; it may be a global key as well.
234
+        """,
235
+        msg='Setting a service passphrase is ineffective '
236
+        'because a key is also set: {service!s}.',
237
+        context='warning message',
238
+        flags='python-brace-format',
239
+    )
240
+    STEP_REMOVE_INEFFECTIVE_VALUE = _prepare_translatable(
241
+        'Removing ineffective setting {path!s} = {old!s}.',
242
+        comments='',
243
+        context='warning message',
244
+        flags='python-brace-format',
245
+    )
246
+    STEP_REPLACE_INVALID_VALUE = _prepare_translatable(
247
+        'Replacing invalid value {old!s} for key {path!s} with {new!s}.',
248
+        comments='',
249
+        context='warning message',
250
+        flags='python-brace-format',
251
+    )
252
+    V01_STYLE_CONFIG = _prepare_translatable(
253
+        'Using deprecated v0.1-style config file {old!r}, '
254
+        'instead of v0.2-style {new!r}.  '
255
+        'Support for v0.1-style config filenames will be removed in v1.0.',
256
+        comments='',
257
+        context='deprecation warning message',
258
+        flags='python-brace-format',
259
+    )
260
+    V10_SUBCOMMAND_REQUIRED = _prepare_translatable(
261
+        comments=r"""
262
+        TRANSLATORS: This deprecation warning may be issued at any
263
+        level, i.e. we may actually be talking about subcommands, or
264
+        sub-subcommands, or sub-sub-subcommands, etc., which is what the
265
+        "here" is supposed to indicate.
266
+        """,
267
+        msg='A subcommand will be required here in v1.0.  '
268
+        'See --help for available subcommands.  '
269
+        'Defaulting to subcommand "vault".',
270
+        context='deprecation warning message',
271
+    )
272
+
273
+
274
+class ErrMsgTemplate(enum.Enum):
275
+    CANNOT_CONNECT_TO_AGENT = _prepare_translatable(
276
+        comments=r"""
277
+        TRANSLATORS: The error message is usually supplied by the
278
+        operating system, e.g. ENOENT/"No such file or directory".
279
+        """,
280
+        msg='Cannot connect to the SSH agent: {error!s}: {filename!r}.',
281
+        context='error message',
282
+        flags='python-brace-format',
283
+    )
284
+    CANNOT_DECODEIMPORT_VAULT_SETTINGS = _prepare_translatable(
285
+        msg='Cannot import vault settings: cannot decode JSON: {error!s}.',
286
+        comments='',
287
+        context='error message',
288
+        flags='python-brace-format',
289
+    )
290
+    CANNOT_EXPORT_VAULT_SETTINGS = _prepare_translatable(
291
+        comments=r"""
292
+        TRANSLATORS: The error message is usually supplied by the
293
+        operating system, e.g. ENOENT/"No such file or directory".
294
+        """,
295
+        msg='Cannot export vault settings: {error!s}: {filename!r}.',
296
+        context='error message',
297
+        flags='python-brace-format',
298
+    )
299
+    CANNOT_IMPORT_VAULT_SETTINGS = _prepare_translatable(
300
+        comments=r"""
301
+        TRANSLATORS: The error message is usually supplied by the
302
+        operating system, e.g. ENOENT/"No such file or directory".
303
+        """,
304
+        msg='Cannot import vault settings: {error!s}: {filename!r}.',
305
+        context='error message',
306
+        flags='python-brace-format',
307
+    )
308
+    CANNOT_LOAD_USER_CONFIG = _prepare_translatable(
309
+        comments=r"""
310
+        TRANSLATORS: The error message is usually supplied by the
311
+        operating system, e.g. ENOENT/"No such file or directory".
312
+        """,
313
+        msg='Cannot load user config: {error!s}: {filename!r}.',
314
+        context='error message',
315
+        flags='python-brace-format',
316
+    )
317
+    CANNOT_LOAD_VAULT_SETTINGS = _prepare_translatable(
318
+        comments=r"""
319
+        TRANSLATORS: The error message is usually supplied by the
320
+        operating system, e.g. ENOENT/"No such file or directory".
321
+        """,
322
+        msg='Cannot load vault settings: {error!s}: {filename!r}.',
323
+        context='error message',
324
+        flags='python-brace-format',
325
+    )
326
+    CANNOT_PARSE_AS_VAULT_CONFIG = _prepare_translatable(
327
+        comments=r"""
328
+        TRANSLATORS: Unlike the "Cannot load {path!r} as a {fmt!s} vault
329
+        configuration." message, *this* error message is emitted when we
330
+        have tried loading the path in each of our supported formats,
331
+        and failed.  The user will thus see the above "Cannot load ..."
332
+        warning message potentially multiple times, and this error
333
+        message at the very bottom.
334
+        """,
335
+        msg='Cannot parse {path!r} as a valid vault-native '
336
+        'configuration file/directory.',
337
+        context='error message',
338
+        flags='python-brace-format',
339
+    )
340
+    CANNOT_STORE_VAULT_SETTINGS = _prepare_translatable(
341
+        comments=r"""
342
+        TRANSLATORS: The error message is usually supplied by the
343
+        operating system, e.g. ENOENT/"No such file or directory".
344
+        """,
345
+        msg='Cannot store vault settings: {error!s}: {filename!r}.',
346
+        context='error message',
347
+        flags='python-brace-format',
348
+    )
349
+    CANNOT_UPDATE_SETTINGS_NO_SETTINGS = _prepare_translatable(
350
+        msg='Cannot update {settings_type!s} settings '
351
+        'without any given settings.  '
352
+        'You must specify at least one of --lower, ..., '
353
+        '--symbol, or --phrase or --key.',
354
+        comments='',
355
+        context='error message',
356
+        flags='python-brace-format',
357
+    )
358
+    INVALID_VAULT_CONFIG = _prepare_translatable(
359
+        comments=r"""
360
+        TRANSLATORS: This error message is a reaction to a validator
361
+        function saying *that* the configuration is not valid, but not
362
+        *how* it is not valid.  The configuration file is principally
363
+        parsable, however.
364
+        """,
365
+        msg='Invalid vault config: {config!r}.',
366
+        context='error message',
367
+        flags='python-brace-format',
368
+    )
369
+    MISSING_MODULE = _prepare_translatable(
370
+        'Cannot load the required Python module {module!r}.',
371
+        comments='',
372
+        context='error message',
373
+        flags='python-brace-format',
374
+    )
375
+    NO_AF_UNIX = _prepare_translatable(
376
+        'Cannot connect to an SSH agent because this Python version '
377
+        'does not support UNIX domain sockets.',
378
+        comments='',
379
+        context='error message',
380
+    )
381
+    NO_KEY_OR_PHRASE = _prepare_translatable(
382
+        'No passphrase or key was given in the configuration.  '
383
+        'In this case, the --phrase or --key argument is required.',
384
+        comments='',
385
+        context='error message',
386
+    )
387
+    NO_SSH_AGENT_FOUND = _prepare_translatable(
388
+        'Cannot find any running SSH agent because SSH_AUTH_SOCK is not set.',
389
+        comments='',
390
+        context='error message',
391
+    )
392
+    PARAMS_MUTUALLY_EXCLUSIVE = _prepare_translatable(
393
+        comments=r"""
394
+        TRANSLATORS: The params are long-form command-line option names.
395
+        Typical example: "--key is mutually exclusive with --phrase."
396
+        """,
397
+        msg='{param1!s} is mutually exclusive with {param2!s}.',
398
+        context='error message',
399
+        flags='python-brace-format',
400
+    )
401
+    PARAMS_NEEDS_SERVICE_OR_CONFIG = _prepare_translatable(
402
+        comments=r"""
403
+        TRANSLATORS: The param is a long-form command-line option name,
404
+        and "SERVICE" is the command-line argument for the (sometimes
405
+        optional) service name.
406
+        """,
407
+        msg='{param!s} requires a SERVICE or --config.',
408
+        context='error message',
409
+        flags='python-brace-format',
410
+    )
411
+    PARAMS_NEEDS_SERVICE = _prepare_translatable(
412
+        comments=r"""
413
+        TRANSLATORS: The param is a long-form command-line option name,
414
+        and "SERVICE" is the command-line argument for the (sometimes
415
+        optional) service name.
416
+        """,
417
+        msg='{param!s} requires a SERVICE.',
418
+        context='error message',
419
+        flags='python-brace-format',
420
+    )
421
+    PARAMS_NO_SERVICE = _prepare_translatable(
422
+        comments=r"""
423
+        TRANSLATORS: The param is a long-form command-line option name,
424
+        and "SERVICE" is the command-line argument for the (sometimes
425
+        optional) service name.
426
+        """,
427
+        msg='{param!s} does not take a SERVICE argument.',
428
+        context='error message',
429
+        flags='python-brace-format',
430
+    )
431
+    SERVICE_REQUIRED = _prepare_translatable(
432
+        'Generating a passphrase requires a SERVICE.',
433
+        comments='',
434
+        context='error message',
435
+    )
436
+    SSH_KEY_NOT_LOADED = _prepare_translatable(
437
+        'The requested SSH key is not loaded into the agent.',
438
+        comments='',
439
+        context='error message',
440
+    )
441
+    USER_ABORTED_EDIT = _prepare_translatable(
442
+        'Not saving any new notes: the user aborted the request.',
443
+        comments='',
444
+        context='error message',
445
+    )
446
+    USER_ABORTED_PASSPHRASE = _prepare_translatable(
447
+        'No passphrase was given; the user aborted the request.',
448
+        comments='',
449
+        context='error message',
450
+    )
451
+    USER_ABORTED_SSH_KEY_SELECTION = _prepare_translatable(
452
+        'No SSH key was selected; the user aborted the request.',
453
+        comments='',
454
+        context='error message',
455
+    )
456
+    USER_CONFIG_INVALID = _prepare_translatable(
457
+        comments=r"""
458
+        TRANSLATORS: The error message is usually supplied by the
459
+        operating system, e.g. ENOENT/"No such file or directory".
460
+        """,
461
+        msg='The user configuration file is invalid.  '
462
+        '{error!s}: {filename!r}.',
463
+        context='error message',
464
+        flags='python-brace-format',
465
+    )
0 466