Coverage for src\derivepassphrase\_internals\cli_messages.py: 100.000%

1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>

2#

3# SPDX-Licence-Identifier: MIT

4

5"""Messages for the command-line interface of `derivepassphrase`.

6

7 Also contains some machinery related to internationalization and

8 localization.

9

10!!! warning

11

12 Non-public module (implementation detail), provided for didactical and

13 educational purposes only. Subject to change without notice, including

14 removal.

15

16"""

17

18 from __future__ import annotations (empty)

19

20 import contextlib (empty)

21 import datetime (empty)

22 import enum (empty)

23 import functools (empty)

24 import gettext (empty)

25 import inspect (empty)

26 import os (empty)

27 import pathlib (empty)

28 import string (empty)

29 import sys (empty)

30 import textwrap (empty)

31 import types (empty)

32 from typing import TYPE_CHECKING, NamedTuple, Protocol, TextIO, Union, cast (empty)

33

34 from typing_extensions import TypeAlias, override (empty)

35

36 from derivepassphrase import _internals (empty)

37

38 if TYPE_CHECKING: (empty)

39 from collections.abc import Iterable, Iterator, Mapping, Sequence

40

41 from typing_extensions import Any, Self

42

43 __all__ = ('PROG_NAME',) (empty)

44

45 PROG_NAME = _internals.PROG_NAME (empty)

46 VERSION = _internals.VERSION (empty)

47 AUTHOR = _internals.AUTHOR (empty)

48

49

50 def load_translations( (empty)

51 localedirs: list[str | bytes | os.PathLike] | None = None, (empty)

52 languages: Sequence[str] | None = None, (empty)

53 class_: type[gettext.NullTranslations] | None = None, (empty)

54) -> gettext.NullTranslations: # pragma: no cover (empty)

55 """Load a translation catalog for derivepassphrase.

56

57 Runs [`gettext.translation`][] under the hood for multiple locale

58 directories. `fallback=True` is implied.

59

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`][].

70

71 Returns:

72 A (potentially dummy) translation catalog.

73

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.

79

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: (empty)

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'): (empty)

94 xdg_data_home = ( (empty)

95 pathlib.Path(os.environ['APPDATA']) (empty)

96 if os.environ.get('APPDATA') (empty)

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 = [ (empty)

106 pathlib.Path(xdg_data_home, 'locale'), (empty)

107 pathlib.Path(sys.prefix, 'share', 'locale'), (empty)

108 pathlib.Path(sys.base_prefix, 'share', 'locale'), (empty)

109 ]

110 for localedir in localedirs: (empty)

111 with contextlib.suppress(OSError): (empty)

112 return gettext.translation( (empty)

113 PROG_NAME, (empty)

114 localedir=os.fsdecode(localedir), (empty)

115 languages=languages, (empty)

116 class_=class_, (empty)

117 )

118 return gettext.NullTranslations() (empty)

119

120

121 translation = load_translations() (empty)

122 _debug_translation_message_cache: dict[ (empty)

123 tuple[str, str],

124 tuple[MsgTemplate, frozenset],

125] = {}

126

127

128 class DebugTranslations(gettext.NullTranslations): (empty)

129 """A debug object indicating which known message is being requested.

130

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.

134

135 """

136

137 @staticmethod (empty)

138 def _load_cache() -> None: (empty)

139 cache = _debug_translation_message_cache (empty)

140 for enum_class in MSG_TEMPLATE_CLASSES: (empty)

141 for member in enum_class.__members__.values(): (empty)

142 value = cast('TranslatableString', member.value) (empty)

143 queue: list[tuple[TranslatableString, frozenset[str]]] = [ (empty)

144 (value, frozenset())

145 ]

146 value2 = value.maybe_without_filename() (empty)

147 if value != value2: (empty)

148 queue.append((value2, frozenset({'filename'}))) (empty)

149 for v, trimmed in queue: (empty)

150 singular = v.singular (empty)

151 plural = v.plural (empty)

152 context = v.l10n_context (empty)

153 cache.setdefault((context, singular), (member, trimmed)) (empty)

154 # Currently no translatable messages use plural forms

155 if plural: # pragma: no cover (empty)

156 cache.setdefault((context, plural), (member, trimmed))

157

158 @classmethod (empty)

159 def _locate_message( (empty)

160 cls,

161 message: str,

162 /,

163 *,

164 context: str = '',

165 message_plural: str = '',

166 n: int = 1,

167 ) -> str:

168 try: 6 ctx 2qb} ~ x r b

169 enum_value, trimmed = _debug_translation_message_cache[ 6 ctx 2qb} ~ x r b

170 context, message

171 ]

172 except KeyError: 2 ctx 2qbx

173 return message if not message_plural or n == 1 else message_plural 2 ctx 2qbx

174 return cls._format_enum_name_maybe_with_fields( 4 ctx 1}~rb

175 enum_name=str(enum_value),

176 ts=cast('TranslatableString', enum_value.value),

177 trimmed=trimmed,

178 )

179

180 @staticmethod (empty)

181 def _format_enum_name_maybe_with_fields( (empty)

182 enum_name: str,

183 ts: TranslatableString,

184 trimmed: frozenset[str] = frozenset(),

185 ) -> str:

186 formatted_fields = [ 4 ctx 1}~rb

187 f'{f}=None' if f in trimmed else f'{f}={{{f}!r}}'

188 for f in ts.fields()

189 ]

190 return ( 4 ctx 1}~rb

191 '{}({})'.format(enum_name, ', '.join(formatted_fields))

192 if formatted_fields

193 else str(enum_name)

194 )

195

196 @override (empty)

197 def gettext( (empty)

198 self,

199 message: str,

200 /,

201 ) -> str:

202 return self._locate_message(message) 2 ctx 2qbx

203

204 @override (empty)

205 def ngettext( (empty)

206 self,

207 msgid1: str, (empty)

208 msgid2: str, (empty)

209 n: int, (empty)

210 /,

211 ) -> str: # pragma: no cover (empty)

212 """""" # noqa: D419

213 # Currently unused, so no coverage.

214 return self._locate_message(msgid1, message_plural=msgid2, n=n)

215

216 @override (empty)

217 def pgettext( (empty)

218 self,

219 context: str,

220 message: str,

221 /,

222 ) -> str:

223 return self._locate_message(message, context=context) 4 ctx 1}~rb

224

225 @override (empty)

226 def npgettext( (empty)

227 self,

228 context: str, (empty)

229 msgid1: str, (empty)

230 msgid2: str, (empty)

231 n: int, (empty)

232 /,

233 ) -> str: # pragma: no cover (empty)

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 )

242

243

244 class TranslatableString(NamedTuple): (empty)

245 """Translatable string as used by the `derivepassphrase` command-line.

