Coverage for src\derivepassphrase\_internals\cli_messages.py: 100.000%
477 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2#
3# SPDX-Licence-Identifier: MIT
5"""Messages for the command-line interface of `derivepassphrase`.
7Also contains some machinery related to internationalization and
8localization.
10!!! warning
12 Non-public module (implementation detail), provided for didactical and
13 educational purposes only. Subject to change without notice, including
14 removal.
16"""
18from __future__ import annotations
20import contextlib
21import datetime
22import enum
23import functools
24import gettext
25import inspect
26import os
27import pathlib
28import string
29import sys
30import textwrap
31import types
32from typing import TYPE_CHECKING, NamedTuple, Protocol, TextIO, Union, cast
34from typing_extensions import TypeAlias, override
36from derivepassphrase import _internals
38if TYPE_CHECKING:
39 from collections.abc import Iterable, Iterator, Mapping, Sequence
41 from typing_extensions import Any, Self
43__all__ = ('PROG_NAME',)
45PROG_NAME = _internals.PROG_NAME
46VERSION = _internals.VERSION
47AUTHOR = _internals.AUTHOR
50def load_translations(
51 localedirs: list[str | bytes | os.PathLike] | None = None,
52 languages: Sequence[str] | None = None,
53 class_: type[gettext.NullTranslations] | None = None,
54) -> gettext.NullTranslations: # pragma: no cover
55 """Load a translation catalog for derivepassphrase.
57 Runs [`gettext.translation`][] under the hood for multiple locale
58 directories. `fallback=True` is implied.
60 Args:
61 localedirs:
62 A list of directories to run [`gettext.translation`][]
63 against. Defaults to `$XDG_DATA_HOME/locale` (usually
64 `~/.local/share/locale`), `{sys.prefix}/share/locale` and
65 `{sys.base_prefix}/share/locale` if not given.
66 languages:
67 Passed directly to [`gettext.translation`][].
68 class_:
69 Passed directly to [`gettext.translation`][].
71 Returns:
72 A (potentially dummy) translation catalog.
74 Raises:
75 RuntimeError:
76 `APPDATA` (on Windows) or `XDG_DATA_HOME` (otherwise) is not
77 set. We attempted to compute the default value, but failed
78 to determine the home directory.
80 """
81 # This function deals completely with external resources that are
82 # hard to mock for testing, and thus is exempt from coverage.
83 if localedirs is None:
84 # TODO(the-13th-letter): Define a public (and opaque) enum for these
85 # special directories so that they are available to callers as well,
86 # without computation. Shift the computation into a separate
87 # top-level function, so that it can be stubbed during tests.
88 # Support the `.../site-packages/share/locale` special directory via
89 # a new enum value, because that is where the derivepassphrase wheel
90 # stores its packaged translations. Then reimplement `gettext.find`
91 # and `gettext.translation` with support for `importlib.resources`.
92 # The heavy lifting is already being done by `locale.normalize`.
93 if sys.platform.startswith('win'):
94 xdg_data_home = (
95 pathlib.Path(os.environ['APPDATA'])
96 if os.environ.get('APPDATA')
97 else pathlib.Path('~').expanduser()
98 )
99 elif os.environ.get('XDG_DATA_HOME'):
100 xdg_data_home = pathlib.Path(os.environ['XDG_DATA_HOME'])
101 else:
102 xdg_data_home = (
103 pathlib.Path('~').expanduser() / '.local' / '.share'
104 )
105 localedirs = [
106 pathlib.Path(xdg_data_home, 'locale'),
107 pathlib.Path(sys.prefix, 'share', 'locale'),
108 pathlib.Path(sys.base_prefix, 'share', 'locale'),
109 ]
110 for localedir in localedirs:
111 with contextlib.suppress(OSError):
112 return gettext.translation(
113 PROG_NAME,
114 localedir=os.fsdecode(localedir),
115 languages=languages,
116 class_=class_,
117 )
118 return gettext.NullTranslations()
121translation = load_translations()
122_debug_translation_message_cache: dict[
123 tuple[str, str],
124 tuple[MsgTemplate, frozenset],
125] = {}
128class DebugTranslations(gettext.NullTranslations):
129 """A debug object indicating which known message is being requested.
131 Each call to the `*gettext` methods will return the enum name if the
132 message is a known translatable message for the `derivepassphrase`
133 command-line interface, or the message itself otherwise.
135 """
137 @staticmethod
138 def _load_cache() -> None:
139 cache = _debug_translation_message_cache
140 for enum_class in MSG_TEMPLATE_CLASSES:
141 for member in enum_class.__members__.values():
142 value = cast('TranslatableString', member.value)
143 queue: list[tuple[TranslatableString, frozenset[str]]] = [
144 (value, frozenset())
145 ]
146 value2 = value.maybe_without_filename()
147 if value != value2:
148 queue.append((value2, frozenset({'filename'})))
149 for v, trimmed in queue:
150 singular = v.singular
151 plural = v.plural
152 context = v.l10n_context
153 cache.setdefault((context, singular), (member, trimmed))
154 # Currently no translatable messages use plural forms
155 if plural: # pragma: no cover
156 cache.setdefault((context, plural), (member, trimmed))
158 @classmethod
159 def _locate_message(
160 cls,
161 message: str,
162 /,
163 *,
164 context: str = '',
165 message_plural: str = '',
166 n: int = 1,
167 ) -> str:
168 try: 2qb} ~ x r b
169 enum_value, trimmed = _debug_translation_message_cache[ 2qb} ~ x r b
170 context, message
171 ]
172 except KeyError: 2qbx
173 return message if not message_plural or n == 1 else message_plural 2qbx
174 return cls._format_enum_name_maybe_with_fields( 1}~rb
175 enum_name=str(enum_value),
176 ts=cast('TranslatableString', enum_value.value),
177 trimmed=trimmed,
178 )
180 @staticmethod
181 def _format_enum_name_maybe_with_fields(
182 enum_name: str,
183 ts: TranslatableString,
184 trimmed: frozenset[str] = frozenset(),
185 ) -> str:
186 formatted_fields = [ 1}~rb
187 f'{f}=None' if f in trimmed else f'{f}={{{f}!r}}'
188 for f in ts.fields()
189 ]
190 return ( 1}~rb
191 '{}({})'.format(enum_name, ', '.join(formatted_fields))
192 if formatted_fields
193 else str(enum_name)
194 )
196 @override
197 def gettext(
198 self,
199 message: str,
200 /,
201 ) -> str:
202 return self._locate_message(message) 2qbx
204 @override
205 def ngettext(
206 self,
207 msgid1: str,
208 msgid2: str,
209 n: int,
210 /,
211 ) -> str: # pragma: no cover
212 """""" # noqa: D419
213 # Currently unused, so no coverage.
214 return self._locate_message(msgid1, message_plural=msgid2, n=n)
216 @override
217 def pgettext(
218 self,
219 context: str,
220 message: str,
221 /,
222 ) -> str:
223 return self._locate_message(message, context=context) 1}~rb
225 @override
226 def npgettext(
227 self,
228 context: str,
229 msgid1: str,
230 msgid2: str,
231 n: int,
232 /,
233 ) -> str: # pragma: no cover
234 """""" # noqa: D419
235 # Currently unused, so no coverage.
236 return self._locate_message(
237 msgid1,
238 context=context,
239 message_plural=msgid2,
240 n=n,
241 )
244class TranslatableString(NamedTuple):
245 """Translatable string as used by the `derivepassphrase` command-line.
247 For typing purposes.
249 Attributes:
250 l10n_context:
251 The localization context, as per [`gettext`][]. Used to
252 disambiguate different uses of the same translatable string.
253 singular:
254 The translatable message, base case.
255 plural:
256 The translatable message, plural case. Usually unset.
257 translator_comments:
258 Explicit commentary for the translator.
259 flags:
260 `.mo` file flags for this message, e.g. to indicate the
261 string formatting style in use.
263 """
265 l10n_context: str
266 """"""
267 singular: str
268 """"""
269 plural: str = ''
270 """"""
271 flags: frozenset[str] = frozenset()
272 """"""
273 translator_comments: str = ''
274 """"""
276 def fields(self) -> list[str]:
277 """Return the replacement fields this template requires.
279 Raises:
280 NotImplementedError:
281 Replacement field discovery for %-formatting is not
282 implemented.
284 """
285 if 'python-format' in self.flags: # pragma: no cover 1a}~rb
286 # Currently unused, so no coverage.
287 err_msg = (
288 'Replacement field discovery for %-formatting '
289 'is not implemented'
290 )
291 raise NotImplementedError(err_msg)
292 if ( 1a}~rb
293 'no-python-brace-format' in self.flags
294 or 'python-brace-format' not in self.flags
295 ):
296 return [] 1a}~r
297 formatter = string.Formatter() 1a}~b
298 fields: dict[str, int] = {} 1a}~b
299 for _lit, field, _spec, _conv in formatter.parse(self.singular): 1a}~b
300 if field is not None and field not in fields: 1a}~b
301 fields[field] = len(fields) 1a}~b
302 return sorted(fields, key=fields.__getitem__) 1a}~b
304 @staticmethod
305 def _maybe_rewrap(
306 string: str,
307 /,
308 *,
309 fix_sentence_endings: bool = True,
310 ) -> str:
311 string = inspect.cleandoc(string)
312 if not any(s.strip() == '\b' for s in string.splitlines()):
313 string = '\n'.join(
314 textwrap.wrap(
315 string,
316 width=float('inf'), # type: ignore[arg-type]
317 fix_sentence_endings=fix_sentence_endings,
318 )
319 )
320 else:
321 string = ''.join(
322 s
323 for s in string.splitlines(True) # noqa: FBT003
324 if s.strip() != '\b'
325 )
326 return string
328 def maybe_without_filename(self) -> Self:
329 """Return a new translatable string without the "filename" field.
331 Only acts upon translatable strings containing the exact
332 contents `": {filename!r}"`. The specified part will be
333 removed. This is correct usage in English for messages like
334 `"Cannot open file: {error}: {filename!r}."`, but not
335 necessarily in other languages.
337 """
338 filename_str = ': {filename!r}' 1adefghijklmnopqstvuwbc
339 ret = self 1adefghijklmnopqstvuwbc
340 a, sep1, b = self.singular.partition(filename_str) 1adefghijklmnopqstvuwbc
341 c, sep2, d = self.plural.partition(filename_str) 1adefghijklmnopqstvuwbc
342 if sep1: 1adefghijklmnopqstvuwbc
343 ret = ret._replace(singular=(a + b)) 1adefghijklmnopqstubc
344 # Currently no translatable messages use plural forms
345 if sep2: # pragma: no cover 1adefghijklmnopqstvuwbc
346 ret = ret._replace(plural=(c + d))
347 return ret 1adefghijklmnopqstvuwbc
349 def rewrapped(self) -> Self:
350 """Return a rewrapped version of self.
352 Normalizes all parts assumed to contain English prose.
354 """
355 msg = self._maybe_rewrap(self.singular, fix_sentence_endings=True)
356 plural = self._maybe_rewrap(self.plural, fix_sentence_endings=True)
357 context = self.l10n_context.strip()
358 comments = self._maybe_rewrap(
359 self.translator_comments, fix_sentence_endings=False
360 )
361 return self._replace(
362 singular=msg,
363 plural=plural,
364 l10n_context=context,
365 translator_comments=comments,
366 )
368 def with_comments(self, comments: str, /) -> Self:
369 """Add or replace the string's translator comments.
371 The comments are assumed to contain English prose, and will be
372 normalized.
374 Returns:
375 A new [`TranslatableString`][] with the specified comments.
377 """
378 if comments.strip() and not comments.lstrip().startswith(
379 'TRANSLATORS:'
380 ): # pragma: no cover
381 # Currently unused, so no coverage.
382 comments = 'TRANSLATORS: ' + comments.lstrip()
383 comments = self._maybe_rewrap(comments, fix_sentence_endings=False)
384 return self._replace(translator_comments=comments)
386 def validate_flags(self, *extra_flags: str) -> Self:
387 """Add all flags, then validate them against the string.
389 Returns:
390 A new [`TranslatableString`][] with the extra flags added,
391 and all flags validated.
393 Raises:
394 ValueError:
395 The flags failed to validate. See the exact error
396 message for details.
398 Examples:
399 >>> TranslatableString('', 'all OK').validate_flags()
400 ... # doctest: +NORMALIZE_WHITESPACE
401 TranslatableString(l10n_context='', singular='all OK', plural='',
402 flags=frozenset(), translator_comments='')
403 >>> TranslatableString('', '20% OK').validate_flags(
404 ... 'no-python-format'
405 ... )
406 ... # doctest: +NORMALIZE_WHITESPACE
407 TranslatableString(l10n_context='', singular='20% OK', plural='',
408 flags=frozenset({'no-python-format'}),
409 translator_comments='')
410 >>> TranslatableString('', '%d items').validate_flags()
411 ... # doctest: +ELLIPSIS
412 Traceback (most recent call last):
413 ...
414 ValueError: Missing flag for how to deal with percent character ...
415 >>> TranslatableString('', '{braces}').validate_flags()
416 ... # doctest: +ELLIPSIS
417 Traceback (most recent call last):
418 ...
419 ValueError: Missing flag for how to deal with brace character ...
420 >>> TranslatableString('', 'no braces').validate_flags(
421 ... 'python-brace-format'
422 ... )
423 ... # doctest: +ELLIPSIS
424 Traceback (most recent call last):
425 ...
426 ValueError: Missing format string parameters ...
428 """
429 all_flags = frozenset(f.strip() for f in self.flags.union(extra_flags))
430 if '{' in self.singular and not bool(
431 all_flags & {'python-brace-format', 'no-python-brace-format'}
432 ):
433 msg = (
434 f'Missing flag for how to deal with brace character '
435 f'in {self.singular!r}'
436 )
437 raise ValueError(msg)
438 if '%' in self.singular and not bool(
439 all_flags & {'python-format', 'no-python-format'}
440 ):
441 msg = (
442 f'Missing flag for how to deal with percent character '
443 f'in {self.singular!r}'
444 )
445 raise ValueError(msg)
446 if (
447 all_flags & {'python-format', 'python-brace-format'}
448 and '%' not in self.singular
449 and '{' not in self.singular
450 ):
451 msg = f'Missing format string parameters in {self.singular!r}'
452 raise ValueError(msg)
453 return self._replace(flags=all_flags)
456def translatable(
457 context: str,
458 single: str,
459 /,
460 flags: Iterable[str] = (),
461 plural: str = '',
462 comments: str = '',
463) -> TranslatableString:
464 """Return a [`TranslatableString`][] with validated parts.
466 This factory function is really only there to make the enum
467 definitions more readable. It is the main implementation of the
468 [`TranslatableStringConstructor`][].
470 """
471 flags = (
472 frozenset(flags) if not isinstance(flags, str) else frozenset({flags})
473 )
474 return (
475 TranslatableString(context, single, plural=plural, flags=flags)
476 .rewrapped()
477 .with_comments(comments)
478 .validate_flags()
479 )
482class TranslatedString:
483 """A string object that stringifies to its translation.
485 The translation and replacement value rendering is only performed
486 when this string object is actually stringified.
488 """
490 def __init__(
491 self,
492 template: str | TranslatableString | MsgTemplate,
493 args_dict: Mapping[str, Any] = types.MappingProxyType({}),
494 /,
495 **kwargs: Any, # noqa: ANN401
496 ) -> None:
497 """Initializer.
499 Args:
500 template:
501 A template string, suitable for [`str.format`][]. If
502 a string, use it directly. If
503 a [`TranslatableString`][], or a known enum value whose
504 value is a `TranslatableString`, then use that string's
505 "singular" entry.
506 args_dict:
507 Keyword arguments to be passed to [`str.format`][].
508 kwargs:
509 More keyword arguments to be passed to [`str.format`][].
511 """
512 if isinstance(template, MSG_TEMPLATE_CLASSES): 2a bbB C D E F G ( ) * + H , I J cbdbebfbK nbL M N O d P e gbhbf g h Q ibR S i j T U V W X Y jbk l Z m n o p 0 - . 1 2 3 q kbrbs sbobtbubvb4 5 6 7 8 lb9 mb! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ pb` abx r b c { |
513 template = cast('TranslatableString', template.value) 2a bbB C D E F G ( ) * + H , I J cbdbebfbK nbL M N O d P e gbhbf g h Q ibR S i j T U V W X Y jbk l Z m n o p 0 - . 1 2 3 q kbrbs sbobtbubvb4 5 6 7 8 lb9 mb! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ pb` r b c
514 self.template = template 2a bbB C D E F G ( ) * + H , I J cbdbebfbK nbL M N O d P e gbhbf g h Q ibR S i j T U V W X Y jbk l Z m n o p 0 - . 1 2 3 q kbrbs sbobtbubvb4 5 6 7 8 lb9 mb! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ pb` abx r b c { |
515 self.kwargs = {**args_dict, **kwargs} 2a bbB C D E F G ( ) * + H , I J cbdbebfbK nbL M N O d P e gbhbf g h Q ibR S i j T U V W X Y jbk l Z m n o p 0 - . 1 2 3 q kbrbs sbobtbubvb4 5 6 7 8 lb9 mb! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ pb` abx r b c { |
516 self._rendered: str | None = None 2a bbB C D E F G ( ) * + H , I J cbdbebfbK nbL M N O d P e gbhbf g h Q ibR S i j T U V W X Y jbk l Z m n o p 0 - . 1 2 3 q kbrbs sbobtbubvb4 5 6 7 8 lb9 mb! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ pb` abx r b c { |
518 def __bool__(self) -> bool:
519 """Return true if the rendered string is truthy."""
520 return bool(str(self)) 1BCDEFH
522 def __eq__(self, other: object) -> bool: # pragma: no cover
523 """Return true if the rendered string is equal to `other`."""
524 # Only used interactively during debugging, so no coverage.
525 return str(self) == other 1rbc
527 def __hash__(self) -> int: # pragma: no cover
528 """Return the hash of the rendered string."""
529 # Only used interactively during debugging, so no coverage.
530 return hash(str(self)) 2a bbG I J cbdbebfbK L M N O d P e gbhbf g h Q ibR S i j T U V W X Y jbk l Z m n o p 0 1 2 3 q kb4 5 6 7 8 lb9 mb! # $ % ' r b c
532 def __repr__(self) -> str: # pragma: no cover
533 """""" # noqa: D419
534 # Only used interactively during debugging, so no coverage.
535 return (
536 f'{self.__class__.__name__}({self.template!r}, '
537 f'{dict(self.kwargs)!r})'
538 )
540 def __str__(self) -> str:
541 """Return the rendered translation of this string.
543 First, look up the translation of the string's template. Then
544 fill in the replacement fields. Cache the result for future
545 calls.
547 """
548 if self._rendered is None: 2a bbB C D E F G ( ) * + H , I J cbdbebfbK nbL M N O d P e gbhbf g h Q ibR S i j T U V W X Y jbk l Z m n o p 0 - . 1 2 3 q kbs ob4 5 6 7 8 lb9 mb! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ pb` abx r b c { |
549 do_escape = False 2a B C D E F G ( ) * + H , I J K L M N O d P e f g h Q R S i j T U V W X Y k l Z m n o p 0 - . 1 2 3 q s 4 5 6 7 8 9 ! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ ` abx r b c { |
550 if isinstance(self.template, str): 2a B C D E F G ( ) * + H , I J K L M N O d P e f g h Q R S i j T U V W X Y k l Z m n o p 0 - . 1 2 3 q s 4 5 6 7 8 9 ! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ ` abx r b c { |
551 context = '' 2y z A abx {
552 template = self.template 2y z A abx {
553 else:
554 context = self.template.l10n_context 1aBCDEFG()*+H,IJKLMNOdPefghQRSijTUVWXYklZmnop0-.123qs456789!#$%'/:ytvuw;z=A?@[]^_`rbc|
555 template = self.template.singular 1aBCDEFG()*+H,IJKLMNOdPefghQRSijTUVWXYklZmnop0-.123qs456789!#$%'/:ytvuw;z=A?@[]^_`rbc|
556 do_escape = 'no-python-brace-format' in self.template.flags 1aBCDEFG()*+H,IJKLMNOdPefghQRSijTUVWXYklZmnop0-.123qs456789!#$%'/:ytvuw;z=A?@[]^_`rbc|
557 template = ( 2a B C D E F G ( ) * + H , I J K L M N O d P e f g h Q R S i j T U V W X Y k l Z m n o p 0 - . 1 2 3 q s 4 5 6 7 8 9 ! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ ` abx r b c { |
558 translation.pgettext(context, template)
559 if context
560 else translation.gettext(template)
561 )
562 template = self._escape(template) if do_escape else template 2a B C D E F G ( ) * + H , I J K L M N O d P e f g h Q R S i j T U V W X Y k l Z m n o p 0 - . 1 2 3 q s 4 5 6 7 8 9 ! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ ` abx r b c { |
563 kwargs = { 2a B C D E F G ( ) * + H , I J K L M N O d P e f g h Q R S i j T U V W X Y k l Z m n o p 0 - . 1 2 3 q s 4 5 6 7 8 9 ! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ ` abx r b c { |
564 k: str(v) if isinstance(v, TranslatedString) else v
565 for k, v in self.kwargs.items()
566 }
567 self._rendered = template.format(**kwargs) 2a B C D E F G ( ) * + H , I J K L M N O d P e f g h Q R S i j T U V W X Y k l Z m n o p 0 - . 1 2 3 q s 4 5 6 7 8 9 ! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ ` abx r b c { |
568 return self._rendered 2a bbB C D E F G ( ) * + H , I J cbdbebfbK nbL M N O d P e gbhbf g h Q ibR S i j T U V W X Y jbk l Z m n o p 0 - . 1 2 3 q kbs ob4 5 6 7 8 lb9 mb! # $ % ' / : y t v u w ; z = A ? @ [ ] ^ _ pb` abx r b c { |
570 @staticmethod
571 def _escape(template: str) -> str:
572 return template.translate({ 1x{|
573 ord('{'): '{{',
574 ord('}'): '}}',
575 })
577 @classmethod
578 def constant(cls, template: str) -> Self:
579 return cls(cls._escape(template)) 1x{
581 def maybe_without_filename(self) -> Self:
582 """Return a new string without the "filename" field.
584 Only acts upon translated strings containing the exact contents
585 `": {filename!r}"`. The specified part will be removed. This
586 acts upon the string *before* translation, i.e., the string
587 without the filename will be used as a translation base.
589 """
590 new_template = ( 1defghijklmnopqstvuwbc
591 self.template.maybe_without_filename()
592 if not isinstance(self.template, str)
593 else self.template
594 )
595 if ( 1defghijklmnopqstvuwbc
596 not isinstance(new_template, str)
597 and self.kwargs.get('filename') is None
598 and new_template != self.template
599 ):
600 return self.__class__(new_template, self.kwargs) 1dfjlmnobc
601 return self 1eghikpqstvuw
604class TranslatableStringConstructor(Protocol):
605 """Construct a [`TranslatableString`][]."""
607 def __call__(
608 self,
609 context: str,
610 single: str,
611 /,
612 flags: Iterable[str] = (),
613 plural: str = '',
614 comments: str = '',
615 ) -> TranslatableString:
616 """Return a [`TranslatableString`][] from these parts.
618 Usually some form of validation or normalization is performed
619 first on these parts.
621 The main implementation of this is in [`translatable`][].
623 """
626def commented(comments: str = '', /) -> TranslatableStringConstructor:
627 """A "decorator" for readably constructing commented enum values.
629 Returns a partial application of [`translatable`][] with the `comments`
630 argument pre-filled.
632 This is geared towards the quirks of the API documentation extractor
633 `mkdocstrings-python`/`griffe`, which reformat and trim enum value
634 declarations in predictable but somewhat weird ways. Chains of function
635 calls are preserved, though, so use this to our advantage to suggest
636 a specific formatting.
638 This is not necessarily good code style, nor is it a lightweight
639 solution.
641 """ # noqa: DOC201
642 return functools.partial(translatable, comments=comments)
645class Label(enum.Enum):
646 """Labels for the `derivepassphrase` command-line.
648 Includes help text (long-form and short-form), help metavar names,
649 diagnostic labels and interactive prompts.
651 """
653 DEPRECATION_WARNING_LABEL = commented(
654 'This is a short label that will be prepended to '
655 'a warning message, e.g., "Deprecation warning: A subcommand '
656 'will be required in v1.0."',
657 )(
658 'Label :: Diagnostics :: Marker',
659 'Deprecation warning',
660 )
661 """"""
662 WARNING_LABEL = commented(
663 'This is a short label that will be prepended to '
664 'a warning message, e.g., "Warning: An empty service name '
665 'is not supported by vault(1)."',
666 )(
667 'Label :: Diagnostics :: Marker',
668 'Warning',
669 )
670 """"""
671 CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL = commented(
672 'This is one of two values of the settings_type metavar '
673 'used in the CANNOT_UPDATE_SETTINGS_NO_SETTINGS entry. '
674 'It is only used there. '
675 'The full sentence then reads: '
676 '"Cannot update the global settings without any given settings."',
677 )(
678 'Label :: Error message :: Metavar',
679 'global settings',
680 )
681 """"""
682 CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE = commented(
683 'This is one of two values of the settings_type metavar '
684 'used in the CANNOT_UPDATE_SETTINGS_NO_SETTINGS entry. '
685 'It is only used there. '
686 'The full sentence then reads: '
687 '"Cannot update the service-specific settings without any '
688 'given settings."',
689 )(
690 'Label :: Error message :: Metavar',
691 'service-specific settings',
692 )
693 """"""
694 SETTINGS_ORIGIN_INTERACTIVE = commented(
695 'This value is used as the {key} metavar for '
696 'Label.PASSPHRASE_NOT_NORMALIZED if the passphrase was '
697 'entered interactively.',
698 )(
699 'Label :: Error message :: Metavar',
700 'interactive input',
701 )
702 CONFIGURATION_EPILOG = commented(
703 '',
704 )(
705 'Label :: Help text :: Explanation',
706 'Use $VISUAL or $EDITOR to configure the spawned editor.',
707 )
708 """"""
709 DERIVEPASSPHRASE_02 = commented(
710 '',
711 )(
712 'Label :: Help text :: Explanation',
713 'The currently implemented subcommands are "vault" '
714 '(for the scheme used by vault) and "export" '
715 '(for exporting foreign configuration data). '
716 'See the respective `--help` output for instructions. '
717 'If no subcommand is given, we default to "vault".',
718 )
719 """"""
720 DERIVEPASSPHRASE_03 = commented(
721 '',
722 )(
723 'Label :: Help text :: Explanation',
724 'Deprecation notice: Defaulting to "vault" is deprecated. '
725 'Starting in v1.0, the subcommand must be specified explicitly.',
726 )
727 """"""
728 DERIVEPASSPHRASE_EPILOG_01 = commented(
729 '',
730 )(
731 'Label :: Help text :: Explanation',
732 'Configuration is stored in a directory according to the '
733 '`DERIVEPASSPHRASE_PATH` variable, which defaults to '
734 '`~/.derivepassphrase` on UNIX-like systems and '
735 r'`C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.',
736 )
737 """"""
738 DERIVEPASSPHRASE_EXPORT_02 = commented(
739 '',
740 )(
741 'Label :: Help text :: Explanation',
742 'The only available subcommand is "vault", '
743 'which implements the vault-native configuration scheme. '
744 'If no subcommand is given, we default to "vault".',
745 )
746 """"""
747 DERIVEPASSPHRASE_EXPORT_03 = DERIVEPASSPHRASE_03
748 """"""
749 DERIVEPASSPHRASE_EXPORT_VAULT_02 = commented(
750 'The metavar is Label.EXPORT_VAULT_METAVAR_PATH.',
751 )(
752 'Label :: Help text :: Explanation',
753 'Depending on the configuration format, '
754 '{path_metavar} may either be a file or a directory. '
755 'We support the vault "v0.2", "v0.3" and "storeroom" formats.',
756 flags='python-brace-format',
757 )
758 """"""
759 DERIVEPASSPHRASE_EXPORT_VAULT_03 = commented(
760 'The metavar is Label.EXPORT_VAULT_METAVAR_PATH.',
761 )(
762 'Label :: Help text :: Explanation',
763 'If {path_metavar} is explicitly given as `VAULT_PATH`, '
764 'then use the `VAULT_PATH` environment variable to '
765 'determine the correct path. '
766 '(Use `./VAULT_PATH` or similar to indicate a file/directory '
767 'actually named `VAULT_PATH`.)',
768 flags='python-brace-format',
769 )
770 """"""
771 DERIVEPASSPHRASE_VAULT_02 = commented(
772 'The metavar is Label.VAULT_METAVAR_SERVICE.',
773 )(
774 'Label :: Help text :: Explanation',
775 'If operating on global settings, or importing/exporting settings, '
776 'then {service_metavar} must be omitted. '
777 'Otherwise it is required.',
778 flags='python-brace-format',
779 )
780 """"""
781 DERIVEPASSPHRASE_VAULT_EPILOG_01 = commented(
782 '',
783 )(
784 'Label :: Help text :: Explanation',
785 'WARNING: There is NO WAY to retrieve the generated passphrases '
786 'if the master passphrase, the SSH key, or the exact '
787 'passphrase settings are lost, '
788 'short of trying out all possible combinations. '
789 'You are STRONGLY advised to keep independent backups of '
790 'the settings and the SSH key, if any.',
791 )
792 """"""
793 DERIVEPASSPHRASE_VAULT_EPILOG_02 = commented(
794 '',
795 )(
796 'Label :: Help text :: Explanation',
797 'The configuration is NOT encrypted, and you are '
798 'STRONGLY discouraged from using a stored passphrase.',
799 )
800 """"""
801 DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT = commented(
802 "This instruction text is shown above the user's old stored notes "
803 'for this service, if any, if the recommended '
804 '"modern" editor interface is used. '
805 'The next line is the cut marking defined in '
806 'Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER.'
807 )(
808 'Label :: Help text :: Explanation',
809 """\
810\b
811# Enter notes below the line with the cut mark (ASCII scissors and
812# dashes). Lines above the cut mark (such as this one) will be ignored.
813#
814# If you wish to clear the notes, leave everything beyond the cut mark
815# blank. However, if you leave the *entire* file blank, also removing
816# the cut mark, then the edit is aborted, and the old notes contents are
817# retained.
818#
819""",
820 )
821 """"""
822 DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT = commented(
823 'This instruction text is shown if the vault(1)-compatible '
824 '"legacy" editor interface is used and no previous notes exist. '
825 'The interface does not support commentary in the notes, '
826 'so we fill this with obvious placeholder text instead. '
827 '(Please replace this with what *your* language/culture would '
828 'obviously recognize as placeholder text.)'
829 )(
830 'Label :: Help text :: Explanation',
831 'INSERT NOTES HERE',
832 )
833 """"""
834 PASSPHRASE_GENERATION_EPILOG = commented(
835 'The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.',
836 )(
837 'Label :: Help text :: Explanation',
838 'Use {metavar}=0 to exclude a character type from the output.',
839 flags='python-brace-format',
840 )
841 """"""
842 STORAGE_MANAGEMENT_EPILOG = commented(
843 'The metavar is Label.STORAGE_MANAGEMENT_METAVAR_PATH.',
844 )(
845 'Label :: Help text :: Explanation',
846 'Using "-" as {metavar} for standard input/standard output '
847 'is supported.',
848 flags='python-brace-format',
849 )
850 """"""
851 DEPRECATED_COMMAND_LABEL = commented(
852 'We use this format string to indicate, at the beginning '
853 "of a command's help text, that this command is deprecated.",
854 )(
855 'Label :: Help text :: Marker',
856 '(Deprecated) {text}',
857 flags='python-brace-format',
858 )
859 """"""
860 DERIVEPASSPHRASE_VAULT_NOTES_MARKER = commented(
861 'The marker for separating the text from '
862 'Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT '
863 "from the user's input (below the marker). "
864 'The first line starting with this label marks the separation point.',
865 )(
866 'Label :: Help text :: Marker',
867 '# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -',
868 )
869 """"""
870 EXPORT_VAULT_FORMAT_METAVAR_FMT = commented(
871 'This text is used as {metavar} in '
872 'Label.EXPORT_VAULT_FORMAT_HELP_TEXT, yielding e.g. '
873 '"Try the following storage format FMT."',
874 )(
875 'Label :: Help text :: Metavar :: export vault',
876 'FMT',
877 )
878 """"""
879 EXPORT_VAULT_KEY_METAVAR_K = commented(
880 'This text is used as {metavar} in '
881 'Label.EXPORT_VAULT_KEY_HELP_TEXT, yielding e.g. '
882 '"Use K as the storage master key."',
883 )(
884 'Label :: Help text :: Metavar :: export vault',
885 'K',
886 )
887 """"""
888 EXPORT_VAULT_METAVAR_PATH = commented(
889 'Used as "path_metavar" in '
890 'Label.DERIVEPASSPHRASE_EXPORT_VAULT_02 and others, '
891 'yielding e.g. "Depending on the configuration format, '
892 'PATH may either be a file or a directory."',
893 )(
894 'Label :: Help text :: Metavar :: export vault',
895 'PATH',
896 )
897 """"""
898 PASSPHRASE_GENERATION_METAVAR_NUMBER = commented(
899 'This metavar is used in Label.PASSPHRASE_GENERATION_EPILOG, '
900 'Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT and others, '
901 'yielding e.g. "Ensure a passphrase length of NUMBER characters.". ',
902 )(
903 'Label :: Help text :: Metavar :: vault',
904 'NUMBER',
905 )
906 """"""
907 STORAGE_MANAGEMENT_METAVAR_PATH = commented(
908 'This metavar is used in Label.STORAGE_MANAGEMENT_EPILOG, '
909 'Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT and others, '
910 'yielding e.g. "Ensure a passphrase length of NUMBER characters.". ',
911 )(
912 'Label :: Help text :: Metavar :: vault',
913 'PATH',
914 )
915 """"""
916 VAULT_METAVAR_SERVICE = commented(
917 'This metavar is used as "service_metavar" in multiple help texts, '
918 'such as Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT, '
919 'Label.DERIVEPASSPHRASE_VAULT_02, ErrMsgTemplate.SERVICE_REQUIRED, '
920 'etc. Sample texts are "Deriving a passphrase requires a SERVICE.", '
921 '"save the given settings for SERVICE, or global" and '
922 '"If operating on global settings, or importing/exporting settings, '
923 'then SERVICE must be omitted."',
924 )(
925 'Label :: Help text :: Metavar :: vault',
926 'SERVICE',
927 )
928 """"""
929 DEBUG_OPTION_HELP_TEXT = commented(
930 '',
931 )(
932 'Label :: Help text :: One-line description',
933 'Also emit debug information. Implies --verbose.',
934 )
935 """"""
936 DERIVEPASSPHRASE_01 = commented(
937 'This is the first paragraph of the command help text, '
938 'but it also appears (in truncated form, if necessary) '
939 'as one-line help text for this command. '
940 'The translation should thus be as meaningful as possible '
941 'even if truncated.',
942 )(
943 'Label :: Help text :: One-line description',
944 'Derive a strong passphrase, deterministically, from a master secret.',
945 )
946 """"""
947 DERIVEPASSPHRASE_EXPORT_01 = commented(
948 'This is the first paragraph of the command help text, '
949 'but it also appears (in truncated form, if necessary) '
950 'as one-line help text for this command. '
951 'The translation should thus be as meaningful as possible '
952 'even if truncated.',
953 )(
954 'Label :: Help text :: One-line description',
955 'Export a foreign configuration to standard output.',
956 )
957 """"""
958 DERIVEPASSPHRASE_EXPORT_VAULT_01 = commented(
959 'This is the first paragraph of the command help text, '
960 'but it also appears (in truncated form, if necessary) '
961 'as one-line help text for this command. '
962 'The translation should thus be as meaningful as possible '
963 'even if truncated.',
964 )(
965 'Label :: Help text :: One-line description',
966 'Export a vault-native configuration to standard output.',
967 )
968 """"""
969 DERIVEPASSPHRASE_VAULT_01 = commented(
970 'This is the first paragraph of the command help text, '
971 'but it also appears (in truncated form, if necessary) '
972 'as one-line help text for this command. '
973 'The translation should thus be as meaningful as possible '
974 'even if truncated.',
975 )(
976 'Label :: Help text :: One-line description',
977 'Derive a passphrase using the vault derivation scheme.',
978 )
979 """"""
980 DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT = commented(
981 'The metavar is Label.VAULT_METAVAR_SERVICE.',
982 )(
983 'Label :: Help text :: One-line description',
984 'Save the given settings for {service_metavar}, or global.',
985 flags='python-brace-format',
986 )
987 """"""
988 DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT = commented(
989 'The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.',
990 )(
991 'Label :: Help text :: One-line description',
992 'Ensure at least {metavar} "-" or "_" characters.',
993 flags='python-brace-format',
994 )
995 """"""
996 DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT = commented(
997 '',
998 )(
999 'Label :: Help text :: One-line description',
1000 'Delete all settings.',
1001 )
1002 """"""
1003 DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT = commented(
1004 '',
1005 )(
1006 'Label :: Help text :: One-line description',
1007 'Delete the global settings.',
1008 )
1009 """"""
1010 DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT = commented(
1011 'The metavar is Label.VAULT_METAVAR_SERVICE.',
1012 )(
1013 'Label :: Help text :: One-line description',
1014 'Delete the settings for {service_metavar}.',
1015 flags='python-brace-format',
1016 )
1017 """"""
1018 DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT = commented(
1019 'The corresponding option is displayed as '
1020 '"--modern-editor-interface / --vault-legacy-editor-interface", '
1021 'so you may want to hint that the default (legacy) '
1022 'is the second of those options. '
1023 'Though the vault(1) legacy editor interface clearly has deficiencies '
1024 'and (in my opinion) should only be used for compatibility purposes, '
1025 'the one-line help text should try not to sound too judgmental, '
1026 'if possible.',
1027 )(
1028 'Label :: Help text :: One-line description',
1029 'Edit notes using the modern editor interface '
1030 'or the vault-like legacy one (default).',
1031 )
1032 """"""
1033 DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT = commented(
1034 'The corresponding option is displayed as '
1035 '"--export-as=json|sh", so json refers to the JSON format (default) '
1036 'and sh refers to the POSIX sh format. '
1037 'Please ensure that it is clear what the "json" and "sh" refer to '
1038 'in your translation... even if you cannot use texutal correspondence '
1039 'like the English text does.',
1040 )(
1041 'Label :: Help text :: One-line description',
1042 'When exporting, export as JSON (default) or as POSIX sh.',
1043 )
1044 """"""
1045 DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT = commented(
1046 'The metavar is Label.STORAGE_MANAGEMENT_METAVAR_PATH.',
1047 )(
1048 'Label :: Help text :: One-line description',
1049 'Export all saved settings to {metavar}.',
1050 flags='python-brace-format',
1051 )
1052 """"""
1053 DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT = commented(
1054 'The metavar is Label.STORAGE_MANAGEMENT_METAVAR_PATH.',
1055 )(
1056 'Label :: Help text :: One-line description',
1057 'Import saved settings from {metavar}.',
1058 flags='python-brace-format',
1059 )
1060 """"""
1061 DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT = commented(
1062 '',
1063 )(
1064 'Label :: Help text :: One-line description',
1065 'Select a suitable SSH key from the SSH agent.',
1066 )
1067 """"""
1068 DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT = commented(
1069 'The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.',
1070 )(
1071 'Label :: Help text :: One-line description',
1072 'Ensure a passphrase length of {metavar} characters.',
1073 flags='python-brace-format',
1074 )
1075 """"""
1076 DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT = commented(
1077 'The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.',
1078 )(
1079 'Label :: Help text :: One-line description',
1080 'Ensure at least {metavar} lowercase characters.',
1081 flags='python-brace-format',
1082 )
1083 """"""
1084 DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT = commented(
1085 'The metavar is Label.VAULT_METAVAR_SERVICE.',
1086 )(
1087 'Label :: Help text :: One-line description',
1088 'With --config and {service_metavar}, spawn an editor to edit notes.',
1089 flags='python-brace-format',
1090 )
1091 """"""
1092 DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT = commented(
1093 'The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.',
1094 )(
1095 'Label :: Help text :: One-line description',
1096 'Ensure at least {metavar} digits.',
1097 flags='python-brace-format',
1098 )
1099 """"""
1100 DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT = commented(
1101 'The corresponding option is displayed as '
1102 '"--overwrite-existing / --merge-existing", so you may want to '
1103 'hint that the default (merge) is the second of those options.',
1104 )(
1105 'Label :: Help text :: One-line description',
1106 'Overwrite or merge (default) the existing configuration.',
1107 )
1108 """"""
1109 DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT = commented(
1110 '',
1111 )(
1112 'Label :: Help text :: One-line description',
1113 'Prompt for a master passphrase.',
1114 )
1115 """"""
1116 DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT = commented(
1117 'The corresponding option is displayed as '
1118 '"--print-notes-before / --print-notes-after", so you may want to '
1119 'hint that the default (after) is the second of those options.',
1120 )(
1121 'Label :: Help text :: One-line description',
1122 'Print the notes for {service_metavar} (if any) before or '
1123 'after (default) the derived passphrase.',
1124 flags='python-brace-format',
1125 )
1126 """"""
1127 DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT = commented(
1128 'The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.',
1129 )(
1130 'Label :: Help text :: One-line description',
1131 'Restrict runs of identical characters to at most {metavar} '
1132 'characters.',
1133 flags='python-brace-format',
1134 )
1135 """"""
1136 DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT = commented(
1137 'The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.',
1138 )(
1139 'Label :: Help text :: One-line description',
1140 'Ensure at least {metavar} spaces.',
1141 flags='python-brace-format',
1142 )
1143 """"""
1144 DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT = commented(
1145 'The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.',
1146 )(
1147 'Label :: Help text :: One-line description',
1148 'Ensure at least {metavar} symbol characters.',
1149 flags='python-brace-format',
1150 )
1151 """"""
1152 DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT = commented(
1153 'The corresponding option is displayed as '
1154 '"--unset=phrase|key|...|symbol", so the "given setting" is '
1155 'referring to "phrase", "key", "lower", ..., or "symbol", '
1156 'respectively. '
1157 '"with --config" here means that the user must also specify '
1158 '"--config" for this option to have any effect.',
1159 )(
1160 'Label :: Help text :: One-line description',
1161 'With --config, also unset the given setting. '
1162 'May be specified multiple times.',
1163 )
1164 """"""
1165 DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT = commented(
1166 'The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.',
1167 )(
1168 'Label :: Help text :: One-line description',
1169 'Ensure at least {metavar} uppercase characters.',
1170 flags='python-brace-format',
1171 )
1172 """"""
1174 EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT = commented(
1175 'See EXPORT_VAULT_FORMAT_HELP_TEXT. '
1176 'The format names/labels "v0.3", "v0.2" and "storeroom" '
1177 'should not be translated.',
1178 )(
1179 'Label :: Help text :: One-line description',
1180 'Default: v0.3, v0.2, storeroom.',
1181 )
1182 """"""
1183 EXPORT_VAULT_FORMAT_HELP_TEXT = commented(
1184 'The defaults_hint is Label.EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT, '
1185 'the metavar is Label.EXPORT_VAULT_FORMAT_METAVAR_FMT.',
1186 )(
1187 'Label :: Help text :: One-line description',
1188 'Try the following storage format {metavar}. '
1189 'If specified multiple times, the '
1190 'formats will be tried in order. '
1191 '{defaults_hint}',
1192 flags='python-brace-format',
1193 )
1194 """"""
1195 EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT = commented(
1196 'See EXPORT_VAULT_KEY_HELP_TEXT.',
1197 )(
1198 'Label :: Help text :: One-line description',
1199 'Default: check the VAULT_KEY, LOGNAME, USER, or USERNAME '
1200 'environment variables.',
1201 )
1202 """"""
1203 EXPORT_VAULT_KEY_HELP_TEXT = commented(
1204 'The defaults_hint is Label.EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT, '
1205 'the metavar is Label.EXPORT_VAULT_KEY_METAVAR_K.',
1206 )(
1207 'Label :: Help text :: One-line description',
1208 'Use {metavar} as the storage master key. {defaults_hint}',
1209 flags='python-brace-format',
1210 )
1211 """"""
1212 HELP_OPTION_HELP_TEXT = commented(
1213 '',
1214 )(
1215 'Label :: Help text :: One-line description',
1216 'Show this help text, then exit.',
1217 )
1218 """"""
1219 QUIET_OPTION_HELP_TEXT = commented(
1220 '',
1221 )(
1222 'Label :: Help text :: One-line description',
1223 'Suppress even warnings; emit only errors.',
1224 )
1225 """"""
1226 VERBOSE_OPTION_HELP_TEXT = commented(
1227 '',
1228 )(
1229 'Label :: Help text :: One-line description',
1230 'Emit extra/progress information to standard error.',
1231 )
1232 """"""
1233 VERSION_OPTION_HELP_TEXT = commented(
1234 '',
1235 )(
1236 'Label :: Help text :: One-line description',
1237 'Show version and feature information, then exit.',
1238 )
1239 """"""
1240 COMMANDS_LABEL = commented(
1241 '',
1242 )(
1243 'Label :: Help text :: Option group name',
1244 'Commands',
1245 )
1246 """"""
1247 COMPATIBILITY_OPTION_LABEL = commented(
1248 '',
1249 )(
1250 'Label :: Help text :: Option group name',
1251 'Compatibility and extension options',
1252 )
1253 """"""
1254 CONFIGURATION_LABEL = commented(
1255 '',
1256 )(
1257 'Label :: Help text :: Option group name',
1258 'Configuration',
1259 )
1260 """"""
1261 LOGGING_LABEL = commented(
1262 '',
1263 )(
1264 'Label :: Help text :: Option group name',
1265 'Logging',
1266 )
1267 """"""
1268 OPTIONS_LABEL = commented(
1269 '',
1270 )(
1271 'Label :: Help text :: Option group name',
1272 'Options',
1273 )
1274 """"""
1275 OTHER_OPTIONS_LABEL = commented(
1276 '',
1277 )(
1278 'Label :: Help text :: Option group name',
1279 'Other options',
1280 )
1281 """"""
1282 PASSPHRASE_GENERATION_LABEL = commented(
1283 '',
1284 )(
1285 'Label :: Help text :: Option group name',
1286 'Passphrase generation',
1287 )
1288 """"""
1289 STORAGE_MANAGEMENT_LABEL = commented(
1290 '',
1291 )(
1292 'Label :: Help text :: Option group name',
1293 'Storage management',
1294 )
1295 """"""
1296 VERSION_INFO_MAJOR_LIBRARY_TEXT = commented(
1297 'This message reports on the version of a major library '
1298 'currently in use, such as "cryptography".',
1299 )(
1300 'Label :: Info Message',
1301 'Using {dependency_name_and_version}',
1302 flags='python-brace-format',
1303 )
1304 """"""
1305 ENABLED_PEP508_EXTRAS = commented(
1306 'This is part of the version output, emitting lists of enabled '
1307 'PEP 508 extras. '
1308 'A comma-separated English list of items follows, '
1309 'with standard English punctuation.',
1310 )(
1311 'Label :: Info Message:: Table row header',
1312 'PEP 508 extras:',
1313 )
1314 """"""
1315 SUPPORTED_DERIVATION_SCHEMES = commented(
1316 'This is part of the version output, emitting lists of supported '
1317 'derivation schemes. '
1318 'A comma-separated English list of items follows, '
1319 'with standard English punctuation.',
1320 )(
1321 'Label :: Info Message:: Table row header',
1322 'Supported derivation schemes:',
1323 )
1324 """"""
1325 SUPPORTED_FEATURES = commented(
1326 'This is part of the version output, emitting lists of supported '
1327 'features for this subcommand. '
1328 'A comma-separated English list of items follows, '
1329 'with standard English punctuation.',
1330 )(
1331 'Label :: Info Message:: Table row header',
1332 'Supported features:',
1333 )
1334 """"""
1335 SUPPORTED_FOREIGN_CONFIGURATION_FORMATS = commented(
1336 'This is part of the version output, emitting lists of supported '
1337 'foreign configuration formats. '
1338 'A comma-separated English list of items follows, '
1339 'with standard English punctuation.',
1340 )(
1341 'Label :: Info Message:: Table row header',
1342 'Supported foreign configuration formats:',
1343 )
1344 """"""
1345 SUPPORTED_SUBCOMMANDS = commented(
1346 'This is part of the version output, emitting lists of supported '
1347 'subcommands. '
1348 'A comma-separated English list of items follows, '
1349 'with standard English punctuation.',
1350 )(
1351 'Label :: Info Message:: Table row header',
1352 'Supported subcommands:',
1353 )
1354 """"""
1355 UNAVAILABLE_DERIVATION_SCHEMES = commented(
1356 'This is part of the version output, emitting lists of known, '
1357 'unavailable derivation schemes. '
1358 'A comma-separated English list of items follows, '
1359 'with standard English punctuation.',
1360 )(
1361 'Label :: Info Message:: Table row header',
1362 'Known derivation schemes:',
1363 )
1364 """"""
1365 UNAVAILABLE_FEATURES = commented(
1366 'This is part of the version output, emitting lists of known, '
1367 'unavailable features for this subcommand. '
1368 'A comma-separated English list of items follows, '
1369 'with standard English punctuation.',
1370 )(
1371 'Label :: Info Message:: Table row header',
1372 'Known features:',
1373 )
1374 """"""
1375 UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS = commented(
1376 'This is part of the version output, emitting lists of known, '
1377 'unavailable foreign configuration formats. '
1378 'A comma-separated English list of items follows, '
1379 'with standard English punctuation.',
1380 )(
1381 'Label :: Info Message:: Table row header',
1382 'Known foreign configuration formats:',
1383 )
1384 """"""
1385 CONFIRM_THIS_CHOICE_PROMPT_TEXT = commented(
1386 'There is no support for "yes" or "no" in other languages '
1387 'than English, so it is advised that your translation makes it '
1388 'clear that only the strings "y", "yes", "n" or "no" are supported, '
1389 'even if the prompt becomes a bit longer.',
1390 )(
1391 'Label :: Interactive prompt',
1392 'Confirm this choice? (y/N)',
1393 )
1394 """"""
1395 SUITABLE_SSH_KEYS_LABEL = commented(
1396 'This label is the heading of the list of suitable SSH keys.',
1397 )(
1398 'Label :: Interactive prompt',
1399 'Suitable SSH keys:',
1400 )
1401 """"""
1402 YOUR_SELECTION_PROMPT_TEXT = commented(
1403 '',
1404 )(
1405 'Label :: Interactive prompt',
1406 'Your selection? (1-{n}, leave empty to abort)',
1407 flags='python-brace-format',
1408 )
1409 """"""
1412class DebugMsgTemplate(enum.Enum):
1413 """Debug messages for the `derivepassphrase` command-line."""
1415 BUCKET_ITEM_FOUND = commented(
1416 'This message is emitted by the vault configuration exporter '
1417 'for "storeroom"-type configuration directories. '
1418 'The system stores entries in different "buckets" of a hash table. '
1419 'Here, we report on a single item (path and value) we discovered '
1420 'after decrypting the whole bucket. '
1421 '(We ensure the path and value are printable as-is.)',
1422 )(
1423 'Debug message',
1424 'Found bucket item: {path} -> {value}',
1425 flags='python-brace-format',
1426 )
1427 """"""
1428 DECRYPT_BUCKET_ITEM_INFO = commented(
1429 '"AES256-CBC" and "PKCS#7" are, in essence, names of formats, '
1430 'and should not be translated. '
1431 '"IV" means "initialization vector", and is specifically '
1432 'a cryptographic term, as are "plaintext" and "ciphertext".',
1433 )(
1434 'Debug message',
1435 """\
1436Decrypt bucket item contents:
1438\b
1439 Encryption key (master key): {enc_key}
1440 Encryption cipher: AES256-CBC with PKCS#7 padding
1441 Encryption IV: {iv}
1442 Encrypted ciphertext: {ciphertext}
1443 Plaintext: {plaintext}
1444""",
1445 flags='python-brace-format',
1446 )
1447 """"""
1448 DECRYPT_BUCKET_ITEM_KEY_INFO = commented(
1449 '',
1450 )(
1451 'Debug message',
1452 """\
1453Decrypt bucket item:
1455\b
1456 Plaintext: {plaintext}
1457 Encryption key (master key): {enc_key}
1458 Signing key (master key): {sign_key}
1459""",
1460 flags='python-brace-format',
1461 )
1462 """"""
1463 DECRYPT_BUCKET_ITEM_MAC_INFO = commented(
1464 'The MAC stands for "message authentication code", '
1465 'which guarantees the authenticity of the message to anyone '
1466 'who holds the corresponding key, similar to a digital signature. '
1467 'The acronym "MAC" is assumed to be well-known to the '
1468 'English target audience, or at least discoverable by them; '
1469 'they *are* asking for debug output, after all. '
1470 'Please use your judgement as to whether to translate this term '
1471 'or not, expanded or not.',
1472 )(
1473 'Debug message',
1474 """\
1475Decrypt bucket item contents:
1477\b
1478 MAC key: {sign_key}
1479 Authenticated content: {ciphertext}
1480 Claimed MAC value: {claimed_mac}
1481 Computed MAC value: {actual_mac}
1482""",
1483 flags='python-brace-format',
1484 )
1485 """"""
1486 DECRYPT_BUCKET_ITEM_SESSION_KEYS_INFO = commented(
1487 '"AES256-CBC" and "PKCS#7" are, in essence, names of formats, '
1488 'and should not be translated. '
1489 '"IV" means "initialization vector", and is specifically '
1490 'a cryptographic term, as are "plaintext" and "ciphertext".',
1491 )(
1492 'Debug message',
1493 """\
1494Decrypt bucket item session keys:
1496\b
1497 Encryption key (master key): {enc_key}
1498 Encryption cipher: AES256-CBC with PKCS#7 padding
1499 Encryption IV: {iv}
1500 Encrypted ciphertext: {ciphertext}
1501 Plaintext: {plaintext}
1502 Parsed plaintext: {code}
1503""",
1504 flags='python-brace-format',
1505 )
1506 """"""
1507 DECRYPT_BUCKET_ITEM_SESSION_KEYS_MAC_INFO = commented(
1508 'The MAC stands for "message authentication code", '
1509 'which guarantees the authenticity of the message to anyone '
1510 'who holds the corresponding key, similar to a digital signature. '
1511 'The acronym "MAC" is assumed to be well-known to the '
1512 'English target audience, or at least discoverable by them; '
1513 'they *are* asking for debug output, after all. '
1514 'Please use your judgement as to whether to translate this term '
1515 'or not, expanded or not.',
1516 )(
1517 'Debug message',
1518 """\
1519Decrypt bucket item session keys:
1521\b
1522 MAC key (master key): {sign_key}
1523 Authenticated content: {ciphertext}
1524 Claimed MAC value: {claimed_mac}
1525 Computed MAC value: {actual_mac}
1526""",
1527 flags='python-brace-format',
1528 )
1529 """"""
1530 DERIVED_MASTER_KEYS_KEYS = commented(
1531 '',
1532 )(
1533 'Debug message',
1534 """\
1535Derived master keys' keys:
1537\b
1538 Encryption key: {enc_key}
1539 Signing key: {sign_key}
1540 Password: {pw_bytes}
1541 Function call: pbkdf2(algorithm={algorithm!r}, length={length!r}, salt={salt!r}, iterations={iterations!r})
1542""", # noqa: E501
1543 flags='python-brace-format',
1544 )
1545 """"""
1546 DIRECTORY_CONTENTS_CHECK_OK = commented(
1547 'This message is emitted by the vault configuration exporter '
1548 'for "storeroom"-type configuration directories, '
1549 'while "assembling" the items stored in the configuration '
1550 """according to the item's "path". """
1551 'Each "directory" in the path contains a list of children '
1552 'it claims to contain, and this list must be matched '
1553 'against the actual discovered items. '
1554 'Now, at the end, we actually confirm the claim. '
1555 '(We would have already thrown an error here otherwise.)',
1556 )(
1557 'Debug message',
1558 'Directory contents check OK: {path} -> {contents}',
1559 flags='python-brace-format',
1560 )
1561 """"""
1562 MASTER_KEYS_DATA_MAC_INFO = commented(
1563 'The MAC stands for "message authentication code", '
1564 'which guarantees the authenticity of the message to anyone '
1565 'who holds the corresponding key, similar to a digital signature. '
1566 'The acronym "MAC" is assumed to be well-known to the '
1567 'English target audience, or at least discoverable by them; '
1568 'they *are* asking for debug output, after all. '
1569 'Please use your judgement as to whether to translate this term '
1570 'or not, expanded or not.',
1571 )(
1572 'Debug message',
1573 """\
1574Master keys data:
1576\b
1577 MAC key: {sign_key}
1578 Authenticated content: {ciphertext}
1579 Claimed MAC value: {claimed_mac}
1580 Computed MAC value: {actual_mac}
1581""",
1582 flags='python-brace-format',
1583 )
1584 """"""
1585 POSTPONING_DIRECTORY_CONTENTS_CHECK = commented(
1586 'This message is emitted by the vault configuration exporter '
1587 'for "storeroom"-type configuration directories, '
1588 'while "assembling" the items stored in the configuration '
1589 """according to the item's "path". """
1590 'Each "directory" in the path contains a list of children '
1591 'it claims to contain, and this list must be matched '
1592 'against the actual discovered items. '
1593 'When emitting this message, we merely indicate that we saved '
1594 'the "claimed" list for this directory for later.',
1595 )(
1596 'Debug message',
1597 'Postponing directory contents check: {path} -> {contents}',
1598 flags='python-brace-format',
1599 )
1600 """"""
1601 SETTING_CONFIG_STRUCTURE_CONTENTS = commented(
1602 'This message is emitted by the vault configuration exporter '
1603 'for "storeroom"-type configuration directories, '
1604 'while "assembling" the items stored in the configuration '
1605 """according to the item's "path". """
1606 'We confirm that we set the entry at the given path '
1607 'to the given value.',
1608 )(
1609 'Debug message',
1610 'Setting contents: {path} -> {value}',
1611 flags='python-brace-format',
1612 )
1613 """"""
1614 SETTING_CONFIG_STRUCTURE_CONTENTS_EMPTY_DIRECTORY = commented(
1615 'This message is emitted by the vault configuration exporter '
1616 'for "storeroom"-type configuration directories, '
1617 'while "assembling" the items stored in the configuration '
1618 """according to the item's "path". """
1619 'We confirm that we set up a currently empty directory '
1620 'at the given path.',
1621 )(
1622 'Debug message',
1623 'Setting contents (empty directory): {path}',
1624 flags='python-brace-format',
1625 )
1626 """"""
1627 VAULT_NATIVE_CHECKING_MAC_DETAILS = commented(
1628 'This message is emitted by the vault configuration exporter '
1629 'for "native"-type configuration directories. '
1630 'It is preceded by the info message '
1631 'VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC; see the commentary there '
1632 'concerning the terms and thoughts on translating them.',
1633 )(
1634 'Debug message',
1635 """\
1636MAC details:
1638\b
1639 MAC input: {mac_input}
1640 Expected MAC: {mac}
1641""",
1642 flags='python-brace-format',
1643 )
1644 """"""
1645 VAULT_NATIVE_EVP_BYTESTOKEY_INIT = commented(
1646 'This message is emitted by the vault configuration exporter '
1647 'for "native"-type configuration directories: '
1648 'in v0.2, the non-standard and deprecated "EVP_bytestokey" function '
1649 'from OpenSSL must be reimplemented from scratch. '
1650 'The terms "salt" and "IV" (initialization vector) '
1651 'are cryptographic terms.',
1652 )(
1653 'Debug message',
1654 """\
1655evp_bytestokey_md5 (initialization):
1657\b
1658 Input: {data}
1659 Salt: {salt}
1660 Key size: {key_size}
1661 IV size: {iv_size}
1662 Buffer length: {buffer_length}
1663 Buffer: {buffer}
1664""",
1665 flags='python-brace-format',
1666 )
1667 """"""
1668 VAULT_NATIVE_EVP_BYTESTOKEY_RESULT = commented(
1669 'This message is emitted by the vault configuration exporter '
1670 'for "native"-type configuration directories: '
1671 'in v0.2, the non-standard and deprecated "EVP_bytestokey" function '
1672 'from OpenSSL must be reimplemented from scratch. '
1673 'The terms "salt" and "IV" (initialization vector) '
1674 'are cryptographic terms.'
1675 'This function reports on the final results.',
1676 )(
1677 'Debug message',
1678 """\
1679evp_bytestokey_md5 (result):
1681\b
1682 Encryption key: {enc_key}
1683 IV: {iv}
1684""",
1685 flags='python-brace-format',
1686 )
1687 """"""
1688 VAULT_NATIVE_EVP_BYTESTOKEY_ROUND = commented(
1689 'This message is emitted by the vault configuration exporter '
1690 'for "native"-type configuration directories: '
1691 'in v0.2, the non-standard and deprecated "EVP_bytestokey" function '
1692 'from OpenSSL must be reimplemented from scratch. '
1693 'The terms "salt" and "IV" (initialization vector) '
1694 'are cryptographic terms.'
1695 'This function reports on the updated buffer length and contents '
1696 'after executing one round of hashing.',
1697 )(
1698 'Debug message',
1699 """\
1700evp_bytestokey_md5 (round update):
1702\b
1703 Buffer length: {buffer_length}
1704 Buffer: {buffer}
1705""",
1706 flags='python-brace-format',
1707 )
1708 """"""
1709 VAULT_NATIVE_PADDED_PLAINTEXT = commented(
1710 'This message is emitted by the vault configuration exporter '
1711 'for "native"-type configuration directories. '
1712 '"padding" and "plaintext" are cryptographic terms.',
1713 )(
1714 'Debug message',
1715 'Padded plaintext: {contents}',
1716 flags='python-brace-format',
1717 )
1718 """"""
1719 VAULT_NATIVE_PARSE_BUFFER = commented(
1720 'This message is emitted by the vault configuration exporter '
1721 'for "native"-type configuration directories. '
1722 'It is preceded by the info message '
1723 'VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC; see the commentary there '
1724 'concerning the terms and thoughts on translating them.',
1725 )(
1726 'Debug message',
1727 """\
1728Buffer: {contents}
1730\b
1731 IV: {iv}
1732 Payload (ciphertext): {payload}
1733 MAC: {mac}
1734""",
1735 flags='python-brace-format',
1736 )
1737 """"""
1738 VAULT_NATIVE_PBKDF2_CALL = commented(
1739 '',
1740 )(
1741 'Debug message',
1742 """\
1743Master key derivation:
1745\b
1746 PBKDF2 call: PBKDF2-HMAC(password={password!r}, salt={salt!r}, iterations={iterations!r}, key_size={key_size!r}, algorithm={algorithm!r})
1747 Result (binary): {raw_result}
1748 Result (hex key): {result_key!r}
1749""", # noqa: E501
1750 flags='python-brace-format',
1751 )
1752 """"""
1753 VAULT_NATIVE_PLAINTEXT = commented(
1754 'This message is emitted by the vault configuration exporter '
1755 'for "native"-type configuration directories. '
1756 '"plaintext" is a cryptographic term.',
1757 )(
1758 'Debug message',
1759 'Plaintext: {contents}',
1760 flags='python-brace-format',
1761 )
1762 """"""
1763 VAULT_NATIVE_V02_PAYLOAD_MAC_POSTPROCESSING = commented(
1764 'This message is emitted by the vault configuration exporter '
1765 'for "native"-type configuration directories. '
1766 'It is preceded by the info message '
1767 'VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC and the debug message '
1768 'PARSING_NATIVE_PARSE_BUFFER; see the commentary there concerning '
1769 'the terms and thoughts on translating them.',
1770 )(
1771 'Debug message',
1772 """\
1773Postprocessing buffer (v0.2):
1775\b
1776 Payload: {payload} (decoded from base64)
1777 MAC: {mac} (decoded from hex)
1778""",
1779 flags='python-brace-format',
1780 )
1781 """"""
1784class InfoMsgTemplate(enum.Enum):
1785 """Info messages for the `derivepassphrase` command-line."""
1787 ASSEMBLING_CONFIG_STRUCTURE = commented(
1788 'This message is emitted by the vault configuration exporter '
1789 'for "storeroom"-type configuration directories. '
1790 'The system stores entries in different "buckets" of a hash table. '
1791 'After the respective items in the buckets have been decrypted, '
1792 'we then have a list of item paths plus contents to populate. '
1793 "This must be done in a certain order (we don't yet have an "
1794 'existing directory tree to rely on, but rather must '
1795 'build it on-the-fly), hence the term "assembling".',
1796 )(
1797 'Info message',
1798 'Assembling config structure.',
1799 )
1800 """"""
1801 CANNOT_LOAD_AS_VAULT_CONFIG = commented(
1802 '"fmt" is a string such as "v0.2" or "storeroom", '
1803 'indicating the format which we tried to load the '
1804 'vault configuration as.',
1805 )(
1806 'Info message',
1807 'Cannot load {path!r} as a {fmt} vault configuration.',
1808 flags='python-brace-format',
1809 )
1810 """"""
1811 CHECKING_CONFIG_STRUCTURE_CONSISTENCY = commented(
1812 'This message is emitted by the vault configuration exporter '
1813 'for "storeroom"-type configuration directories. '
1814 'Having "assembled" the configuration items according to '
1815 'their claimed paths and contents, we then check if the '
1816 'assembled structure is internally consistent.',
1817 )(
1818 'Info message',
1819 'Checking config structure consistency.',
1820 )
1821 """"""
1822 DECRYPTING_BUCKET = commented(
1823 'This message is emitted by the vault configuration exporter '
1824 'for "storeroom"-type configuration directories. '
1825 'The system stores entries in different "buckets" of a hash table. '
1826 'We parse the directory bucket by bucket. '
1827 'All buckets are numbered in hexadecimal, and typically there are '
1828 '32 buckets, so 2-digit hex numbers.',
1829 )(
1830 'Info message',
1831 'Decrypting bucket {bucket_number}.',
1832 flags='python-brace-format',
1833 )
1834 """"""
1835 PARSING_MASTER_KEYS_DATA = commented(
1836 'This message is emitted by the vault configuration exporter '
1837 'for "storeroom"-type configuration directories. '
1838 '`.keys` is a filename, from which data about the master keys '
1839 'for this configuration are loaded.',
1840 )(
1841 'Info message',
1842 'Parsing master keys data from `.keys`.',
1843 )
1844 """"""
1845 PIP_INSTALL_EXTRA = commented(
1846 'This message immediately follows an error message about '
1847 'a missing library that needs to be installed. '
1848 'The Python Package Index (PyPI) supports declaring sets of '
1849 'optional dependencies as "extras", so users installing from PyPI '
1850 'can request reinstallation with a named "extra" being enabled. '
1851 'This would then let the installer take care of the '
1852 'missing libraries automatically, '
1853 'hence this suggestion to PyPI users.',
1854 )(
1855 'Info message',
1856 'For users installing from PyPI, see the {extra_name!r} extra.',
1857 flags='python-brace-format',
1858 )
1859 """"""
1860 SUCCESSFULLY_MIGRATED = commented(
1861 'This info message immediately follows the '
1862 '"Using deprecated v0.1-style ..." deprecation warning.',
1863 )(
1864 'Info message',
1865 'Successfully migrated to {path!r}.',
1866 flags='python-brace-format',
1867 )
1868 """"""
1869 VAULT_NATIVE_CHECKING_MAC = commented(
1870 '',
1871 )(
1872 'Info message',
1873 'Checking MAC.',
1874 )
1875 """"""
1876 VAULT_NATIVE_DECRYPTING_CONTENTS = commented(
1877 '',
1878 )(
1879 'Info message',
1880 'Decrypting contents.',
1881 )
1882 """"""
1883 VAULT_NATIVE_DERIVING_KEYS = commented(
1884 '',
1885 )(
1886 'Info message',
1887 'Deriving an encryption and signing key.',
1888 )
1889 """"""
1890 VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC = commented(
1891 'This message is emitted by the vault configuration exporter '
1892 'for "native"-type configuration directories. '
1893 '"IV" means "initialization vector", and "MAC" means '
1894 '"message authentication code". '
1895 'They are specifically cryptographic terms, as is "payload". '
1896 'The acronyms "IV" and "MAC" are assumed to be well-known to the '
1897 'English target audience, or at least discoverable by them; '
1898 'they *are* asking for debug output, after all. '
1899 'Please use your judgement as to whether to translate these terms '
1900 'or not, expanded or not.',
1901 )(
1902 'Info message',
1903 'Parsing IV, payload and MAC from the file contents.',
1904 )
1905 """"""
1908class WarnMsgTemplate(enum.Enum):
1909 """Warning messages for the `derivepassphrase` command-line."""
1911 EDITING_NOTES_BUT_NOT_STORING_CONFIG = commented(
1912 '',
1913 )(
1914 'Warning message',
1915 'Specifying --notes without --config is ineffective. '
1916 'No notes will be edited.',
1917 )
1918 EMPTY_SERVICE_NOT_SUPPORTED = commented(
1919 '',
1920 )(
1921 'Warning message',
1922 'An empty {service_metavar} is not supported by vault(1). '
1923 'For compatibility, this will be treated as if '
1924 '{service_metavar} was not supplied, i.e., it will error out, '
1925 'or operate on global settings.',
1926 flags='python-brace-format',
1927 )
1928 """"""
1929 EMPTY_SERVICE_SETTINGS_INACCESSIBLE = commented(
1930 '',
1931 )(
1932 'Warning message',
1933 'An empty {service_metavar} is not supported by vault(1). '
1934 'The empty-string service settings will be inaccessible '
1935 'and ineffective. '
1936 'To ensure that vault(1) and {PROG_NAME} see the settings, ' # noqa: RUF027
1937 'move them into the "global" section.',
1938 flags='python-brace-format',
1939 )
1940 """"""
1941 FAILED_TO_MIGRATE_CONFIG = commented(
1942 '"error" is supplied by the operating system (errno/strerror).',
1943 )(
1944 'Warning message',
1945 'Failed to migrate to {path!r}: {error}: {filename!r}.',
1946 flags='python-brace-format',
1947 )
1948 """"""
1949 GLOBAL_PASSPHRASE_INEFFECTIVE = commented(
1950 '',
1951 )(
1952 'Warning message',
1953 'Setting a global passphrase is ineffective '
1954 'because a key is also set.',
1955 )
1956 """"""
1957 LEGACY_EDITOR_INTERFACE_NOTES_BACKUP = commented(
1958 '',
1959 )(
1960 'Warning message',
1961 'A backup copy of the old notes was saved to {filename!r}. '
1962 'This is a safeguard against editing mistakes, because the '
1963 'vault(1)-compatible legacy editor interface does not allow '
1964 'aborting mid-edit, and because the notes were actually changed.',
1965 flags='python-brace-format',
1966 )
1967 NO_AF_UNIX = commented(
1968 '',
1969 )(
1970 'Warning message',
1971 'Cannot connect to an SSH agent via UNIX domain sockets '
1972 'because this Python version does not support them.',
1973 )
1974 """"""
1975 NO_ANNOYING_OS_NAMED_PIPES = commented(
1976 '',
1977 )(
1978 'Warning message',
1979 'Cannot connect to an SSH agent via Windows named pipes '
1980 'because this Python version does not support them.',
1981 )
1982 """"""
1983 PASSPHRASE_NOT_NORMALIZED = commented(
1984 'The key is a (vault) configuration key, in JSONPath syntax, '
1985 'typically "$.global" for the global passphrase or '
1986 '"$.services.service_name" or "$.services["service with spaces"]" '
1987 'for the services "service_name" and "service with spaces", '
1988 'respectively. '
1989 'Alternatively, it may be the value of '
1990 'Label.SETTINGS_ORIGIN_INTERACTIVE if the passphrase was '
1991 'entered interactively. '
1992 'The form is one of the four Unicode normalization forms: '
1993 'NFC, NFD, NFKC, NFKD. '
1994 'The asterisks are not special. '
1995 'Please feel free to substitute any other appropriate way to '
1996 'mark up emphasis of the word "displays".',
1997 )(
1998 'Warning message',
1999 'The {key} passphrase is not {form}-normalized. '
2000 'Its serialization as a byte string may not be what you '
2001 'expect it to be, even if it *displays* correctly. '
2002 'Please make sure to double-check any derived passphrases '
2003 'for unexpected results.',
2004 flags='python-brace-format',
2005 )
2006 """"""
2007 SERVICE_NAME_INCOMPLETABLE = commented(
2008 '',
2009 )(
2010 'Warning message',
2011 'The service name {service!r} contains an ASCII control character, '
2012 'which is not supported by our shell completion code. '
2013 'This service name will therefore not be available for completion '
2014 'on the command-line. '
2015 'You may of course still type it in manually in whatever format '
2016 'your shell accepts, but we highly recommend choosing a different '
2017 'service name instead.',
2018 flags='python-brace-format',
2019 )
2020 """"""
2021 SERVICE_PASSPHRASE_INEFFECTIVE = commented(
2022 'The key that is set need not necessarily be set at the '
2023 'service level; it may be a global key as well.',
2024 )(
2025 'Warning message',
2026 'Setting a service passphrase is ineffective '
2027 'because a key is also set: {service}.',
2028 flags='python-brace-format',
2029 )
2030 """"""
2031 STEP_REMOVE_INEFFECTIVE_VALUE = commented(
2032 '',
2033 )(
2034 'Warning message',
2035 'Removing ineffective setting {path} = {old}.',
2036 flags='python-brace-format',
2037 )
2038 """"""
2039 STEP_REPLACE_INVALID_VALUE = commented(
2040 '',
2041 )(
2042 'Warning message',
2043 'Replacing invalid value {old} for key {path} with {new}.',
2044 flags='python-brace-format',
2045 )
2046 """"""
2047 V01_STYLE_CONFIG = commented(
2048 '',
2049 )(
2050 'Warning message :: Deprecation',
2051 'Using deprecated v0.1-style config file {old!r}, '
2052 'instead of v0.2-style {new!r}. '
2053 'Support for v0.1-style config filenames will be removed in v1.0.',
2054 flags='python-brace-format',
2055 )
2056 """"""
2057 V10_SUBCOMMAND_REQUIRED = commented(
2058 'This deprecation warning may be issued at any level, '
2059 'i.e. we may actually be talking about subcommands, '
2060 'or sub-subcommands, or sub-sub-subcommands, etc., '
2061 'which is what the "here" is supposed to indicate.',
2062 )(
2063 'Warning message :: Deprecation',
2064 'A subcommand will be required here in v1.0. '
2065 'See --help for available subcommands. '
2066 'Defaulting to subcommand "vault".',
2067 )
2068 """"""
2071class ErrMsgTemplate(enum.Enum):
2072 """Error messages for the `derivepassphrase` command-line."""
2074 AGENT_REFUSED_LIST_KEYS = commented(
2075 '"loaded keys" being keys loaded into the agent.',
2076 )(
2077 'Error message',
2078 'The SSH agent failed to or refused to supply a list of loaded keys.',
2079 )
2080 """"""
2081 AGENT_REFUSED_SIGNATURE = commented(
2082 'The message to be signed is the vault UUID, '
2083 "but there's no space to explain that here, "
2084 'so ideally the error message does not go into detail.',
2085 )(
2086 'Error message',
2087 'The SSH agent failed to or refused to issue a signature '
2088 'with the selected key, necessary for deriving a service passphrase.',
2089 )
2090 """"""
2091 CANNOT_CONNECT_TO_AGENT = commented(
2092 '"error" is supplied by the operating system (errno/strerror).',
2093 )(
2094 'Error message',
2095 'Cannot connect to the SSH agent: {error}: {filename!r}.',
2096 flags='python-brace-format',
2097 )
2098 """"""
2099 CANNOT_DECODEIMPORT_VAULT_SETTINGS = commented(
2100 '"error" is supplied by the operating system (errno/strerror).',
2101 )(
2102 'Error message',
2103 'Cannot import vault settings: cannot decode JSON: {error}.',
2104 flags='python-brace-format',
2105 )
2106 """"""
2107 CANNOT_EXPORT_VAULT_SETTINGS = commented(
2108 '"error" is supplied by the operating system (errno/strerror).',
2109 )(
2110 'Error message',
2111 'Cannot export vault settings: {error}: {filename!r}.',
2112 flags='python-brace-format',
2113 )
2114 """"""
2115 CANNOT_IMPORT_VAULT_SETTINGS = commented(
2116 '"error" is supplied by the operating system (errno/strerror).',
2117 )(
2118 'Error message',
2119 'Cannot import vault settings: {error}: {filename!r}.',
2120 flags='python-brace-format',
2121 )
2122 """"""
2123 CANNOT_LOAD_USER_CONFIG = commented(
2124 '"error" is supplied by the operating system (errno/strerror).',
2125 )(
2126 'Error message',
2127 'Cannot load user config: {error}: {filename!r}.',
2128 flags='python-brace-format',
2129 )
2130 """"""
2131 CANNOT_LOAD_VAULT_SETTINGS = commented(
2132 '"error" is supplied by the operating system (errno/strerror).',
2133 )(
2134 'Error message',
2135 'Cannot load vault settings: {error}: {filename!r}.',
2136 flags='python-brace-format',
2137 )
2138 """"""
2139 CANNOT_PARSE_AS_VAULT_CONFIG = commented(
2140 'Unlike the "Cannot load {path!r} as a {fmt} '
2141 'vault configuration." message, *this* error message is emitted '
2142 'when we have tried loading the path in each of our '
2143 'supported formats, and failed. '
2144 'The user will thus see the above "Cannot load ..." warning message '
2145 'potentially multiple times, '
2146 'and this error message at the very bottom.',
2147 )(
2148 'Error message',
2149 'Cannot parse {path!r} as a valid vault-native '
2150 'configuration file/directory.',
2151 flags='python-brace-format',
2152 )
2153 """"""
2154 CANNOT_PARSE_AS_VAULT_CONFIG_OSERROR = commented(
2155 '"error" is supplied by the operating system (errno/strerror).',
2156 )(
2157 'Error message',
2158 r'Cannot parse {path!r} as a valid vault-native '
2159 'configuration file/directory: {error}: {filename!r}.',
2160 flags='python-brace-format',
2161 )
2162 """"""
2163 CANNOT_STORE_VAULT_SETTINGS = commented(
2164 '"error" is supplied by the operating system (errno/strerror).',
2165 )(
2166 'Error message',
2167 'Cannot store vault settings: {error}: {filename!r}.',
2168 flags='python-brace-format',
2169 )
2170 """"""
2171 CANNOT_UNDERSTAND_AGENT = commented(
2172 'This error message is used whenever we cannot make '
2173 'any sense of a response from the SSH agent '
2174 'because the response is ill-formed '
2175 '(truncated, improperly encoded, etc.) '
2176 'or otherwise violates the communications protocol. '
2177 'Well-formed responses that adhere to the protocol, '
2178 'even if they indicate that the requested operation failed, '
2179 'are handled with a different error message.',
2180 )(
2181 'Error message',
2182 "Cannot understand the SSH agent's response because it "
2183 'violates the communication protocol.',
2184 )
2185 """"""
2186 CANNOT_UPDATE_SETTINGS_NO_SETTINGS = commented(
2187 'The settings_type metavar contains translations for '
2188 'either "global settings" or "service-specific settings"; '
2189 'see the CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL and '
2190 'CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE entries. '
2191 'The first sentence will thus read either '
2192 '"Cannot update the global settings without any given settings." or '
2193 '"Cannot update the service-specific settings without any '
2194 'given settings.". '
2195 'You may update this entry, and the two metavar entries, '
2196 'in any way you see fit that achieves the desired translations '
2197 'of the first sentence.',
2198 )(
2199 'Error message',
2200 'Cannot update the {settings_type} without any given settings. '
2201 'You must specify at least one of --lower, ..., --symbol, --notes, '
2202 'or --phrase or --key.',
2203 flags='python-brace-format',
2204 )
2205 """"""
2206 INVALID_USER_CONFIG = commented(
2207 '"error" is supplied by the operating system (errno/strerror).',
2208 )(
2209 'Error message',
2210 'The user configuration file is invalid. {error}: {filename!r}.',
2211 flags='python-brace-format',
2212 )
2213 """"""
2214 INVALID_VAULT_CONFIG = commented(
2215 'This error message is a reaction to a validator function '
2216 'saying *that* the configuration is not valid, '
2217 'but not *how* it is not valid. '
2218 'The configuration file is principally parsable, however.',
2219 )(
2220 'Error message',
2221 'Invalid vault config: {config!r}.',
2222 flags='python-brace-format',
2223 )
2224 """"""
2225 MISSING_MODULE = commented(
2226 '',
2227 )(
2228 'Error message',
2229 'Cannot load the required Python module {module!r}.',
2230 flags='python-brace-format',
2231 )
2232 """"""
2233 NO_AGENT_SUPPORT = commented(
2234 '',
2235 )(
2236 'Error message',
2237 'Cannot connect to an SSH agent because this Python version '
2238 'does not support communicating with it.',
2239 )
2240 """"""
2241 NO_KEY_OR_PHRASE = commented(
2242 '',
2243 )(
2244 'Error message',
2245 'No passphrase or key was given in the configuration. '
2246 'In this case, the --phrase or --key argument is required.',
2247 )
2248 """"""
2249 NO_SSH_AGENT_FOUND = commented(
2250 '',
2251 )(
2252 'Error message',
2253 'Cannot find any running SSH agent because SSH_AUTH_SOCK is not set.',
2254 )
2255 """"""
2256 NO_SUITABLE_SSH_KEYS = commented(
2257 '',
2258 )(
2259 'Error message',
2260 'The SSH agent contains no keys suitable for {PROG_NAME}.', # noqa: RUF027
2261 flags='python-brace-format',
2262 )
2263 """"""
2264 PARAMS_MUTUALLY_EXCLUSIVE = commented(
2265 'The params are long-form command-line option names. '
2266 'Typical example: "--key is mutually exclusive with --phrase."',
2267 )(
2268 'Error message',
2269 '{param1} is mutually exclusive with {param2}.',
2270 flags='python-brace-format',
2271 )
2272 """"""
2273 PARAMS_NEEDS_SERVICE = commented(
2274 'The param is a long-form command-line option name, '
2275 'the metavar is Label.VAULT_METAVAR_SERVICE.',
2276 )(
2277 'Error message',
2278 '{param} requires a {service_metavar}.',
2279 flags='python-brace-format',
2280 )
2281 """"""
2282 PARAMS_NEEDS_SERVICE_OR_CONFIG = commented(
2283 'The param is a long-form command-line option name, '
2284 'the metavar is Label.VAULT_METAVAR_SERVICE.',
2285 )(
2286 'Error message',
2287 '{param} requires a {service_metavar} or --config.',
2288 flags='python-brace-format',
2289 )
2290 """"""
2291 PARAMS_NO_SERVICE = commented(
2292 'The param is a long-form command-line option name, '
2293 'the metavar is Label.VAULT_METAVAR_SERVICE.',
2294 )(
2295 'Error message',
2296 '{param} does not take a {service_metavar} argument.',
2297 flags='python-brace-format',
2298 )
2299 """"""
2300 SERVICE_REQUIRED = commented(
2301 'The metavar is Label.VAULT_METAVAR_SERVICE.',
2302 )(
2303 'Error message',
2304 'Deriving a passphrase requires a {service_metavar}.',
2305 flags='python-brace-format',
2306 )
2307 """"""
2308 SET_AND_UNSET_SAME_SETTING = commented(
2309 'The rephrasing '
2310 '"Attempted to unset and set the same setting '
2311 '(--unset={setting} --{setting}=...) at the same time."'
2312 'may or may not be more suitable as a basis for translation instead.',
2313 )(
2314 'Error message',
2315 'Attempted to unset and set --{setting} at the same time.',
2316 flags='python-brace-format',
2317 )
2318 """"""
2319 SSH_KEY_NOT_LOADED = commented(
2320 '',
2321 )(
2322 'Error message',
2323 'The requested SSH key is not loaded into the agent.',
2324 )
2325 """"""
2326 USER_ABORTED_EDIT = commented(
2327 'The user requested to edit the notes for a service, '
2328 'but aborted the request mid-editing.',
2329 )(
2330 'Error message',
2331 'Not saving any new notes: the user aborted the request.',
2332 )
2333 """"""
2334 USER_ABORTED_PASSPHRASE = commented(
2335 'The user was prompted for a master passphrase, '
2336 'but aborted the request.',
2337 )(
2338 'Error message',
2339 'No passphrase was given; the user aborted the request.',
2340 )
2341 """"""
2342 USER_ABORTED_SSH_KEY_SELECTION = commented(
2343 'The user was prompted to select a master SSH key, '
2344 'but aborted the request.',
2345 )(
2346 'Error message',
2347 'No SSH key was selected; the user aborted the request.',
2348 )
2349 """"""
2352MsgTemplate: TypeAlias = Union[
2353 Label,
2354 DebugMsgTemplate,
2355 InfoMsgTemplate,
2356 WarnMsgTemplate,
2357 ErrMsgTemplate,
2358]
2359"""A type alias for all enums containing translatable strings as values."""
2360MSG_TEMPLATE_CLASSES = (
2361 Label,
2362 DebugMsgTemplate,
2363 InfoMsgTemplate,
2364 WarnMsgTemplate,
2365 ErrMsgTemplate,
2366)
2367"""A collection all enums containing translatable strings as values."""
2369DebugTranslations._load_cache() # noqa: SLF001
2372def _write_po_file( # noqa: C901,PLR0912
2373 fileobj: TextIO,
2374 /,
2375 *,
2376 is_template: bool = True,
2377 version: str = VERSION,
2378 build_time: datetime.datetime | None = None,
2379) -> None: # pragma: no cover
2380 r"""Write a .po file to the given file object.
2382 Assumes the file object is opened for writing and accepts string
2383 inputs. The file will *not* be closed when writing is complete.
2384 The file *must* be opened in UTF-8 encoding, lest the file will
2385 declare an incorrect encoding.
2387 This function crucially depends on all translatable strings
2388 appearing in the enums of this module. Certain parts of the
2389 .po header are hard-coded, as is the source filename.
2391 """ # noqa: DOC501
2392 # Only used interactively, so no coverage.
2393 entries: dict[str, dict[str, MsgTemplate]] = {}
2394 for enum_class in MSG_TEMPLATE_CLASSES:
2395 for member in enum_class.__members__.values():
2396 value = cast('TranslatableString', member.value)
2397 ctx = value.l10n_context
2398 msg = value.singular
2399 if (
2400 msg in entries.setdefault(ctx, {})
2401 and entries[ctx][msg] != member
2402 ):
2403 raise AssertionError( # noqa: TRY003
2404 f'Duplicate entry for ({ctx!r}, {msg!r}): ' # noqa: EM102
2405 f'{entries[ctx][msg]!r} and {member!r}'
2406 )
2407 entries[ctx][msg] = member
2408 if build_time is None and os.environ.get('SOURCE_DATE_EPOCH'):
2409 try:
2410 source_date_epoch = int(os.environ['SOURCE_DATE_EPOCH'])
2411 except ValueError as exc:
2412 err_msg = 'Cannot parse SOURCE_DATE_EPOCH'
2413 raise RuntimeError(err_msg) from exc
2414 else:
2415 build_time = datetime.datetime.fromtimestamp(
2416 source_date_epoch,
2417 tz=datetime.timezone.utc,
2418 )
2419 elif build_time is None:
2420 build_time = datetime.datetime.now().astimezone()
2421 if is_template:
2422 header = (
2423 inspect.cleandoc(rf"""
2424 # English translation for {PROG_NAME}.
2425 # Copyright (C) {build_time.strftime('%Y')} AUTHOR
2426 # This file is distributed under the same license as {PROG_NAME}.
2427 # AUTHOR <someone@example.com>, {build_time.strftime('%Y')}.
2428 #
2429 msgid ""
2430 msgstr ""
2431 """).removesuffix('\n')
2432 + '\n'
2433 )
2434 else:
2435 header = (
2436 inspect.cleandoc(rf"""
2437 # English debug translation for {PROG_NAME}.
2438 # Copyright (C) {build_time.strftime('%Y')} {AUTHOR}
2439 # This file is distributed under the same license as {PROG_NAME}.
2440 #
2441 msgid ""
2442 msgstr ""
2443 """).removesuffix('\n')
2444 + '\n'
2445 )
2446 fileobj.write(header)
2447 po_info = {
2448 'Project-Id-Version': f'{PROG_NAME} {version}',
2449 'Report-Msgid-Bugs-To': 'software@the13thletter.info',
2450 'PO-Revision-Date': build_time.strftime('%Y-%m-%d %H:%M%z'),
2451 'MIME-Version': '1.0',
2452 'Content-Type': 'text/plain; charset=UTF-8',
2453 'Content-Transfer-Encoding': '8bit',
2454 'Plural-Forms': 'nplurals=2; plural=(n != 1);',
2455 }
2456 if is_template:
2457 po_info.update({
2458 'POT-Creation-Date': build_time.strftime('%Y-%m-%d %H:%M%z'),
2459 'Last-Translator': 'AUTHOR <someone@example.com>',
2460 'Language': 'en',
2461 'Language-Team': 'English',
2462 })
2463 else:
2464 po_info.update({
2465 'Last-Translator': AUTHOR,
2466 'Language': 'en_US@DEBUG',
2467 'Language-Team': 'English',
2468 })
2469 print(*_format_po_info(po_info), sep='\n', end='\n', file=fileobj)
2471 context_class = {
2472 'Label': Label,
2473 'Debug message': DebugMsgTemplate,
2474 'Info message': InfoMsgTemplate,
2475 'Warning message': WarnMsgTemplate,
2476 'Error message': ErrMsgTemplate,
2477 }
2479 def _sort_position_msg_template_class(
2480 item: tuple[str, Any],
2481 /,
2482 ) -> tuple[int, str]:
2483 context_type = item[0].split(' :: ')[0]
2484 return (
2485 MSG_TEMPLATE_CLASSES.index(context_class[context_type]),
2486 item[0],
2487 )
2489 for _ctx, subdict in sorted(
2490 entries.items(), key=_sort_position_msg_template_class
2491 ):
2492 for _msg, enum_value in sorted(
2493 subdict.items(), key=lambda kv: str(kv[1])
2494 ):
2495 value = cast('TranslatableString', enum_value.value)
2496 value2 = value.maybe_without_filename()
2497 fileobj.writelines(
2498 _format_po_entry(
2499 enum_value, is_debug_translation=not is_template
2500 )
2501 )
2502 if value != value2:
2503 fileobj.writelines(
2504 _format_po_entry(
2505 enum_value,
2506 is_debug_translation=not is_template,
2507 transformed_string=value2,
2508 )
2509 )
2512def _format_po_info(
2513 data: Mapping[str, Any],
2514 /,
2515) -> Iterator[str]: # pragma: no cover
2516 """""" # noqa: D419
2517 # Internal use only, so no coverage.
2518 sortorder = [
2519 'project-id-version',
2520 'report-msgid-bugs-to',
2521 'pot-creation-date',
2522 'po-revision-date',
2523 'last-translator',
2524 'language',
2525 'language-team',
2526 'mime-version',
2527 'content-type',
2528 'content-transfer-encoding',
2529 'plural-forms',
2530 ]
2532 def _sort_position(s: str, /) -> int:
2533 n = len(sortorder)
2534 for i, x in enumerate(sortorder):
2535 if s.lower().rstrip(':') == x:
2536 return i
2537 return n
2539 for key in sorted(data.keys(), key=_sort_position):
2540 value = data[key]
2541 line = f'{key}: {value}\n'
2542 yield _cstr(line)
2545def _format_po_entry(
2546 enum_value: MsgTemplate,
2547 /,
2548 *,
2549 is_debug_translation: bool = False,
2550 transformed_string: TranslatableString | None = None,
2551) -> tuple[str, ...]: # pragma: no cover
2552 """""" # noqa: D419
2553 # Internal use only, so no coverage.
2554 ret: list[str] = ['\n']
2555 ts = transformed_string or cast('TranslatableString', enum_value.value)
2556 if ts.translator_comments:
2557 comments = ts.translator_comments.splitlines(False) # noqa: FBT003
2558 comments.extend(['', f'Message-ID: {enum_value}'])
2559 else:
2560 comments = [f'TRANSLATORS: Message-ID: {enum_value}']
2561 ret.extend(f'#. {line}\n' for line in comments)
2562 if ts.flags:
2563 ret.append(f'#, {", ".join(sorted(ts.flags))}\n')
2564 if ts.l10n_context:
2565 ret.append(f'msgctxt {_cstr(ts.l10n_context)}\n')
2566 ret.append(f'msgid {_cstr(ts.singular)}\n')
2567 if ts.plural:
2568 ret.append(f'msgid_plural {_cstr(ts.plural)}\n')
2569 value = (
2570 DebugTranslations().pgettext(ts.l10n_context, ts.singular)
2571 if is_debug_translation
2572 else ''
2573 )
2574 ret.append(f'msgstr {_cstr(value)}\n')
2575 return tuple(ret)
2578def _cstr(s: str) -> str: # pragma: no cover
2579 """""" # noqa: D419
2581 # Internal use only, so no coverage.
2582 def escape(string: str) -> str:
2583 return string.translate({
2584 0: r'\000',
2585 1: r'\001',
2586 2: r'\002',
2587 3: r'\003',
2588 4: r'\004',
2589 5: r'\005',
2590 6: r'\006',
2591 7: r'\007',
2592 8: r'\b',
2593 9: r'\t',
2594 10: r'\n',
2595 11: r'\013',
2596 12: r'\f',
2597 13: r'\r',
2598 14: r'\016',
2599 15: r'\017',
2600 ord('"'): r'\"',
2601 ord('\\'): r'\\',
2602 127: r'\177',
2603 })
2605 return '\n'.join(
2606 f'"{escape(line)}"'
2607 for line in s.splitlines(True) or [''] # noqa: FBT003
2608 )
2611if __name__ == '__main__':
2612 import argparse
2614 def validate_build_time(value: str | None) -> datetime.datetime | None:
2615 if value is None:
2616 return None
2617 ret = datetime.datetime.fromisoformat(value)
2618 if ret.isoformat(sep=' ', timespec='seconds') != value:
2619 raise ValueError(f'invalid time specification: {value}') # noqa: EM102,TRY003
2620 return ret
2622 ap = argparse.ArgumentParser()
2623 ex = ap.add_mutually_exclusive_group()
2624 ex.add_argument(
2625 '--template',
2626 action='store_true',
2627 dest='is_template',
2628 default=True,
2629 help='Generate a template file (default)',
2630 )
2631 ex.add_argument(
2632 '--debug-translation',
2633 action='store_false',
2634 dest='is_template',
2635 default=True,
2636 help='Generate a "debug" translation file',
2637 )
2638 ap.add_argument(
2639 '--set-version',
2640 action='store',
2641 dest='version',
2642 default=VERSION,
2643 help='Override declared software version',
2644 )
2645 ap.add_argument(
2646 '--set-build-time',
2647 action='store',
2648 dest='build_time',
2649 default=None,
2650 type=validate_build_time,
2651 help='Override the time of build (YYYY-MM-DD HH:MM:SS+HH:MM format, '
2652 'default: $SOURCE_DATE_EPOCH, or the current time)',
2653 )
2654 args = ap.parse_args()
2655 _write_po_file(
2656 sys.stdout,
2657 version=args.version,
2658 is_template=args.is_template,
2659 build_time=args.build_time,
2660 )