246

247 For typing purposes.

248

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.

262

263 """

264

265 l10n_context: str (empty)

266 """""" (empty)

267 singular: str (empty)

268 """""" (empty)

269 plural: str = '' (empty)

270 """""" (empty)

271 flags: frozenset[str] = frozenset() (empty)

272 """""" (empty)

273 translator_comments: str = '' (empty)

274 """""" (empty)

275

276 def fields(self) -> list[str]: (empty)

277 """Return the replacement fields this template requires.

278

279 Raises:

280 NotImplementedError:

281 Replacement field discovery for %-formatting is not

282 implemented.

283

284 """

285 if 'python-format' in self.flags: # pragma: no cover 5 ctx 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 ( 5 ctx 1a}~rb

293 'no-python-brace-format' in self.flags

294 or 'python-brace-format' not in self.flags

295 ):

296 return [] 4 ctx 1a}~r

297 formatter = string.Formatter() 4 ctx 1a}~b

298 fields: dict[str, int] = {} 4 ctx 1a}~b

299 for _lit, field, _spec, _conv in formatter.parse(self.singular): 4 ctx 1a}~b

300 if field is not None and field not in fields: 4 ctx 1a}~b

301 fields[field] = len(fields) 4 ctx 1a}~b

302 return sorted(fields, key=fields.__getitem__) 4 ctx 1a}~b

303

304 @staticmethod (empty)

305 def _maybe_rewrap( (empty)

306 string: str,

307 /,

308 *,

309 fix_sentence_endings: bool = True,

310 ) -> str:

311 string = inspect.cleandoc(string) (empty)

312 if not any(s.strip() == '\b' for s in string.splitlines()): (empty)

313 string = '\n'.join( (empty)

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( (empty)

322 s

323 for s in string.splitlines(True) # noqa: FBT003

324 if s.strip() != '\b'

325 )

326 return string (empty)

327

328 def maybe_without_filename(self) -> Self: (empty)

329 """Return a new translatable string without the "filename" field.

330

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.

336

337 """

338 filename_str = ': {filename!r}' 22 ctx 1adefghijklmnopqstvuwbc

339 ret = self 22 ctx 1adefghijklmnopqstvuwbc

340 a, sep1, b = self.singular.partition(filename_str) 22 ctx 1adefghijklmnopqstvuwbc

341 c, sep2, d = self.plural.partition(filename_str) 22 ctx 1adefghijklmnopqstvuwbc

342 if sep1: 22 ctx 1adefghijklmnopqstvuwbc

343 ret = ret._replace(singular=(a + b)) 20 ctx 1adefghijklmnopqstubc

344 # Currently no translatable messages use plural forms

345 if sep2: # pragma: no cover 22 ctx 1adefghijklmnopqstvuwbc

346 ret = ret._replace(plural=(c + d))

347 return ret 22 ctx 1adefghijklmnopqstvuwbc

348

349 def rewrapped(self) -> Self: (empty)

350 """Return a rewrapped version of self.

351

352 Normalizes all parts assumed to contain English prose.

353

354 """

355 msg = self._maybe_rewrap(self.singular, fix_sentence_endings=True) (empty)

356 plural = self._maybe_rewrap(self.plural, fix_sentence_endings=True) (empty)

357 context = self.l10n_context.strip() (empty)

358 comments = self._maybe_rewrap( (empty)

359 self.translator_comments, fix_sentence_endings=False

360 )

361 return self._replace( (empty)

362 singular=msg,

363 plural=plural,

364 l10n_context=context,

365 translator_comments=comments,

366 )

367

368 def with_comments(self, comments: str, /) -> Self: (empty)

369 """Add or replace the string's translator comments.

370

371 The comments are assumed to contain English prose, and will be

372 normalized.

373

374 Returns:

375 A new [`TranslatableString`][] with the specified comments.

376

377 """

378 if comments.strip() and not comments.lstrip().startswith( (empty)

379 'TRANSLATORS:'

380 ): # pragma: no cover

381 # Currently unused, so no coverage.

382 comments = 'TRANSLATORS: ' + comments.lstrip() (empty)

383 comments = self._maybe_rewrap(comments, fix_sentence_endings=False) (empty)

384 return self._replace(translator_comments=comments) (empty)

385

386 def validate_flags(self, *extra_flags: str) -> Self: (empty)

387 """Add all flags, then validate them against the string.

388

389 Returns:

390 A new [`TranslatableString`][] with the extra flags added,

391 and all flags validated.

392

393 Raises:

394 ValueError:

395 The flags failed to validate. See the exact error

396 message for details.

397

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 ...

427

428 """

429 all_flags = frozenset(f.strip() for f in self.flags.union(extra_flags)) (empty)

430 if '{' in self.singular and not bool( (empty)

431 all_flags & {'python-brace-format', 'no-python-brace-format'}

432 ):

433 msg = ( (empty)

434 f'Missing flag for how to deal with brace character '

435 f'in {self.singular! r}'

436 )

437 raise ValueError(msg) (empty)

438 if '%' in self.singular and not bool( (empty)

439 all_flags & {'python-format', 'no-python-format'}

440 ):

441 msg = ( (empty)

442 f'Missing flag for how to deal with percent character '

443 f'in {self.singular! r}'

444 )

445 raise ValueError(msg) (empty)

446 if ( (empty)

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}' (empty)

452 raise ValueError(msg) (empty)

453 return self._replace(flags=all_flags) (empty)

454

455

456 def translatable( (empty)

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.

465

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`][].

469

470 """

471 flags = ( (empty)

472 frozenset(flags) if not isinstance(flags, str) else frozenset({flags})

473 )

474 return ( (empty)

475 TranslatableString(context, single, plural=plural, flags=flags)

476 .rewrapped()

477 .with_comments(comments)

478 .validate_flags()

479 )

480

481

482 class TranslatedString: (empty)

483 """A string object that stringifies to its translation.

484

485 The translation and replacement value rendering is only performed

486 when this string object is actually stringified.

487

488 """

489

490 def __init__( (empty)

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.

498

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`][].

510

511 """

512 if isinstance(template, MSG_TEMPLATE_CLASSES): 108 ctx 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) 104 ctx 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 108 ctx 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} 108 ctx 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 108 ctx 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 { |

517

518 def __bool__(self) -> bool: (empty)

519 """Return true if the rendered string is truthy."""

520 return bool(str(self)) 6 ctx 1BCDEFH

521

522 def __eq__(self, other: object) -> bool: # pragma: no cover (empty)

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 3 ctx 1rbc

526

527 def __hash__(self) -> int: # pragma: no cover (empty)

528 """Return the hash of the rendered string."""

529 # Only used interactively during debugging, so no coverage.

530 return hash(str(self)) 64 ctx 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

531

532 def __repr__(self) -> str: # pragma: no cover (empty)

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 )

539

540 def __str__(self) -> str: (empty)

541 """Return the rendered translation of this string.

542

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.

546

547 """

548 if self._rendered is None: 103 ctx 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 88 ctx 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): 88 ctx 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 = '' 6 ctx 2y z A abx {

552 template = self.template 6 ctx 2y z A abx {

553 else:

554 context = self.template.l10n_context 85 ctx 1aBCDEFG()*+H,IJKLMNOdPefghQRSijTUVWXYklZmnop0-.123qs456789!#$%'/:ytvuw;z=A?@[]^_`rbc|

555 template = self.template.singular 85 ctx 1aBCDEFG()*+H,IJKLMNOdPefghQRSijTUVWXYklZmnop0-.123qs456789!#$%'/:ytvuw;z=A?@[]^_`rbc|

556 do_escape = 'no-python-brace-format' in self.template.flags 85 ctx 1aBCDEFG()*+H,IJKLMNOdPefghQRSijTUVWXYklZmnop0-.123qs456789!#$%'/:ytvuw;z=A?@[]^_`rbc|

557 template = ( 88 ctx 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 88 ctx 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 = { 88 ctx 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) 88 ctx 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 103 ctx 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 { |

569

570 @staticmethod (empty)

571 def _escape(template: str) -> str: (empty)

572 return template.translate({ 3 ctx 1x{|

573 ord('{'): '{{',

574 ord('}'): '}}',

575 })

576

577 @classmethod (empty)

578 def constant(cls, template: str) -> Self: (empty)

579 return cls(cls._escape(template)) 2 ctx 1x{

580

581 def maybe_without_filename(self) -> Self: (empty)

582 """Return a new string without the "filename" field.

583

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.

588

589 """

590 new_template = ( 21 ctx 1defghijklmnopqstvuwbc

591 self.template.maybe_without_filename()

592 if not isinstance(self.template, str)

593 else self.template

594 )

595 if ( 21 ctx 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) 9 ctx 1dfjlmnobc

601 return self 12 ctx 1eghikpqstvuw

602

603

604 class TranslatableStringConstructor(Protocol): (empty)

605 """Construct a [`TranslatableString`][].""" (empty)

606

607 def __call__( (empty)

608 self,

609 context: str, (empty)

610 single: str, (empty)

611 /,

612 flags: Iterable[str] = (), (empty)

613 plural: str = '', (empty)

614 comments: str = '', (empty)

615 ) -> TranslatableString: (empty)

616 """Return a [`TranslatableString`][] from these parts.

617

618 Usually some form of validation or normalization is performed

619 first on these parts.

620

621 The main implementation of this is in [`translatable`][].

622

623 """

624

625

626 def commented(comments: str = '', /) -> TranslatableStringConstructor: (empty)

627 """A "decorator" for readably constructing commented enum values.

628

629 Returns a partial application of [`translatable`][] with the `comments`

630 argument pre-filled.

631

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.

637

638 This is not necessarily good code style, nor is it a lightweight

639 solution.

640

641 """ # noqa: DOC201

642 return functools.partial(translatable, comments=comments) (empty)

643

644

645 class Label(enum.Enum): (empty)

646 """Labels for the `derivepassphrase` command-line.

647

648 Includes help text (long-form and short-form), help metavar names,

649 diagnostic labels and interactive prompts.

650

651 """

652

653 DEPRECATION_WARNING_LABEL = commented( (empty)

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 """""" (empty)

662 WARNING_LABEL = commented( (empty)

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 """""" (empty)

671 CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL = commented( (empty)

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 """""" (empty)

682 CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE = commented( (empty)

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 """""" (empty)

694 SETTINGS_ORIGIN_INTERACTIVE = commented( (empty)

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( (empty)

703 '',

704 )(

705 'Label :: Help text :: Explanation',

706 'Use $VISUAL or $EDITOR to configure the spawned editor.',

707 )

708 """""" (empty)

709 DERIVEPASSPHRASE_02 = commented( (empty)

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 """""" (empty)

720 DERIVEPASSPHRASE_03 = commented( (empty)

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 """""" (empty)

728 DERIVEPASSPHRASE_EPILOG_01 = commented( (empty)

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 """""" (empty)

738 DERIVEPASSPHRASE_EXPORT_02 = commented( (empty)

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 """""" (empty)

747 DERIVEPASSPHRASE_EXPORT_03 = DERIVEPASSPHRASE_03 (empty)

748 """""" (empty)

749 DERIVEPASSPHRASE_EXPORT_VAULT_02 = commented( (empty)

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 """""" (empty)

759 DERIVEPASSPHRASE_EXPORT_VAULT_03 = commented( (empty)

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 """""" (empty)

771 DERIVEPASSPHRASE_VAULT_02 = commented( (empty)

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 """""" (empty)

781 DERIVEPASSPHRASE_VAULT_EPILOG_01 = commented( (empty)

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 """""" (empty)

793 DERIVEPASSPHRASE_VAULT_EPILOG_02 = commented( (empty)

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 """""" (empty)

801 DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT = commented( (empty)

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 """""" (empty)

822 DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT = commented( (empty)

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 """""" (empty)

834 PASSPHRASE_GENERATION_EPILOG = commented( (empty)

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 """""" (empty)

842 STORAGE_MANAGEMENT_EPILOG = commented( (empty)

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 """""" (empty)

851 DEPRECATED_COMMAND_LABEL = commented( (empty)

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 """""" (empty)

860 DERIVEPASSPHRASE_VAULT_NOTES_MARKER = commented( (empty)

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 """""" (empty)

870 EXPORT_VAULT_FORMAT_METAVAR_FMT = commented( (empty)

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 """""" (empty)

879 EXPORT_VAULT_KEY_METAVAR_K = commented( (empty)

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 """""" (empty)

888 EXPORT_VAULT_METAVAR_PATH = commented( (empty)

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 """""" (empty)

898 PASSPHRASE_GENERATION_METAVAR_NUMBER = commented( (empty)

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 """""" (empty)

907 STORAGE_MANAGEMENT_METAVAR_PATH = commented( (empty)

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 """""" (empty)

916 VAULT_METAVAR_SERVICE = commented( (empty)

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 """""" (empty)

929 DEBUG_OPTION_HELP_TEXT = commented( (empty)

930 '',

931 )(

932 'Label :: Help text :: One-line description',

933 'Also emit debug information. Implies --verbose.',

934 )

935 """""" (empty)

936 DERIVEPASSPHRASE_01 = commented( (empty)

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 """""" (empty)

947 DERIVEPASSPHRASE_EXPORT_01 = commented( (empty)

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 """""" (empty)

958 DERIVEPASSPHRASE_EXPORT_VAULT_01 = commented( (empty)

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 """""" (empty)

969 DERIVEPASSPHRASE_VAULT_01 = commented( (empty)

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 """""" (empty)

980 DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT = commented( (empty)

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 """""" (empty)

988 DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT = commented( (empty)

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 """""" (empty)

996 DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT = commented( (empty)

997 '',

998 )(

999 'Label :: Help text :: One-line description',

1000 'Delete all settings.',

1001 )

1002 """""" (empty)

1003 DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT = commented( (empty)

1004 '',

1005 )(

1006 'Label :: Help text :: One-line description',

1007 'Delete the global settings.',

1008 )

1009 """""" (empty)

1010 DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT = commented( (empty)

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 """""" (empty)

1018 DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT = commented( (empty)

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 """""" (empty)

1033 DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT = commented( (empty)

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 """""" (empty)

1045 DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT = commented( (empty)

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 """""" (empty)

1053 DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT = commented( (empty)

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 """""" (empty)

1061 DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT = commented( (empty)

1062 '',

1063 )(

1064 'Label :: Help text :: One-line description',

1065 'Select a suitable SSH key from the SSH agent.',

1066 )

1067 """""" (empty)

1068 DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT = commented( (empty)

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 """""" (empty)

1076 DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT = commented( (empty)

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 """""" (empty)

1084 DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT = commented( (empty)

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 """""" (empty)

1092 DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT = commented( (empty)

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 """""" (empty)

1100 DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT = commented( (empty)

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 """""" (empty)

1109 DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT = commented( (empty)

1110 '',

1111 )(

1112 'Label :: Help text :: One-line description',

1113 'Prompt for a master passphrase.',

1114 )

1115 """""" (empty)

1116 DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT = commented( (empty)

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 """""" (empty)

1127 DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT = commented( (empty)

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 """""" (empty)

1136 DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT = commented( (empty)

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 """""" (empty)

1144 DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT = commented( (empty)

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 """""" (empty)

1152 DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT = commented( (empty)

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 """""" (empty)

1165 DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT = commented( (empty)

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 """""" (empty)

1173

1174 EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT = commented( (empty)

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 """""" (empty)

1183 EXPORT_VAULT_FORMAT_HELP_TEXT = commented( (empty)

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 """""" (empty)

1195 EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT = commented( (empty)

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 """""" (empty)

1203 EXPORT_VAULT_KEY_HELP_TEXT = commented( (empty)

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 """""" (empty)

1212 HELP_OPTION_HELP_TEXT = commented( (empty)

1213 '',

1214 )(

1215 'Label :: Help text :: One-line description',

1216 'Show this help text, then exit.',

1217 )

1218 """""" (empty)

1219 QUIET_OPTION_HELP_TEXT = commented( (empty)

1220 '',

1221 )(

1222 'Label :: Help text :: One-line description',

1223 'Suppress even warnings; emit only errors.',

1224 )

1225 """""" (empty)

1226 VERBOSE_OPTION_HELP_TEXT = commented( (empty)

1227 '',

1228 )(

1229 'Label :: Help text :: One-line description',

1230 'Emit extra/progress information to standard error.',

1231 )

1232 """""" (empty)

1233 VERSION_OPTION_HELP_TEXT = commented( (empty)

1234 '',

1235 )(

1236 'Label :: Help text :: One-line description',

1237 'Show version and feature information, then exit.',

1238 )

1239 """""" (empty)

1240 COMMANDS_LABEL = commented( (empty)

1241 '',

1242 )(

1243 'Label :: Help text :: Option group name',

1244 'Commands',

1245 )

1246 """""" (empty)

1247 COMPATIBILITY_OPTION_LABEL = commented( (empty)

1248 '',

1249 )(

1250 'Label :: Help text :: Option group name',

1251 'Compatibility and extension options',

1252 )

1253 """""" (empty)

1254 CONFIGURATION_LABEL = commented( (empty)

1255 '',

1256 )(

1257 'Label :: Help text :: Option group name',

1258 'Configuration',

1259 )

1260 """""" (empty)

1261 LOGGING_LABEL = commented( (empty)

1262 '',

1263 )(

1264 'Label :: Help text :: Option group name',

1265 'Logging',

1266 )

1267 """""" (empty)

1268 OPTIONS_LABEL = commented( (empty)

1269 '',

1270 )(

1271 'Label :: Help text :: Option group name',

1272 'Options',

1273 )

1274 """""" (empty)

1275 OTHER_OPTIONS_LABEL = commented( (empty)

1276 '',

1277 )(

1278 'Label :: Help text :: Option group name',

1279 'Other options',

1280 )

1281 """""" (empty)

1282 PASSPHRASE_GENERATION_LABEL = commented( (empty)

1283 '',

1284 )(

1285 'Label :: Help text :: Option group name',

1286 'Passphrase generation',

1287 )

1288 """""" (empty)

1289 STORAGE_MANAGEMENT_LABEL = commented( (empty)

1290 '',

1291 )(

1292 'Label :: Help text :: Option group name',

1293 'Storage management',

1294 )

1295 """""" (empty)

1296 VERSION_INFO_MAJOR_LIBRARY_TEXT = commented( (empty)

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 """""" (empty)

1305 ENABLED_PEP508_EXTRAS = commented( (empty)

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 """""" (empty)

1315 SUPPORTED_DERIVATION_SCHEMES = commented( (empty)

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 """""" (empty)

1325 SUPPORTED_FEATURES = commented( (empty)

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 """""" (empty)

1335 SUPPORTED_FOREIGN_CONFIGURATION_FORMATS = commented( (empty)

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 """""" (empty)

1345 SUPPORTED_SUBCOMMANDS = commented( (empty)

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 """""" (empty)

1355 UNAVAILABLE_DERIVATION_SCHEMES = commented( (empty)

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 """""" (empty)

1365 UNAVAILABLE_FEATURES = commented( (empty)

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 """""" (empty)

1375 UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS = commented( (empty)

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 """""" (empty)

1385 CONFIRM_THIS_CHOICE_PROMPT_TEXT = commented( (empty)

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 """""" (empty)

1395 SUITABLE_SSH_KEYS_LABEL = commented( (empty)

1396 'This label is the heading of the list of suitable SSH keys.',

1397 )(

1398 'Label :: Interactive prompt',

1399 'Suitable SSH keys:',

1400 )

1401 """""" (empty)

1402 YOUR_SELECTION_PROMPT_TEXT = commented( (empty)

1403 '',

1404 )(

1405 'Label :: Interactive prompt',

1406 'Your selection? (1-{n}, leave empty to abort)',

1407 flags='python-brace-format',

1408 )

1409 """""" (empty)

1410

1411

1412 class DebugMsgTemplate(enum.Enum): (empty)

1413 """Debug messages for the `derivepassphrase` command-line."""

1414

1415 BUCKET_ITEM_FOUND = commented( (empty)

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 """""" (empty)

1428 DECRYPT_BUCKET_ITEM_INFO = commented( (empty)

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 """\

1436 Decrypt bucket item contents:

1437

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 """""" (empty)

1448 DECRYPT_BUCKET_ITEM_KEY_INFO = commented( (empty)

1449 '',

1450 )(

1451 'Debug message',

1452 """\

1453 Decrypt bucket item:

1454

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 """""" (empty)

1463 DECRYPT_BUCKET_ITEM_MAC_INFO = commented( (empty)

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 """\

1475 Decrypt bucket item contents:

1476

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 """""" (empty)

1486 DECRYPT_BUCKET_ITEM_SESSION_KEYS_INFO = commented( (empty)

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 """\

1494 Decrypt bucket item session keys:

1495

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 """""" (empty)

1507 DECRYPT_BUCKET_ITEM_SESSION_KEYS_MAC_INFO = commented( (empty)

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 """\

1519 Decrypt bucket item session keys:

1520

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 """""" (empty)

1530 DERIVED_MASTER_KEYS_KEYS = commented( (empty)

1531 '',

1532 )(

1533 'Debug message',

1534 """\

1535 Derived master keys' keys:

1536

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 """""" (empty)

1546 DIRECTORY_CONTENTS_CHECK_OK = commented( (empty)

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 """""" (empty)

1562 MASTER_KEYS_DATA_MAC_INFO = commented( (empty)

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 """\

1574 Master keys data:

1575

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 """""" (empty)

1585 POSTPONING_DIRECTORY_CONTENTS_CHECK = commented( (empty)

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 """""" (empty)

1601 SETTING_CONFIG_STRUCTURE_CONTENTS = commented( (empty)

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 """""" (empty)

1614 SETTING_CONFIG_STRUCTURE_CONTENTS_EMPTY_DIRECTORY = commented( (empty)

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 """""" (empty)

1627 VAULT_NATIVE_CHECKING_MAC_DETAILS = commented( (empty)

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 """\

1636 MAC details:

1637

1638\b

1639 MAC input: {mac_input}

1640 Expected MAC: {mac}

1641""",

1642 flags='python-brace-format',

1643 )

1644 """""" (empty)

1645 VAULT_NATIVE_EVP_BYTESTOKEY_INIT = commented( (empty)

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 """\

1655 evp_bytestokey_md5 (initialization):

1656

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 """""" (empty)

1668 VAULT_NATIVE_EVP_BYTESTOKEY_RESULT = commented( (empty)

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 """\

1679 evp_bytestokey_md5 (result):

1680

1681\b

1682 Encryption key: {enc_key}

1683 IV: {iv}

1684""",

1685 flags='python-brace-format',

1686 )

1687 """""" (empty)

1688 VAULT_NATIVE_EVP_BYTESTOKEY_ROUND = commented( (empty)

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 """\

1700 evp_bytestokey_md5 (round update):

1701

1702\b

1703 Buffer length: {buffer_length}

1704 Buffer: {buffer}

1705""",

1706 flags='python-brace-format',

1707 )

1708 """""" (empty)

1709 VAULT_NATIVE_PADDED_PLAINTEXT = commented( (empty)

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 """""" (empty)

1719 VAULT_NATIVE_PARSE_BUFFER = commented( (empty)

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 """\

1728 Buffer: {contents}

1729

1730\b

1731 IV: {iv}

1732 Payload (ciphertext): {payload}

1733 MAC: {mac}

1734""",

1735 flags='python-brace-format',

1736 )

1737 """""" (empty)

1738 VAULT_NATIVE_PBKDF2_CALL = commented( (empty)

1739 '',

1740 )(

1741 'Debug message',

1742 """\

1743 Master key derivation:

1744

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 """""" (empty)

1753 VAULT_NATIVE_PLAINTEXT = commented( (empty)

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 """""" (empty)

1763 VAULT_NATIVE_V02_PAYLOAD_MAC_POSTPROCESSING = commented( (empty)

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 """\

1773 Postprocessing buffer (v0.2):

1774

1775\b

1776 Payload: {payload} (decoded from base64)

1777 MAC: {mac} (decoded from hex)

1778""",

1779 flags='python-brace-format',

1780 )

1781 """""" (empty)

1782

1783

1784 class InfoMsgTemplate(enum.Enum): (empty)

1785 """Info messages for the `derivepassphrase` command-line."""

1786

1787 ASSEMBLING_CONFIG_STRUCTURE = commented( (empty)

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 """""" (empty)

1801 CANNOT_LOAD_AS_VAULT_CONFIG = commented( (empty)

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 """""" (empty)

1811 CHECKING_CONFIG_STRUCTURE_CONSISTENCY = commented( (empty)

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 """""" (empty)

1822 DECRYPTING_BUCKET = commented( (empty)

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 """""" (empty)

1835 PARSING_MASTER_KEYS_DATA = commented( (empty)

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 """""" (empty)

1845 PIP_INSTALL_EXTRA = commented( (empty)

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 """""" (empty)

1860 SUCCESSFULLY_MIGRATED = commented( (empty)

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 """""" (empty)

1869 VAULT_NATIVE_CHECKING_MAC = commented( (empty)

1870 '',

1871 )(

1872 'Info message',

1873 'Checking MAC.',

1874 )

1875 """""" (empty)

1876 VAULT_NATIVE_DECRYPTING_CONTENTS = commented( (empty)

1877 '',

1878 )(

1879 'Info message',

1880 'Decrypting contents.',

1881 )

1882 """""" (empty)

1883 VAULT_NATIVE_DERIVING_KEYS = commented( (empty)

1884 '',

1885 )(

1886 'Info message',

1887 'Deriving an encryption and signing key.',

1888 )

1889 """""" (empty)

1890 VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC = commented( (empty)

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 """""" (empty)

1906

1907

1908 class WarnMsgTemplate(enum.Enum): (empty)

1909 """Warning messages for the `derivepassphrase` command-line."""

1910

1911 EDITING_NOTES_BUT_NOT_STORING_CONFIG = commented( (empty)

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( (empty)

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 """""" (empty)

1929 EMPTY_SERVICE_SETTINGS_INACCESSIBLE = commented( (empty)

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 """""" (empty)

1941 FAILED_TO_MIGRATE_CONFIG = commented( (empty)

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 """""" (empty)

1949 GLOBAL_PASSPHRASE_INEFFECTIVE = commented( (empty)

1950 '',

1951 )(

1952 'Warning message',

1953 'Setting a global passphrase is ineffective '

1954 'because a key is also set.',

1955 )

1956 """""" (empty)

1957 LEGACY_EDITOR_INTERFACE_NOTES_BACKUP = commented( (empty)

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( (empty)

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 """""" (empty)

1975 NO_ANNOYING_OS_NAMED_PIPES = commented( (empty)

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 """""" (empty)

1983 PASSPHRASE_NOT_NORMALIZED = commented( (empty)

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 """""" (empty)

2007 SERVICE_NAME_INCOMPLETABLE = commented( (empty)

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 """""" (empty)

2021 SERVICE_PASSPHRASE_INEFFECTIVE = commented( (empty)

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 """""" (empty)

2031 STEP_REMOVE_INEFFECTIVE_VALUE = commented( (empty)

2032 '',

2033 )(

2034 'Warning message',

2035 'Removing ineffective setting {path} = {old}.',

2036 flags='python-brace-format',

2037 )

2038 """""" (empty)

2039 STEP_REPLACE_INVALID_VALUE = commented( (empty)

2040 '',

2041 )(

2042 'Warning message',

2043 'Replacing invalid value {old} for key {path} with {new}.',

2044 flags='python-brace-format',

2045 )

2046 """""" (empty)

2047 V01_STYLE_CONFIG = commented( (empty)

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 """""" (empty)

2057 V10_SUBCOMMAND_REQUIRED = commented( (empty)

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 """""" (empty)

2069

2070

2071 class ErrMsgTemplate(enum.Enum): (empty)

2072 """Error messages for the `derivepassphrase` command-line."""

2073

2074 AGENT_REFUSED_LIST_KEYS = commented( (empty)

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 """""" (empty)

2081 AGENT_REFUSED_SIGNATURE = commented( (empty)

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 """""" (empty)

2091 CANNOT_CONNECT_TO_AGENT = commented( (empty)

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 """""" (empty)

2099 CANNOT_DECODEIMPORT_VAULT_SETTINGS = commented( (empty)

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 """""" (empty)

2107 CANNOT_EXPORT_VAULT_SETTINGS = commented( (empty)

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 """""" (empty)

2115 CANNOT_IMPORT_VAULT_SETTINGS = commented( (empty)

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 """""" (empty)

2123 CANNOT_LOAD_USER_CONFIG = commented( (empty)

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 """""" (empty)

2131 CANNOT_LOAD_VAULT_SETTINGS = commented( (empty)

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 """""" (empty)

2139 CANNOT_PARSE_AS_VAULT_CONFIG = commented( (empty)

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 """""" (empty)

2154 CANNOT_PARSE_AS_VAULT_CONFIG_OSERROR = commented( (empty)

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 """""" (empty)

2163 CANNOT_STORE_VAULT_SETTINGS = commented( (empty)

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 """""" (empty)

2171 CANNOT_UNDERSTAND_AGENT = commented( (empty)

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 """""" (empty)

2186 CANNOT_UPDATE_SETTINGS_NO_SETTINGS = commented( (empty)

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 """""" (empty)

2206 INVALID_USER_CONFIG = commented( (empty)

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 """""" (empty)

2214 INVALID_VAULT_CONFIG = commented( (empty)

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 """""" (empty)

2225 MISSING_MODULE = commented( (empty)

2226 '',

2227 )(

2228 'Error message',

2229 'Cannot load the required Python module {module!r}.',

2230 flags='python-brace-format',

2231 )

2232 """""" (empty)

2233 NO_AGENT_SUPPORT = commented( (empty)

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 """""" (empty)

2241 NO_KEY_OR_PHRASE = commented( (empty)

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 """""" (empty)

2249 NO_SSH_AGENT_FOUND = commented( (empty)

2250 '',

2251 )(

2252 'Error message',

2253 'Cannot find any running SSH agent because SSH_AUTH_SOCK is not set.',

2254 )

2255 """""" (empty)

2256 NO_SUITABLE_SSH_KEYS = commented( (empty)

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 """""" (empty)

2264 PARAMS_MUTUALLY_EXCLUSIVE = commented( (empty)

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 """""" (empty)

2273 PARAMS_NEEDS_SERVICE = commented( (empty)

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 """""" (empty)

2282 PARAMS_NEEDS_SERVICE_OR_CONFIG = commented( (empty)

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 """""" (empty)

2291 PARAMS_NO_SERVICE = commented( (empty)

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 """""" (empty)

2300 SERVICE_REQUIRED = commented( (empty)

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 """""" (empty)

2308 SET_AND_UNSET_SAME_SETTING = commented( (empty)

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 """""" (empty)

2319 SSH_KEY_NOT_LOADED = commented( (empty)

2320 '',

2321 )(

2322 'Error message',

2323 'The requested SSH key is not loaded into the agent.',

2324 )

2325 """""" (empty)

2326 USER_ABORTED_EDIT = commented( (empty)

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 """""" (empty)

2334 USER_ABORTED_PASSPHRASE = commented( (empty)

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 """""" (empty)

2342 USER_ABORTED_SSH_KEY_SELECTION = commented( (empty)

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 """""" (empty)

2350

2351

2352 MsgTemplate: TypeAlias = Union[ (empty)

2353 Label,

2354 DebugMsgTemplate,

2355 InfoMsgTemplate,

2356 WarnMsgTemplate,

2357 ErrMsgTemplate,

2358]

2359"""A type alias for all enums containing translatable strings as values.""" (empty)

2360 MSG_TEMPLATE_CLASSES = ( (empty)

2361 Label,

2362 DebugMsgTemplate,

2363 InfoMsgTemplate,

2364 WarnMsgTemplate,

2365 ErrMsgTemplate,

2366)

2367"""A collection all enums containing translatable strings as values.""" (empty)

2368

2369 DebugTranslations._load_cache() # noqa: SLF001 (empty)

2370

2371

2372 def _write_po_file( # noqa: C901,PLR0912 (empty)

2373 fileobj: TextIO, (empty)

2374 /,

2375 *,

2376 is_template: bool = True, (empty)

2377 version: str = VERSION, (empty)

2378 build_time: datetime.datetime | None = None, (empty)

2379) -> None: # pragma: no cover (empty)

2380 r"""Write a .po file to the given file object.

2381

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.

2386

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.

2390

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)

2470

2471 context_class = {

2472 'Label': Label,

2473 'Debug message': DebugMsgTemplate,

2474 'Info message': InfoMsgTemplate,

2475 'Warning message': WarnMsgTemplate,

2476 'Error message': ErrMsgTemplate,

2477 }

2478

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 )

2488

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 )

2510

2511

2512 def _format_po_info( (empty)

2513 data: Mapping[str, Any], (empty)

2514 /,

2515) -> Iterator[str]: # pragma: no cover (empty)

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 ]

2531

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

2538

2539 for key in sorted(data.keys(), key=_sort_position):

2540 value = data[key]

2541 line = f'{key}: {value}\n'

2542 yield _cstr(line)

2543

2544

2545 def _format_po_entry( (empty)

2546 enum_value: MsgTemplate, (empty)

2547 /,

2548 *,

2549 is_debug_translation: bool = False, (empty)

2550 transformed_string: TranslatableString | None = None, (empty)

2551) -> tuple[str, ...]: # pragma: no cover (empty)

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)

2576

2577

2578 def _cstr(s: str) -> str: # pragma: no cover (empty)

2579 """""" # noqa: D419

2580

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 })

2604

2605 return '\n'.join(

2606 f'"{escape(line)}"'

2607 for line in s.splitlines(True) or [''] # noqa: FBT003

2608 )

2609

2610

2611 if __name__ == '__main__': (empty)

2612 import argparse

2613

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

2621

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 )