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

487 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 21:34 +0200

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

2# 

3# SPDX-Licence-Identifier: Zlib 

4 

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

6 

7Also contains some machinery related to internationalization and 

8localization. 

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 

18from __future__ import annotations 

19 

20import contextlib 

21import datetime 

22import enum 

23import functools 

24import gettext 

25import inspect 

26import os 

27import pathlib 

28import string 

29import sys 

30import textwrap 

31import types 

32from typing import TYPE_CHECKING, NamedTuple, Protocol, TextIO, Union, cast 

33 

34from typing_extensions import TypeAlias, override 

35 

36from derivepassphrase import _internals 

37 

38if TYPE_CHECKING: 

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

40 

41 from typing_extensions import Any, Self 

42 

43__all__ = ("PROG_NAME",) 

44 

45PROG_NAME = _internals.PROG_NAME 

46VERSION = _internals.VERSION 

47AUTHOR = _internals.AUTHOR 

48 

49 

50def load_translations( 

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

52 languages: Sequence[str] | None = None, 

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

54) -> gettext.NullTranslations: # pragma: no cover [external] 

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 if localedirs is None: 

82 # TODO(the-13th-letter): Define a public (and opaque) enum for these 

83 # special directories so that they are available to callers as well, 

84 # without computation. Shift the computation into a separate 

85 # top-level function, so that it can be stubbed during tests. 

86 # Support the `.../site-packages/share/locale` special directory via 

87 # a new enum value, because that is where the derivepassphrase wheel 

88 # stores its packaged translations. Then reimplement `gettext.find` 

89 # and `gettext.translation` with support for `importlib.resources`. 

90 # The heavy lifting is already being done by `locale.normalize`. 

91 if sys.platform.startswith("win"): 

92 xdg_data_home = ( 

93 pathlib.Path(os.environ["APPDATA"]) 

94 if os.environ.get("APPDATA") 

95 else pathlib.Path("~").expanduser() 

96 ) 

97 elif os.environ.get("XDG_DATA_HOME"): 

98 xdg_data_home = pathlib.Path(os.environ["XDG_DATA_HOME"]) 

99 else: 

100 xdg_data_home = ( 

101 pathlib.Path("~").expanduser() / ".local" / ".share" 

102 ) 

103 localedirs = [ 

104 pathlib.Path(xdg_data_home, "locale"), 

105 pathlib.Path(sys.prefix, "share", "locale"), 

106 pathlib.Path(sys.base_prefix, "share", "locale"), 

107 ] 

108 for localedir in localedirs: 

109 with contextlib.suppress(OSError): 

110 return gettext.translation( 

111 PROG_NAME, 

112 localedir=os.fsdecode(localedir), 

113 languages=languages, 

114 class_=class_, 

115 ) 

116 return gettext.NullTranslations() 

117 

118 

119translation = load_translations() 

120_debug_translation_message_cache: dict[ 

121 tuple[str, str], 

122 tuple[MsgTemplate, frozenset], 

123] = {} 

124 

125 

126class DebugTranslations(gettext.NullTranslations): 

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

128 

129 Each call to the `*gettext` methods will return the enum name if the 

130 message is a known translatable message for the `derivepassphrase` 

131 command-line interface, or the message itself otherwise. 

132 

133 """ 

134 

135 @staticmethod 

136 def _load_cache() -> None: 

137 cache = _debug_translation_message_cache 

138 for enum_class in MSG_TEMPLATE_CLASSES: 

139 for member in enum_class.__members__.values(): 

140 value = member.value 

141 queue: list[tuple[TranslatableString, frozenset[str]]] = [ 

142 (value, frozenset()) 

143 ] 

144 value2 = value.maybe_without_filename() 

145 if value != value2: 

146 queue.append((value2, frozenset({"filename"}))) 

147 for v, trimmed in queue: 

148 singular = v.singular 

149 plural = v.plural 

150 context = v.l10n_context 

151 cache.setdefault((context, singular), (member, trimmed)) 

152 if plural: # pragma: no cover [unused] 

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

154 

155 @classmethod 

156 def _locate_message( 

157 cls, 

158 message: str, 

159 /, 

160 *, 

161 context: str = "", 

162 message_plural: str = "", 

163 n: int = 1, 

164 ) -> str: 

165 try: 2B rbC x r b

166 enum_value, trimmed = _debug_translation_message_cache[ 2B rbC x r b

167 context, message 

168 ] 

169 except KeyError: 2rbx

170 return message if not message_plural or n == 1 else message_plural 2rbx

171 return cls._format_enum_name_maybe_with_fields( 1BCrb

172 enum_name=str(enum_value), 

173 ts=enum_value.value, 

174 trimmed=trimmed, 

175 ) 

176 

177 @staticmethod 

178 def _format_enum_name_maybe_with_fields( 

179 enum_name: str, 

180 ts: TranslatableString, 

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

182 ) -> str: 

183 formatted_fields = [ 1BCrb

184 f"{f}=None" if f in trimmed else f"{f}={{{f}!r}}" 

185 for f in ts.fields() 

186 ] 

187 return ( 1BCrb

188 "{}({})".format(enum_name, ", ".join(formatted_fields)) 

189 if formatted_fields 

190 else str(enum_name) 

191 ) 

192 

193 @override 

194 def gettext( 

195 self, 

196 message: str, 

197 ) -> str: 

198 return self._locate_message(message) 2rbx

199 

200 @override 

201 def ngettext( 

202 self, 

203 msgid1: str, 

204 msgid2: str, 

205 n: int, 

206 ) -> str: # pragma: no cover [unused] 

207 """""" # noqa: D419 

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

209 

210 @override 

211 def pgettext( 

212 self, 

213 context: str, 

214 message: str, 

215 ) -> str: 

216 return self._locate_message(message, context=context) 1BCrb

217 

218 @override 

219 def npgettext( 

220 self, 

221 context: str, 

222 msgid1: str, 

223 msgid2: str, 

224 n: int, 

225 ) -> str: # pragma: no cover [unused] 

226 """""" # noqa: D419 

227 return self._locate_message( 

228 msgid1, 

229 context=context, 

230 message_plural=msgid2, 

231 n=n, 

232 ) 

233 

234 

235class TranslatableString(NamedTuple): 

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

237 

238 For typing purposes. 

239 

240 Attributes: 

241 l10n_context: 

242 The localization context, as per [`gettext`][]. Used to 

243 disambiguate different uses of the same translatable string. 

244 singular: 

245 The translatable message, base case. 

246 plural: 

247 The translatable message, plural case. Usually unset. 

248 translator_comments: 

249 Explicit commentary for the translator. 

250 flags: 

251 `.mo` file flags for this message, e.g. to indicate the 

252 string formatting style in use. 

253 

254 """ 

255 

256 l10n_context: str 

257 """""" 

258 singular: str 

259 """""" 

260 plural: str = "" 

261 """""" 

262 flags: frozenset[str] = frozenset() 

263 """""" 

264 translator_comments: str = "" 

265 """""" 

266 

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

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

269 

270 Raises: 

271 NotImplementedError: 

272 Replacement field discovery for %-formatting is not 

273 implemented. 

274 

275 """ 

276 if "python-format" in self.flags: # pragma: no cover [unused] 1aBCrb

277 err_msg = ( 

278 "Replacement field discovery for %-formatting " 

279 "is not implemented" 

280 ) 

281 raise NotImplementedError(err_msg) 

282 if ( 1aBCrb

283 "no-python-brace-format" in self.flags 

284 or "python-brace-format" not in self.flags 

285 ): 

286 return [] 1aBCr

287 formatter = string.Formatter() 1aBCb

288 fields: dict[str, int] = {} 1aBCb

289 for _lit, field, _spec, _conv in formatter.parse(self.singular): 1aBCb

290 if field is not None and field not in fields: 1aBCb

291 fields[field] = len(fields) 1aBCb

292 return sorted(fields, key=fields.__getitem__) 1aBCb

293 

294 @staticmethod 

295 def _maybe_rewrap( 

296 string: str, 

297 /, 

298 *, 

299 fix_sentence_endings: bool = True, 

300 ) -> str: 

301 string = inspect.cleandoc(string) 

302 if not any(s.strip() == "\b" for s in string.splitlines()): 

303 string = "\n".join( 

304 textwrap.wrap( 

305 string, 

306 width=float("inf"), # type: ignore[arg-type] 

307 fix_sentence_endings=fix_sentence_endings, 

308 ) 

309 ) 

310 else: 

311 string = "".join( 

312 s 

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

314 if s.strip() != "\b" 

315 ) 

316 return string 

317 

318 def maybe_without_filename(self) -> Self: 

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

320 

321 Only acts upon translatable strings containing the exact 

322 contents `": {filename!r}"`. The specified part will be 

323 removed. This is correct usage in English for messages like 

324 `"Cannot open file: {error}: {filename!r}."`, but not 

325 necessarily in other languages. 

326 

327 """ 

328 filename_str = ": {filename!r}" 1adefgshijklmnopqvtwubc

329 ret = self 1adefgshijklmnopqvtwubc

330 a, sep1, b = self.singular.partition(filename_str) 1adefgshijklmnopqvtwubc

331 c, sep2, d = self.plural.partition(filename_str) 1adefgshijklmnopqvtwubc

332 if sep1: 1adefgshijklmnopqvtwubc

333 ret = ret._replace(singular=(a + b)) 1adefgshijklmnopqtubc

334 if sep2: # pragma: no cover [unused] 1adefgshijklmnopqvtwubc

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

336 return ret 1adefgshijklmnopqvtwubc

337 

338 def rewrapped(self) -> Self: 

339 """Return a rewrapped version of self. 

340 

341 Normalizes all parts assumed to contain English prose. 

342 

343 """ 

344 msg = self._maybe_rewrap(self.singular, fix_sentence_endings=True) 

345 plural = self._maybe_rewrap(self.plural, fix_sentence_endings=True) 

346 context = self.l10n_context.strip() 

347 comments = self._maybe_rewrap( 

348 self.translator_comments, fix_sentence_endings=False 

349 ) 

350 return self._replace( 

351 singular=msg, 

352 plural=plural, 

353 l10n_context=context, 

354 translator_comments=comments, 

355 ) 

356 

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

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

359 

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

361 normalized. 

362 

363 Returns: 

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

365 

366 """ 

367 if comments.strip() and not comments.lstrip().startswith( 

368 "TRANSLATORS:" 

369 ): # pragma: no cover [unused] 

370 comments = "TRANSLATORS: " + comments.lstrip() 

371 comments = self._maybe_rewrap(comments, fix_sentence_endings=False) 

372 return self._replace(translator_comments=comments) 

373 

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

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

376 

377 Returns: 

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

379 and all flags validated. 

380 

381 Raises: 

382 ValueError: 

383 The flags failed to validate. See the exact error 

384 message for details. 

385 

386 Examples: 

387 >>> TranslatableString("", "all OK").validate_flags() 

388 ... # doctest: +NORMALIZE_WHITESPACE 

389 TranslatableString(l10n_context='', singular='all OK', plural='', 

390 flags=frozenset(), translator_comments='') 

391 >>> TranslatableString("", "20% OK").validate_flags( 

392 ... "no-python-format" 

393 ... ) 

394 ... # doctest: +NORMALIZE_WHITESPACE 

395 TranslatableString(l10n_context='', singular='20% OK', plural='', 

396 flags=frozenset({'no-python-format'}), 

397 translator_comments='') 

398 >>> TranslatableString("", "%d items").validate_flags() 

399 ... # doctest: +ELLIPSIS 

400 Traceback (most recent call last): 

401 ... 

402 ValueError: Missing flag for how to deal with percent character ... 

403 >>> TranslatableString("", "{braces}").validate_flags() 

404 ... # doctest: +ELLIPSIS 

405 Traceback (most recent call last): 

406 ... 

407 ValueError: Missing flag for how to deal with brace character ... 

408 >>> TranslatableString("", "no braces").validate_flags( 

409 ... "python-brace-format" 

410 ... ) 

411 ... # doctest: +ELLIPSIS 

412 Traceback (most recent call last): 

413 ... 

414 ValueError: Missing format string parameters ... 

415 

416 """ 

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

418 if "{" in self.singular and not bool( 

419 all_flags & {"python-brace-format", "no-python-brace-format"} 

420 ): 

421 msg = ( 

422 f"Missing flag for how to deal with brace character " 

423 f"in {self.singular!r}" 

424 ) 

425 raise ValueError(msg) 

426 if "%" in self.singular and not bool( 

427 all_flags & {"python-format", "no-python-format"} 

428 ): 

429 msg = ( 

430 f"Missing flag for how to deal with percent character " 

431 f"in {self.singular!r}" 

432 ) 

433 raise ValueError(msg) 

434 if ( 

435 all_flags & {"python-format", "python-brace-format"} 

436 and "%" not in self.singular 

437 and "{" not in self.singular 

438 ): 

439 msg = f"Missing format string parameters in {self.singular!r}" 

440 raise ValueError(msg) 

441 return self._replace(flags=all_flags) 

442 

443 

444def translatable( 

445 context: str, 

446 single: str, 

447 /, 

448 flags: Iterable[str] = (), 

449 plural: str = "", 

450 comments: str = "", 

451) -> TranslatableString: 

452 """Return a [`TranslatableString`][] with validated parts. 

453 

454 This factory function is really only there to make the enum 

455 definitions more readable. It is the main implementation of the 

456 [`TranslatableStringConstructor`][]. 

457 

458 """ 

459 flags = ( 

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

461 ) 

462 return ( 

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

464 .rewrapped() 

465 .with_comments(comments) 

466 .validate_flags() 

467 ) 

468 

469 

470class TranslatedString: 

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

472 

473 The translation and replacement value rendering is only performed 

474 when this string object is actually stringified. 

475 

476 """ 

477 

478 def __init__( 

479 self, 

480 template: str | TranslatableString | MsgTemplate, 

481 args_dict: Mapping[str, Any] = types.MappingProxyType({}), 

482 /, 

483 **kwargs: Any, # noqa: ANN401 

484 ) -> None: 

485 """Initializer. 

486 

487 Args: 

488 template: 

489 A template string, suitable for [`str.format`][]. If 

490 a string, use it directly. If 

491 a [`TranslatableString`][], or a known enum value whose 

492 value is a `TranslatableString`, then use that string's 

493 "singular" entry. 

494 args_dict: 

495 Keyword arguments to be passed to [`str.format`][]. 

496 kwargs: 

497 More keyword arguments to be passed to [`str.format`][]. 

498 

499 """ 

500 # The cast is still needed for mypy. For this use case -- 

501 # multiple related enums, all values of which share the same 

502 # type, even across enums -- mypy's type inference is 

503 # frustratingly surface-level. 

504 # 

505 # pyrefly: ignore [redundant-cast] 

506 self.template = cast( 2a D d E F e f ^ G H I _ ` { | tbubvbJ wb} ~ abK L M g s N xbO P Q R S T U V W sbX Y Z 0 1 2 3 obpb4 5 6 h i j k qb7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

507 "str | TranslatableString", 

508 template.value 

509 if isinstance(template, MSG_TEMPLATE_CLASSES) 

510 else template, 

511 ) 

512 self.kwargs = {**args_dict, **kwargs} 2a D d E F e f ^ G H I _ ` { | tbubvbJ wb} ~ abK L M g s N xbO P Q R S T U V W sbX Y Z 0 1 2 3 obpb4 5 6 h i j k qb7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

513 self._rendered: str | None = None 2a D d E F e f ^ G H I _ ` { | tbubvbJ wb} ~ abK L M g s N xbO P Q R S T U V W sbX Y Z 0 1 2 3 obpb4 5 6 h i j k qb7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

514 

515 def __bool__(self) -> bool: 

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

517 return bool(str(self)) 1HIU

518 

519 def __eq__(self, other: object) -> bool: # pragma: no cover [debug] 

520 """Return true if the rendered string is equal to `other`.""" 

521 return str(self) == other 1rbc

522 

523 def __hash__(self) -> int: # pragma: no cover [debug] 

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

525 return hash(str(self)) 2a D d E F e f G J K L M g N O P Q R S T V W X Y Z 0 1 2 3 obpb4 5 6 h i j k qb7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ r b c

526 

527 def __repr__(self) -> str: # pragma: no cover [debug] 

528 """""" # noqa: D419 

529 return ( 

530 f"{self.__class__.__name__}({self.template!r}, " 

531 f"{dict(self.kwargs)!r})" 

532 ) 

533 

534 def __str__(self) -> str: 

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

536 

537 First, look up the translation of the string's template. Then 

538 fill in the replacement fields. Cache the result for future 

539 calls. 

540 

541 """ 

542 if self._rendered is None: 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W sbX Y Z 0 1 2 3 obpb4 5 6 h i j k qb7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

543 do_escape = False 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 h i j k 7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

544 if isinstance(self.template, str): 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 h i j k 7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

545 context = "" 2y z A [ x nb

546 template = self.template 2y z A [ x nb

547 else: 

548 context = self.template.l10n_context 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 h i j k 7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmbr b c ]

549 template = self.template.singular 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 h i j k 7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmbr b c ]

550 do_escape = "no-python-brace-format" in self.template.flags 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 h i j k 7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmbr b c ]

551 template = ( 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 h i j k 7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

552 translation.pgettext(context, template) 

553 if context 

554 else translation.gettext(template) 

555 ) 

556 template = self._escape(template) if do_escape else template 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 h i j k 7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

557 kwargs = { 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 h i j k 7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

558 k: str(v) if isinstance(v, TranslatedString) else v 

559 for k, v in self.kwargs.items() 

560 } 

561 self._rendered = template.format(**kwargs) 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 h i j k 7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

562 return self._rendered 2a D d E F e f ^ G H I _ ` { | J } ~ abK L M g s N O P Q R S T U V W sbX Y Z 0 1 2 3 obpb4 5 6 h i j k qb7 l m 8 9 n o ! # p q $ % ' ( ) * + , - . / : ; = ? @ v t bbw u cby dbebfbz A gbhbibjbkblbmb[ x r b c nb]

563 

564 @staticmethod 

565 def _escape(template: str) -> str: 

566 return template.translate({ 1[x]

567 ord("{"): "{{", 

568 ord("}"): "}}", 

569 }) 

570 

571 @classmethod 

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

573 return cls(cls._escape(template)) 1[x

574 

575 def maybe_without_filename(self) -> Self: 

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

577 

578 Only acts upon translated strings containing the exact contents 

579 `": {filename!r}"`. The specified part will be removed. This 

580 acts upon the string *before* translation, i.e., the string 

581 without the filename will be used as a translation base. 

582 

583 """ 

584 new_template = ( 1defgshijklmnopqvtwubc

585 self.template.maybe_without_filename() 

586 if not isinstance(self.template, str) 

587 else self.template 

588 ) 

589 if ( 1defgshijklmnopqvtwubc

590 not isinstance(new_template, str) 

591 and self.kwargs.get("filename") is None 

592 and new_template != self.template 

593 ): 

594 return self.__class__(new_template, self.kwargs) 1dehmpbc

595 return self 1fgsijklnoqvtwu

596 

597 

598class TranslatableStringConstructor(Protocol): 

599 """Construct a [`TranslatableString`][].""" 

600 

601 def __call__( 

602 self, 

603 context: str, 

604 single: str, 

605 /, 

606 flags: Iterable[str] = (), 

607 plural: str = "", 

608 comments: str = "", 

609 ) -> TranslatableString: 

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

611 

612 Usually some form of validation or normalization is performed 

613 first on these parts. 

614 

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

616 

617 """ 

618 

619 

620def commented(comments: str = "", /) -> TranslatableStringConstructor: 

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

622 

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

624 argument pre-filled. 

625 

626 This is geared towards the quirks of the API documentation extractor 

627 `mkdocstrings-python`/`griffe`, which reformat and trim enum value 

628 declarations in predictable but somewhat weird ways. Chains of function 

629 calls are preserved, though, so use this to our advantage to suggest 

630 a specific formatting. 

631 

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

633 solution. 

634 

635 """ # noqa: DOC201 

636 return functools.partial(translatable, comments=comments) 

637 

638 

639class Label(enum.Enum): 

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

641 

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

643 diagnostic labels and interactive prompts. 

644 

645 """ 

646 

647 DEPRECATION_WARNING_LABEL = commented( 

648 "This is a short label that will be prepended to " 

649 'a warning message, e.g., "Deprecation warning: A subcommand ' 

650 'will be required in v1.0."', 

651 )( 

652 "Label :: Diagnostics :: Marker", 

653 "Deprecation warning", 

654 ) 

655 """""" 

656 WARNING_LABEL = commented( 

657 "This is a short label that will be prepended to " 

658 'a warning message, e.g., "Warning: An empty service name ' 

659 'is not supported by vault(1)."', 

660 )( 

661 "Label :: Diagnostics :: Marker", 

662 "Warning", 

663 ) 

664 """""" 

665 CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL = commented( 

666 "This is one of two values of the settings_type metavar " 

667 "used in the CANNOT_UPDATE_SETTINGS_NO_SETTINGS entry. " 

668 "It is only used there. " 

669 "The full sentence then reads: " 

670 '"Cannot update the global settings without any given settings."', 

671 )( 

672 "Label :: Error message :: Metavar", 

673 "global settings", 

674 ) 

675 """""" 

676 CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE = commented( 

677 "This is one of two values of the settings_type metavar " 

678 "used in the CANNOT_UPDATE_SETTINGS_NO_SETTINGS entry. " 

679 "It is only used there. " 

680 "The full sentence then reads: " 

681 '"Cannot update the service-specific settings without any ' 

682 'given settings."', 

683 )( 

684 "Label :: Error message :: Metavar", 

685 "service-specific settings", 

686 ) 

687 """""" 

688 SETTINGS_ORIGIN_INTERACTIVE = commented( 

689 "This value is used as the {key} metavar for " 

690 "Label.PASSPHRASE_NOT_NORMALIZED if the passphrase was " 

691 "entered interactively.", 

692 )( 

693 "Label :: Error message :: Metavar", 

694 "interactive input", 

695 ) 

696 CONFIGURATION_EPILOG = commented( 

697 "", 

698 )( 

699 "Label :: Help text :: Explanation", 

700 "Use $VISUAL or $EDITOR to configure the spawned editor.", 

701 ) 

702 """""" 

703 DERIVEPASSPHRASE_02 = commented( 

704 "", 

705 )( 

706 "Label :: Help text :: Explanation", 

707 'The currently implemented subcommands are "vault" ' 

708 '(for the scheme used by vault) and "export" ' 

709 "(for exporting foreign configuration data). " 

710 "See the respective `--help` output for instructions. " 

711 'If no subcommand is given, we default to "vault".', 

712 ) 

713 """""" 

714 DERIVEPASSPHRASE_03 = commented( 

715 "", 

716 )( 

717 "Label :: Help text :: Explanation", 

718 'Deprecation notice: Defaulting to "vault" is deprecated. ' 

719 "Starting in v1.0, the subcommand must be specified explicitly.", 

720 ) 

721 """""" 

722 DERIVEPASSPHRASE_EPILOG_01 = commented( 

723 "", 

724 )( 

725 "Label :: Help text :: Explanation", 

726 "Configuration is stored in a directory according to the " 

727 "`DERIVEPASSPHRASE_PATH` variable, which defaults to " 

728 "`~/.derivepassphrase` on UNIX-like systems and " 

729 r"`C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.", 

730 ) 

731 """""" 

732 DERIVEPASSPHRASE_EXPORT_02 = commented( 

733 "", 

734 )( 

735 "Label :: Help text :: Explanation", 

736 'The only available subcommand is "vault", ' 

737 "which implements the vault-native configuration scheme. " 

738 'If no subcommand is given, we default to "vault".', 

739 ) 

740 """""" 

741 DERIVEPASSPHRASE_EXPORT_03 = DERIVEPASSPHRASE_03 

742 """""" 

743 DERIVEPASSPHRASE_EXPORT_VAULT_02 = commented( 

744 "The metavar is Label.EXPORT_VAULT_METAVAR_PATH.", 

745 )( 

746 "Label :: Help text :: Explanation", 

747 "Depending on the configuration format, " 

748 "{path_metavar} may either be a file or a directory. " 

749 'We support the vault "v0.2", "v0.3" and "storeroom" formats.', 

750 flags="python-brace-format", 

751 ) 

752 """""" 

753 DERIVEPASSPHRASE_EXPORT_VAULT_03 = commented( 

754 "The metavar is Label.EXPORT_VAULT_METAVAR_PATH.", 

755 )( 

756 "Label :: Help text :: Explanation", 

757 "If {path_metavar} is explicitly given as `VAULT_PATH`, " 

758 "then use the `VAULT_PATH` environment variable to " 

759 "determine the correct path. " 

760 "(Use `./VAULT_PATH` or similar to indicate a file/directory " 

761 "actually named `VAULT_PATH`.)", 

762 flags="python-brace-format", 

763 ) 

764 """""" 

765 DERIVEPASSPHRASE_VAULT_02 = commented( 

766 "The metavar is Label.VAULT_METAVAR_SERVICE.", 

767 )( 

768 "Label :: Help text :: Explanation", 

769 "If operating on global settings, or importing/exporting settings, " 

770 "then {service_metavar} must be omitted. " 

771 "Otherwise it is required.", 

772 flags="python-brace-format", 

773 ) 

774 """""" 

775 DERIVEPASSPHRASE_VAULT_EPILOG_01 = commented( 

776 "", 

777 )( 

778 "Label :: Help text :: Explanation", 

779 "WARNING: There is NO WAY to retrieve the generated passphrases " 

780 "if the master passphrase, the SSH key, or the exact " 

781 "passphrase settings are lost, " 

782 "short of trying out all possible combinations. " 

783 "You are STRONGLY advised to keep independent backups of " 

784 "the settings and the SSH key, if any.", 

785 ) 

786 """""" 

787 DERIVEPASSPHRASE_VAULT_EPILOG_02 = commented( 

788 "", 

789 )( 

790 "Label :: Help text :: Explanation", 

791 "The configuration is NOT encrypted, and you are " 

792 "STRONGLY discouraged from using a stored passphrase.", 

793 ) 

794 """""" 

795 DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT = commented( 

796 "This instruction text is shown above the user's old stored notes " 

797 "for this service, if any, if the recommended " 

798 '"modern" editor interface is used. ' 

799 "The next line is the cut marking defined in " 

800 "Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER." 

801 )( 

802 "Label :: Help text :: Explanation", 

803 """\ 

804\b 

805# Enter notes below the line with the cut mark (ASCII scissors and 

806# dashes). Lines above the cut mark (such as this one) will be ignored. 

807# 

808# If you wish to clear the notes, leave everything beyond the cut mark 

809# blank. However, if you leave the *entire* file blank, also removing 

810# the cut mark, then the edit is aborted, and the old notes contents are 

811# retained. 

812# 

813""", 

814 ) 

815 """""" 

816 DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT = commented( 

817 "This instruction text is shown if the vault(1)-compatible " 

818 '"legacy" editor interface is used and no previous notes exist. ' 

819 "The interface does not support commentary in the notes, " 

820 "so we fill this with obvious placeholder text instead. " 

821 "(Please replace this with what *your* language/culture would " 

822 "obviously recognize as placeholder text.)" 

823 )( 

824 "Label :: Help text :: Explanation", 

825 "INSERT NOTES HERE", 

826 ) 

827 """""" 

828 PASSPHRASE_GENERATION_EPILOG = commented( 

829 "The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.", 

830 )( 

831 "Label :: Help text :: Explanation", 

832 "Use {metavar}=0 to exclude a character type from the output.", 

833 flags="python-brace-format", 

834 ) 

835 """""" 

836 STORAGE_MANAGEMENT_EPILOG = commented( 

837 "The metavar is Label.STORAGE_MANAGEMENT_METAVAR_PATH.", 

838 )( 

839 "Label :: Help text :: Explanation", 

840 'Using "-" as {metavar} for standard input/standard output ' 

841 "is supported.", 

842 flags="python-brace-format", 

843 ) 

844 """""" 

845 DEPRECATED_COMMAND_LABEL = commented( 

846 "We use this format string to indicate, at the beginning " 

847 "of a command's help text, that this command is deprecated.", 

848 )( 

849 "Label :: Help text :: Marker", 

850 "(Deprecated) {text}", 

851 flags="python-brace-format", 

852 ) 

853 """""" 

854 DERIVEPASSPHRASE_VAULT_NOTES_MARKER = commented( 

855 "The marker for separating the text from " 

856 "Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT " 

857 "from the user's input (below the marker). " 

858 "The first line starting with this label marks the separation point.", 

859 )( 

860 "Label :: Help text :: Marker", 

861 "# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -", 

862 ) 

863 """""" 

864 EXPORT_VAULT_FORMAT_METAVAR_FMT = commented( 

865 "This text is used as {metavar} in " 

866 "Label.EXPORT_VAULT_FORMAT_HELP_TEXT, yielding e.g. " 

867 '"Try the following storage format FMT."', 

868 )( 

869 "Label :: Help text :: Metavar :: export vault", 

870 "FMT", 

871 ) 

872 """""" 

873 EXPORT_VAULT_KEY_METAVAR_K = commented( 

874 "This text is used as {metavar} in " 

875 "Label.EXPORT_VAULT_KEY_HELP_TEXT, yielding e.g. " 

876 '"Use K as the storage master key."', 

877 )( 

878 "Label :: Help text :: Metavar :: export vault", 

879 "K", 

880 ) 

881 """""" 

882 EXPORT_VAULT_METAVAR_PATH = commented( 

883 'Used as "path_metavar" in ' 

884 "Label.DERIVEPASSPHRASE_EXPORT_VAULT_02 and others, " 

885 'yielding e.g. "Depending on the configuration format, ' 

886 'PATH may either be a file or a directory."', 

887 )( 

888 "Label :: Help text :: Metavar :: export vault", 

889 "PATH", 

890 ) 

891 """""" 

892 PASSPHRASE_GENERATION_METAVAR_NUMBER = commented( 

893 "This metavar is used in Label.PASSPHRASE_GENERATION_EPILOG, " 

894 "Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT and others, " 

895 'yielding e.g. "Ensure a passphrase length of NUMBER characters.". ', 

896 )( 

897 "Label :: Help text :: Metavar :: vault", 

898 "NUMBER", 

899 ) 

900 """""" 

901 STORAGE_MANAGEMENT_METAVAR_PATH = commented( 

902 "This metavar is used in Label.STORAGE_MANAGEMENT_EPILOG, " 

903 "Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT and others, " 

904 'yielding e.g. "Ensure a passphrase length of NUMBER characters.". ', 

905 )( 

906 "Label :: Help text :: Metavar :: vault", 

907 "PATH", 

908 ) 

909 """""" 

910 VAULT_COMPATIBILITY_METAVAR_SSH_AGENT_SOCKET_PROVIDER = commented( 

911 "This metavar is used in " 

912 "Label.DERIVEPASSPHRASE_VAULT_SSH_AGENT_SOCKET_PROVIDER_HELP_TEXT, " 

913 'yielding e.g. "Use PROVIDER as the SSH agent socket provider.". ', 

914 )( 

915 "Label :: Help text :: Metavar :: vault", 

916 "PROVIDER", 

917 ) 

918 """""" 

919 VAULT_METAVAR_SERVICE = commented( 

920 'This metavar is used as "service_metavar" in multiple help texts, ' 

921 "such as Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT, " 

922 "Label.DERIVEPASSPHRASE_VAULT_02, ErrMsgTemplate.SERVICE_REQUIRED, " 

923 'etc. Sample texts are "Deriving a passphrase requires a SERVICE.", ' 

924 '"save the given settings for SERVICE, or global" and ' 

925 '"If operating on global settings, or importing/exporting settings, ' 

926 'then SERVICE must be omitted."', 

927 )( 

928 "Label :: Help text :: Metavar :: vault", 

929 "SERVICE", 

930 ) 

931 """""" 

932 DEBUG_OPTION_HELP_TEXT = commented( 

933 "", 

934 )( 

935 "Label :: Help text :: One-line description", 

936 "Also emit debug information. Implies --verbose.", 

937 ) 

938 """""" 

939 DERIVEPASSPHRASE_01 = commented( 

940 "This is the first paragraph of the command help text, " 

941 "but it also appears (in truncated form, if necessary) " 

942 "as one-line help text for this command. " 

943 "The translation should thus be as meaningful as possible " 

944 "even if truncated.", 

945 )( 

946 "Label :: Help text :: One-line description", 

947 "Derive a strong passphrase, deterministically, from a master secret.", 

948 ) 

949 """""" 

950 DERIVEPASSPHRASE_EXPORT_01 = commented( 

951 "This is the first paragraph of the command help text, " 

952 "but it also appears (in truncated form, if necessary) " 

953 "as one-line help text for this command. " 

954 "The translation should thus be as meaningful as possible " 

955 "even if truncated.", 

956 )( 

957 "Label :: Help text :: One-line description", 

958 "Export a foreign configuration to standard output.", 

959 ) 

960 """""" 

961 DERIVEPASSPHRASE_EXPORT_VAULT_01 = commented( 

962 "This is the first paragraph of the command help text, " 

963 "but it also appears (in truncated form, if necessary) " 

964 "as one-line help text for this command. " 

965 "The translation should thus be as meaningful as possible " 

966 "even if truncated.", 

967 )( 

968 "Label :: Help text :: One-line description", 

969 "Export a vault-native configuration to standard output.", 

970 ) 

971 """""" 

972 DERIVEPASSPHRASE_VAULT_01 = commented( 

973 "This is the first paragraph of the command help text, " 

974 "but it also appears (in truncated form, if necessary) " 

975 "as one-line help text for this command. " 

976 "The translation should thus be as meaningful as possible " 

977 "even if truncated.", 

978 )( 

979 "Label :: Help text :: One-line description", 

980 "Derive a passphrase using the vault derivation scheme.", 

981 ) 

982 """""" 

983 DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT = commented( 

984 "The metavar is Label.VAULT_METAVAR_SERVICE.", 

985 )( 

986 "Label :: Help text :: One-line description", 

987 "Save the given settings for {service_metavar}, or global.", 

988 flags="python-brace-format", 

989 ) 

990 """""" 

991 DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT = commented( 

992 "The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.", 

993 )( 

994 "Label :: Help text :: One-line description", 

995 'Ensure at least {metavar} "-" or "_" characters.', 

996 flags="python-brace-format", 

997 ) 

998 """""" 

999 DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT = commented( 

1000 "", 

1001 )( 

1002 "Label :: Help text :: One-line description", 

1003 "Delete all settings.", 

1004 ) 

1005 """""" 

1006 DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT = commented( 

1007 "", 

1008 )( 

1009 "Label :: Help text :: One-line description", 

1010 "Delete the global settings.", 

1011 ) 

1012 """""" 

1013 DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT = commented( 

1014 "The metavar is Label.VAULT_METAVAR_SERVICE.", 

1015 )( 

1016 "Label :: Help text :: One-line description", 

1017 "Delete the settings for {service_metavar}.", 

1018 flags="python-brace-format", 

1019 ) 

1020 """""" 

1021 DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT = commented( 

1022 "The corresponding option is displayed as " 

1023 '"--modern-editor-interface / --vault-legacy-editor-interface", ' 

1024 "so you may want to hint that the default (legacy) " 

1025 "is the second of those options. " 

1026 "Though the vault(1) legacy editor interface clearly has deficiencies " 

1027 "and (in my opinion) should only be used for compatibility purposes, " 

1028 "the one-line help text should try not to sound too judgmental, " 

1029 "if possible.", 

1030 )( 

1031 "Label :: Help text :: One-line description", 

1032 "Edit notes using the modern editor interface " 

1033 "or the vault-like legacy one (default).", 

1034 ) 

1035 """""" 

1036 DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT = commented( 

1037 "The corresponding option is displayed as " 

1038 '"--export-as=json|sh", so json refers to the JSON format (default) ' 

1039 "and sh refers to the POSIX sh format. " 

1040 'Please ensure that it is clear what the "json" and "sh" refer to ' 

1041 "in your translation... even if you cannot use texutal correspondence " 

1042 "like the English text does.", 

1043 )( 

1044 "Label :: Help text :: One-line description", 

1045 "When exporting, export as JSON (default) or as POSIX sh.", 

1046 ) 

1047 """""" 

1048 DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT = commented( 

1049 "The metavar is Label.STORAGE_MANAGEMENT_METAVAR_PATH.", 

1050 )( 

1051 "Label :: Help text :: One-line description", 

1052 "Export all saved settings to {metavar}.", 

1053 flags="python-brace-format", 

1054 ) 

1055 """""" 

1056 DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT = commented( 

1057 "The metavar is Label.STORAGE_MANAGEMENT_METAVAR_PATH.", 

1058 )( 

1059 "Label :: Help text :: One-line description", 

1060 "Import saved settings from {metavar}.", 

1061 flags="python-brace-format", 

1062 ) 

1063 """""" 

1064 DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT = commented( 

1065 "", 

1066 )( 

1067 "Label :: Help text :: One-line description", 

1068 "Select a suitable SSH key from the SSH agent.", 

1069 ) 

1070 """""" 

1071 DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT = commented( 

1072 "The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.", 

1073 )( 

1074 "Label :: Help text :: One-line description", 

1075 "Ensure a passphrase length of {metavar} characters.", 

1076 flags="python-brace-format", 

1077 ) 

1078 """""" 

1079 DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT = commented( 

1080 "The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.", 

1081 )( 

1082 "Label :: Help text :: One-line description", 

1083 "Ensure at least {metavar} lowercase characters.", 

1084 flags="python-brace-format", 

1085 ) 

1086 """""" 

1087 DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT = commented( 

1088 "The metavar is Label.VAULT_METAVAR_SERVICE.", 

1089 )( 

1090 "Label :: Help text :: One-line description", 

1091 "With --config and {service_metavar}, spawn an editor to edit notes.", 

1092 flags="python-brace-format", 

1093 ) 

1094 """""" 

1095 DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT = commented( 

1096 "The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.", 

1097 )( 

1098 "Label :: Help text :: One-line description", 

1099 "Ensure at least {metavar} digits.", 

1100 flags="python-brace-format", 

1101 ) 

1102 """""" 

1103 DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT = commented( 

1104 "The corresponding option is displayed as " 

1105 '"--overwrite-existing / --merge-existing", so you may want to ' 

1106 "hint that the default (merge) is the second of those options.", 

1107 )( 

1108 "Label :: Help text :: One-line description", 

1109 "Overwrite or merge (default) the existing configuration.", 

1110 ) 

1111 """""" 

1112 DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT = commented( 

1113 "", 

1114 )( 

1115 "Label :: Help text :: One-line description", 

1116 "Prompt for a master passphrase.", 

1117 ) 

1118 """""" 

1119 DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT = commented( 

1120 "The corresponding option is displayed as " 

1121 '"--print-notes-before / --print-notes-after", so you may want to ' 

1122 "hint that the default (after) is the second of those options.", 

1123 )( 

1124 "Label :: Help text :: One-line description", 

1125 "Print the notes for {service_metavar} (if any) before or " 

1126 "after (default) the derived passphrase.", 

1127 flags="python-brace-format", 

1128 ) 

1129 """""" 

1130 DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT = commented( 

1131 "The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.", 

1132 )( 

1133 "Label :: Help text :: One-line description", 

1134 "Restrict runs of identical characters to at most {metavar} " 

1135 "characters.", 

1136 flags="python-brace-format", 

1137 ) 

1138 """""" 

1139 DERIVEPASSPHRASE_VAULT_SSH_AGENT_SOCKET_PROVIDER_HELP_TEXT = commented( 

1140 "The metavar is " 

1141 "Label.VAULT_COMPATIBILITY_METAVAR_SSH_AGENT_SOCKET_PROVIDER.", 

1142 )( 

1143 "Label :: Help text :: One-line description", 

1144 "Use {metavar} as the SSH agent socket provider.", 

1145 flags="python-brace-format", 

1146 ) 

1147 """""" 

1148 DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT = commented( 

1149 "The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.", 

1150 )( 

1151 "Label :: Help text :: One-line description", 

1152 "Ensure at least {metavar} spaces.", 

1153 flags="python-brace-format", 

1154 ) 

1155 """""" 

1156 DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT = commented( 

1157 "The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.", 

1158 )( 

1159 "Label :: Help text :: One-line description", 

1160 "Ensure at least {metavar} symbol characters.", 

1161 flags="python-brace-format", 

1162 ) 

1163 """""" 

1164 DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT = commented( 

1165 "The corresponding option is displayed as " 

1166 '"--unset=phrase|key|...|symbol", so the "given setting" is ' 

1167 'referring to "phrase", "key", "lower", ..., or "symbol", ' 

1168 "respectively. " 

1169 '"with --config" here means that the user must also specify ' 

1170 '"--config" for this option to have any effect.', 

1171 )( 

1172 "Label :: Help text :: One-line description", 

1173 "With --config, also unset the given setting. " 

1174 "May be specified multiple times.", 

1175 ) 

1176 """""" 

1177 DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT = commented( 

1178 "The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.", 

1179 )( 

1180 "Label :: Help text :: One-line description", 

1181 "Ensure at least {metavar} uppercase characters.", 

1182 flags="python-brace-format", 

1183 ) 

1184 """""" 

1185 

1186 EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT = commented( 

1187 "See EXPORT_VAULT_FORMAT_HELP_TEXT. " 

1188 'The format names/labels "v0.3", "v0.2" and "storeroom" ' 

1189 "should not be translated.", 

1190 )( 

1191 "Label :: Help text :: One-line description", 

1192 "Default: v0.3, v0.2, storeroom.", 

1193 ) 

1194 """""" 

1195 EXPORT_VAULT_FORMAT_HELP_TEXT = commented( 

1196 "The defaults_hint is Label.EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT, " 

1197 "the metavar is Label.EXPORT_VAULT_FORMAT_METAVAR_FMT.", 

1198 )( 

1199 "Label :: Help text :: One-line description", 

1200 "Try the following storage format {metavar}. " 

1201 "If specified multiple times, the " 

1202 "formats will be tried in order. " 

1203 "{defaults_hint}", 

1204 flags="python-brace-format", 

1205 ) 

1206 """""" 

1207 EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT = commented( 

1208 "See EXPORT_VAULT_KEY_HELP_TEXT.", 

1209 )( 

1210 "Label :: Help text :: One-line description", 

1211 "Default: check the VAULT_KEY, LOGNAME, USER, or USERNAME " 

1212 "environment variables.", 

1213 ) 

1214 """""" 

1215 EXPORT_VAULT_KEY_HELP_TEXT = commented( 

1216 "The defaults_hint is Label.EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT, " 

1217 "the metavar is Label.EXPORT_VAULT_KEY_METAVAR_K.", 

1218 )( 

1219 "Label :: Help text :: One-line description", 

1220 "Use {metavar} as the storage master key. {defaults_hint}", 

1221 flags="python-brace-format", 

1222 ) 

1223 """""" 

1224 HELP_OPTION_HELP_TEXT = commented( 

1225 "", 

1226 )( 

1227 "Label :: Help text :: One-line description", 

1228 "Show this help text, then exit.", 

1229 ) 

1230 """""" 

1231 QUIET_OPTION_HELP_TEXT = commented( 

1232 "", 

1233 )( 

1234 "Label :: Help text :: One-line description", 

1235 "Suppress even warnings; emit only errors.", 

1236 ) 

1237 """""" 

1238 VERBOSE_OPTION_HELP_TEXT = commented( 

1239 "", 

1240 )( 

1241 "Label :: Help text :: One-line description", 

1242 "Emit extra/progress information to standard error.", 

1243 ) 

1244 """""" 

1245 VERSION_OPTION_HELP_TEXT = commented( 

1246 "", 

1247 )( 

1248 "Label :: Help text :: One-line description", 

1249 "Show version and feature information, then exit.", 

1250 ) 

1251 """""" 

1252 COMMANDS_LABEL = commented( 

1253 "", 

1254 )( 

1255 "Label :: Help text :: Option group name", 

1256 "Commands", 

1257 ) 

1258 """""" 

1259 COMPATIBILITY_OPTION_LABEL = commented( 

1260 "", 

1261 )( 

1262 "Label :: Help text :: Option group name", 

1263 "Compatibility and extension options", 

1264 ) 

1265 """""" 

1266 CONFIGURATION_LABEL = commented( 

1267 "", 

1268 )( 

1269 "Label :: Help text :: Option group name", 

1270 "Configuration", 

1271 ) 

1272 """""" 

1273 LOGGING_LABEL = commented( 

1274 "", 

1275 )( 

1276 "Label :: Help text :: Option group name", 

1277 "Logging", 

1278 ) 

1279 """""" 

1280 OPTIONS_LABEL = commented( 

1281 "", 

1282 )( 

1283 "Label :: Help text :: Option group name", 

1284 "Options", 

1285 ) 

1286 """""" 

1287 OTHER_OPTIONS_LABEL = commented( 

1288 "", 

1289 )( 

1290 "Label :: Help text :: Option group name", 

1291 "Other options", 

1292 ) 

1293 """""" 

1294 PASSPHRASE_GENERATION_LABEL = commented( 

1295 "", 

1296 )( 

1297 "Label :: Help text :: Option group name", 

1298 "Passphrase generation", 

1299 ) 

1300 """""" 

1301 STORAGE_MANAGEMENT_LABEL = commented( 

1302 "", 

1303 )( 

1304 "Label :: Help text :: Option group name", 

1305 "Storage management", 

1306 ) 

1307 """""" 

1308 VERSION_INFO_MAJOR_LIBRARY_TEXT = commented( 

1309 "This message reports on the version of a major library " 

1310 'currently in use, such as "cryptography".', 

1311 )( 

1312 "Label :: Info Message", 

1313 "Using {dependency_name_and_version}", 

1314 flags="python-brace-format", 

1315 ) 

1316 """""" 

1317 ENABLED_PEP508_EXTRAS = commented( 

1318 "This is part of the version output, emitting lists of enabled " 

1319 "PEP 508 extras. " 

1320 "A comma-separated English list of items follows, " 

1321 "with standard English punctuation.", 

1322 )( 

1323 "Label :: Info Message:: Table row header", 

1324 "PEP 508 extras:", 

1325 ) 

1326 """""" 

1327 FEATURE_ITEM_ALIASES = commented( 

1328 "This is part of the version output, designating a list of names " 

1329 "as aliases to the previous entry. " 

1330 "A comma-separated English list of items follows. " 

1331 "This label, and the list of names, is included in parentheses: " 

1332 "`entry_name (aliases: alias1, alias2, ...)`.", 

1333 )( 

1334 "Label :: Info Message:: Table row header", 

1335 "aliases:", 

1336 ) 

1337 """""" 

1338 SUPPORTED_DERIVATION_SCHEMES = commented( 

1339 "This is part of the version output, emitting lists of supported " 

1340 "derivation schemes. " 

1341 "A comma-separated English list of items follows, " 

1342 "with standard English punctuation.", 

1343 )( 

1344 "Label :: Info Message:: Table row header", 

1345 "Supported derivation schemes:", 

1346 ) 

1347 """""" 

1348 SUPPORTED_FEATURES = commented( 

1349 "This is part of the version output, emitting lists of supported " 

1350 "features for this subcommand. " 

1351 "A comma-separated English list of items follows, " 

1352 "with standard English punctuation.", 

1353 )( 

1354 "Label :: Info Message:: Table row header", 

1355 "Supported features:", 

1356 ) 

1357 """""" 

1358 SUPPORTED_FOREIGN_CONFIGURATION_FORMATS = commented( 

1359 "This is part of the version output, emitting lists of supported " 

1360 "foreign configuration formats. " 

1361 "A comma-separated English list of items follows, " 

1362 "with standard English punctuation.", 

1363 )( 

1364 "Label :: Info Message:: Table row header", 

1365 "Supported foreign configuration formats:", 

1366 ) 

1367 """""" 

1368 SUPPORTED_SSH_AGENT_SOCKET_PROVIDERS = commented( 

1369 "This is part of the version output, emitting lists of supported " 

1370 "SSH agent socket providers. " 

1371 "A comma-separated English list of items follows, " 

1372 "with standard English punctuation.", 

1373 )( 

1374 "Label :: Info Message:: Table row header", 

1375 "Supported SSH agent socket providers:", 

1376 ) 

1377 """""" 

1378 SUPPORTED_SUBCOMMANDS = commented( 

1379 "This is part of the version output, emitting lists of supported " 

1380 "subcommands. " 

1381 "A comma-separated English list of items follows, " 

1382 "with standard English punctuation.", 

1383 )( 

1384 "Label :: Info Message:: Table row header", 

1385 "Supported subcommands:", 

1386 ) 

1387 """""" 

1388 UNAVAILABLE_DERIVATION_SCHEMES = commented( 

1389 "This is part of the version output, emitting lists of known, " 

1390 "unavailable derivation schemes. " 

1391 "A comma-separated English list of items follows, " 

1392 "with standard English punctuation.", 

1393 )( 

1394 "Label :: Info Message:: Table row header", 

1395 "Known derivation schemes:", 

1396 ) 

1397 """""" 

1398 UNAVAILABLE_FEATURES = commented( 

1399 "This is part of the version output, emitting lists of known, " 

1400 "unavailable features for this subcommand. " 

1401 "A comma-separated English list of items follows, " 

1402 "with standard English punctuation.", 

1403 )( 

1404 "Label :: Info Message:: Table row header", 

1405 "Known features:", 

1406 ) 

1407 """""" 

1408 UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS = commented( 

1409 "This is part of the version output, emitting lists of known, " 

1410 "unavailable foreign configuration formats. " 

1411 "A comma-separated English list of items follows, " 

1412 "with standard English punctuation.", 

1413 )( 

1414 "Label :: Info Message:: Table row header", 

1415 "Known foreign configuration formats:", 

1416 ) 

1417 """""" 

1418 UNAVAILABLE_SSH_AGENT_SOCKET_PROVIDERS = commented( 

1419 "This is part of the version output, emitting lists of known, " 

1420 "unavailable SSH agent socket providers. " 

1421 "A comma-separated English list of items follows, " 

1422 "with standard English punctuation.", 

1423 )( 

1424 "Label :: Info Message:: Table row header", 

1425 "Known SSH agent socket providers:", 

1426 ) 

1427 """""" 

1428 CONFIRM_THIS_CHOICE_PROMPT_TEXT = commented( 

1429 'There is no support for "yes" or "no" in other languages ' 

1430 "than English, so it is advised that your translation makes it " 

1431 'clear that only the strings "y", "yes", "n" or "no" are supported, ' 

1432 "even if the prompt becomes a bit longer.", 

1433 )( 

1434 "Label :: Interactive prompt", 

1435 "Confirm this choice? (y/N)", 

1436 ) 

1437 """""" 

1438 SUITABLE_SSH_KEYS_LABEL = commented( 

1439 "This label is the heading of the list of suitable SSH keys.", 

1440 )( 

1441 "Label :: Interactive prompt", 

1442 "Suitable SSH keys:", 

1443 ) 

1444 """""" 

1445 YOUR_SELECTION_PROMPT_TEXT = commented( 

1446 "", 

1447 )( 

1448 "Label :: Interactive prompt", 

1449 "Your selection? (1-{n}, leave empty to abort)", 

1450 flags="python-brace-format", 

1451 ) 

1452 """""" 

1453 

1454 

1455class DebugMsgTemplate(enum.Enum): 

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

1457 

1458 BUCKET_ITEM_FOUND = commented( 

1459 "This message is emitted by the vault configuration exporter " 

1460 'for "storeroom"-type configuration directories. ' 

1461 'The system stores entries in different "buckets" of a hash table. ' 

1462 "Here, we report on a single item (path and value) we discovered " 

1463 "after decrypting the whole bucket. " 

1464 "(We ensure the path and value are printable as-is.)", 

1465 )( 

1466 "Debug message", 

1467 "Found bucket item: {path} -> {value}", 

1468 flags="python-brace-format", 

1469 ) 

1470 """""" 

1471 DECRYPT_BUCKET_ITEM_INFO = commented( 

1472 '"AES256-CBC" and "PKCS#7" are, in essence, names of formats, ' 

1473 "and should not be translated. " 

1474 '"IV" means "initialization vector", and is specifically ' 

1475 'a cryptographic term, as are "plaintext" and "ciphertext".', 

1476 )( 

1477 "Debug message", 

1478 """\ 

1479Decrypt bucket item contents: 

1480 

1481\b 

1482 Encryption key (master key): {enc_key} 

1483 Encryption cipher: AES256-CBC with PKCS#7 padding 

1484 Encryption IV: {iv} 

1485 Encrypted ciphertext: {ciphertext} 

1486 Plaintext: {plaintext} 

1487""", 

1488 flags="python-brace-format", 

1489 ) 

1490 """""" 

1491 DECRYPT_BUCKET_ITEM_KEY_INFO = commented( 

1492 "", 

1493 )( 

1494 "Debug message", 

1495 """\ 

1496Decrypt bucket item: 

1497 

1498\b 

1499 Plaintext: {plaintext} 

1500 Encryption key (master key): {enc_key} 

1501 Signing key (master key): {sign_key} 

1502""", 

1503 flags="python-brace-format", 

1504 ) 

1505 """""" 

1506 DECRYPT_BUCKET_ITEM_MAC_INFO = commented( 

1507 'The MAC stands for "message authentication code", ' 

1508 "which guarantees the authenticity of the message to anyone " 

1509 "who holds the corresponding key, similar to a digital signature. " 

1510 'The acronym "MAC" is assumed to be well-known to the ' 

1511 "English target audience, or at least discoverable by them; " 

1512 "they *are* asking for debug output, after all. " 

1513 "Please use your judgement as to whether to translate this term " 

1514 "or not, expanded or not.", 

1515 )( 

1516 "Debug message", 

1517 """\ 

1518Decrypt bucket item contents: 

1519 

1520\b 

1521 MAC key: {sign_key} 

1522 Authenticated content: {ciphertext} 

1523 Claimed MAC value: {claimed_mac} 

1524 Computed MAC value: {actual_mac} 

1525""", 

1526 flags="python-brace-format", 

1527 ) 

1528 """""" 

1529 DECRYPT_BUCKET_ITEM_SESSION_KEYS_INFO = commented( 

1530 '"AES256-CBC" and "PKCS#7" are, in essence, names of formats, ' 

1531 "and should not be translated. " 

1532 '"IV" means "initialization vector", and is specifically ' 

1533 'a cryptographic term, as are "plaintext" and "ciphertext".', 

1534 )( 

1535 "Debug message", 

1536 """\ 

1537Decrypt bucket item session keys: 

1538 

1539\b 

1540 Encryption key (master key): {enc_key} 

1541 Encryption cipher: AES256-CBC with PKCS#7 padding 

1542 Encryption IV: {iv} 

1543 Encrypted ciphertext: {ciphertext} 

1544 Plaintext: {plaintext} 

1545 Parsed plaintext: {code} 

1546""", 

1547 flags="python-brace-format", 

1548 ) 

1549 """""" 

1550 DECRYPT_BUCKET_ITEM_SESSION_KEYS_MAC_INFO = commented( 

1551 'The MAC stands for "message authentication code", ' 

1552 "which guarantees the authenticity of the message to anyone " 

1553 "who holds the corresponding key, similar to a digital signature. " 

1554 'The acronym "MAC" is assumed to be well-known to the ' 

1555 "English target audience, or at least discoverable by them; " 

1556 "they *are* asking for debug output, after all. " 

1557 "Please use your judgement as to whether to translate this term " 

1558 "or not, expanded or not.", 

1559 )( 

1560 "Debug message", 

1561 """\ 

1562Decrypt bucket item session keys: 

1563 

1564\b 

1565 MAC key (master key): {sign_key} 

1566 Authenticated content: {ciphertext} 

1567 Claimed MAC value: {claimed_mac} 

1568 Computed MAC value: {actual_mac} 

1569""", 

1570 flags="python-brace-format", 

1571 ) 

1572 """""" 

1573 DERIVED_MASTER_KEYS_KEYS = commented( 

1574 "", 

1575 )( 

1576 "Debug message", 

1577 """\ 

1578Derived master keys' keys: 

1579 

1580\b 

1581 Encryption key: {enc_key} 

1582 Signing key: {sign_key} 

1583 Password: {pw_bytes} 

1584 Function call: pbkdf2(algorithm={algorithm!r}, length={length!r}, salt={salt!r}, iterations={iterations!r}) 

1585""", # noqa: E501 

1586 flags="python-brace-format", 

1587 ) 

1588 """""" 

1589 DIRECTORY_CONTENTS_CHECK_OK = commented( 

1590 "This message is emitted by the vault configuration exporter " 

1591 'for "storeroom"-type configuration directories, ' 

1592 'while "assembling" the items stored in the configuration ' 

1593 """according to the item's "path". """ 

1594 'Each "directory" in the path contains a list of children ' 

1595 "it claims to contain, and this list must be matched " 

1596 "against the actual discovered items. " 

1597 "Now, at the end, we actually confirm the claim. " 

1598 "(We would have already thrown an error here otherwise.)", 

1599 )( 

1600 "Debug message", 

1601 "Directory contents check OK: {path} -> {contents}", 

1602 flags="python-brace-format", 

1603 ) 

1604 """""" 

1605 MASTER_KEYS_DATA_MAC_INFO = commented( 

1606 'The MAC stands for "message authentication code", ' 

1607 "which guarantees the authenticity of the message to anyone " 

1608 "who holds the corresponding key, similar to a digital signature. " 

1609 'The acronym "MAC" is assumed to be well-known to the ' 

1610 "English target audience, or at least discoverable by them; " 

1611 "they *are* asking for debug output, after all. " 

1612 "Please use your judgement as to whether to translate this term " 

1613 "or not, expanded or not.", 

1614 )( 

1615 "Debug message", 

1616 """\ 

1617Master keys data: 

1618 

1619\b 

1620 MAC key: {sign_key} 

1621 Authenticated content: {ciphertext} 

1622 Claimed MAC value: {claimed_mac} 

1623 Computed MAC value: {actual_mac} 

1624""", 

1625 flags="python-brace-format", 

1626 ) 

1627 """""" 

1628 POSTPONING_DIRECTORY_CONTENTS_CHECK = commented( 

1629 "This message is emitted by the vault configuration exporter " 

1630 'for "storeroom"-type configuration directories, ' 

1631 'while "assembling" the items stored in the configuration ' 

1632 """according to the item's "path". """ 

1633 'Each "directory" in the path contains a list of children ' 

1634 "it claims to contain, and this list must be matched " 

1635 "against the actual discovered items. " 

1636 "When emitting this message, we merely indicate that we saved " 

1637 'the "claimed" list for this directory for later.', 

1638 )( 

1639 "Debug message", 

1640 "Postponing directory contents check: {path} -> {contents}", 

1641 flags="python-brace-format", 

1642 ) 

1643 """""" 

1644 SETTING_CONFIG_STRUCTURE_CONTENTS = commented( 

1645 "This message is emitted by the vault configuration exporter " 

1646 'for "storeroom"-type configuration directories, ' 

1647 'while "assembling" the items stored in the configuration ' 

1648 """according to the item's "path". """ 

1649 "We confirm that we set the entry at the given path " 

1650 "to the given value.", 

1651 )( 

1652 "Debug message", 

1653 "Setting contents: {path} -> {value}", 

1654 flags="python-brace-format", 

1655 ) 

1656 """""" 

1657 SETTING_CONFIG_STRUCTURE_CONTENTS_EMPTY_DIRECTORY = commented( 

1658 "This message is emitted by the vault configuration exporter " 

1659 'for "storeroom"-type configuration directories, ' 

1660 'while "assembling" the items stored in the configuration ' 

1661 """according to the item's "path". """ 

1662 "We confirm that we set up a currently empty directory " 

1663 "at the given path.", 

1664 )( 

1665 "Debug message", 

1666 "Setting contents (empty directory): {path}", 

1667 flags="python-brace-format", 

1668 ) 

1669 """""" 

1670 VAULT_NATIVE_CHECKING_MAC_DETAILS = commented( 

1671 "This message is emitted by the vault configuration exporter " 

1672 'for "native"-type configuration directories. ' 

1673 "It is preceded by the info message " 

1674 "VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC; see the commentary there " 

1675 "concerning the terms and thoughts on translating them.", 

1676 )( 

1677 "Debug message", 

1678 """\ 

1679MAC details: 

1680 

1681\b 

1682 MAC input: {mac_input} 

1683 Expected MAC: {mac} 

1684""", 

1685 flags="python-brace-format", 

1686 ) 

1687 """""" 

1688 VAULT_NATIVE_EVP_BYTESTOKEY_INIT = commented( 

1689 "This message is emitted by the vault configuration exporter " 

1690 'for "native"-type configuration directories: ' 

1691 'in v0.2, the non-standard and deprecated "EVP_bytestokey" function ' 

1692 "from OpenSSL must be reimplemented from scratch. " 

1693 'The terms "salt" and "IV" (initialization vector) ' 

1694 "are cryptographic terms.", 

1695 )( 

1696 "Debug message", 

1697 """\ 

1698evp_bytestokey_md5 (initialization): 

1699 

1700\b 

1701 Input: {data} 

1702 Salt: {salt} 

1703 Key size: {key_size} 

1704 IV size: {iv_size} 

1705 Buffer length: {buffer_length} 

1706 Buffer: {buffer} 

1707""", 

1708 flags="python-brace-format", 

1709 ) 

1710 """""" 

1711 VAULT_NATIVE_EVP_BYTESTOKEY_RESULT = commented( 

1712 "This message is emitted by the vault configuration exporter " 

1713 'for "native"-type configuration directories: ' 

1714 'in v0.2, the non-standard and deprecated "EVP_bytestokey" function ' 

1715 "from OpenSSL must be reimplemented from scratch. " 

1716 'The terms "salt" and "IV" (initialization vector) ' 

1717 "are cryptographic terms." 

1718 "This function reports on the final results.", 

1719 )( 

1720 "Debug message", 

1721 """\ 

1722evp_bytestokey_md5 (result): 

1723 

1724\b 

1725 Encryption key: {enc_key} 

1726 IV: {iv} 

1727""", 

1728 flags="python-brace-format", 

1729 ) 

1730 """""" 

1731 VAULT_NATIVE_EVP_BYTESTOKEY_ROUND = commented( 

1732 "This message is emitted by the vault configuration exporter " 

1733 'for "native"-type configuration directories: ' 

1734 'in v0.2, the non-standard and deprecated "EVP_bytestokey" function ' 

1735 "from OpenSSL must be reimplemented from scratch. " 

1736 'The terms "salt" and "IV" (initialization vector) ' 

1737 "are cryptographic terms." 

1738 "This function reports on the updated buffer length and contents " 

1739 "after executing one round of hashing.", 

1740 )( 

1741 "Debug message", 

1742 """\ 

1743evp_bytestokey_md5 (round update): 

1744 

1745\b 

1746 Buffer length: {buffer_length} 

1747 Buffer: {buffer} 

1748""", 

1749 flags="python-brace-format", 

1750 ) 

1751 """""" 

1752 VAULT_NATIVE_PADDED_PLAINTEXT = commented( 

1753 "This message is emitted by the vault configuration exporter " 

1754 'for "native"-type configuration directories. ' 

1755 '"padding" and "plaintext" are cryptographic terms.', 

1756 )( 

1757 "Debug message", 

1758 "Padded plaintext: {contents}", 

1759 flags="python-brace-format", 

1760 ) 

1761 """""" 

1762 VAULT_NATIVE_PARSE_BUFFER = commented( 

1763 "This message is emitted by the vault configuration exporter " 

1764 'for "native"-type configuration directories. ' 

1765 "It is preceded by the info message " 

1766 "VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC; see the commentary there " 

1767 "concerning the terms and thoughts on translating them.", 

1768 )( 

1769 "Debug message", 

1770 """\ 

1771Buffer: {contents} 

1772 

1773\b 

1774 IV: {iv} 

1775 Payload (ciphertext): {payload} 

1776 MAC: {mac} 

1777""", 

1778 flags="python-brace-format", 

1779 ) 

1780 """""" 

1781 VAULT_NATIVE_PBKDF2_CALL = commented( 

1782 "", 

1783 )( 

1784 "Debug message", 

1785 """\ 

1786Master key derivation: 

1787 

1788\b 

1789 PBKDF2 call: PBKDF2-HMAC(password={password!r}, salt={salt!r}, iterations={iterations!r}, key_size={key_size!r}, algorithm={algorithm!r}) 

1790 Result (binary): {raw_result} 

1791 Result (hex key): {result_key!r} 

1792""", # noqa: E501 

1793 flags="python-brace-format", 

1794 ) 

1795 """""" 

1796 VAULT_NATIVE_PLAINTEXT = commented( 

1797 "This message is emitted by the vault configuration exporter " 

1798 'for "native"-type configuration directories. ' 

1799 '"plaintext" is a cryptographic term.', 

1800 )( 

1801 "Debug message", 

1802 "Plaintext: {contents}", 

1803 flags="python-brace-format", 

1804 ) 

1805 """""" 

1806 VAULT_NATIVE_V02_PAYLOAD_MAC_POSTPROCESSING = commented( 

1807 "This message is emitted by the vault configuration exporter " 

1808 'for "native"-type configuration directories. ' 

1809 "It is preceded by the info message " 

1810 "VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC and the debug message " 

1811 "PARSING_NATIVE_PARSE_BUFFER; see the commentary there concerning " 

1812 "the terms and thoughts on translating them.", 

1813 )( 

1814 "Debug message", 

1815 """\ 

1816Postprocessing buffer (v0.2): 

1817 

1818\b 

1819 Payload: {payload} (decoded from base64) 

1820 MAC: {mac} (decoded from hex) 

1821""", 

1822 flags="python-brace-format", 

1823 ) 

1824 """""" 

1825 

1826 

1827class InfoMsgTemplate(enum.Enum): 

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

1829 

1830 ASSEMBLING_CONFIG_STRUCTURE = commented( 

1831 "This message is emitted by the vault configuration exporter " 

1832 'for "storeroom"-type configuration directories. ' 

1833 'The system stores entries in different "buckets" of a hash table. ' 

1834 "After the respective items in the buckets have been decrypted, " 

1835 "we then have a list of item paths plus contents to populate. " 

1836 "This must be done in a certain order (we don't yet have an " 

1837 "existing directory tree to rely on, but rather must " 

1838 'build it on-the-fly), hence the term "assembling".', 

1839 )( 

1840 "Info message", 

1841 "Assembling config structure.", 

1842 ) 

1843 """""" 

1844 CANNOT_LOAD_AS_VAULT_CONFIG = commented( 

1845 '"fmt" is a string such as "v0.2" or "storeroom", ' 

1846 "indicating the format which we tried to load the " 

1847 "vault configuration as.", 

1848 )( 

1849 "Info message", 

1850 "Cannot load {path!r} as a {fmt} vault configuration.", 

1851 flags="python-brace-format", 

1852 ) 

1853 """""" 

1854 CHECKING_CONFIG_STRUCTURE_CONSISTENCY = commented( 

1855 "This message is emitted by the vault configuration exporter " 

1856 'for "storeroom"-type configuration directories. ' 

1857 'Having "assembled" the configuration items according to ' 

1858 "their claimed paths and contents, we then check if the " 

1859 "assembled structure is internally consistent.", 

1860 )( 

1861 "Info message", 

1862 "Checking config structure consistency.", 

1863 ) 

1864 """""" 

1865 DECRYPTING_BUCKET = commented( 

1866 "This message is emitted by the vault configuration exporter " 

1867 'for "storeroom"-type configuration directories. ' 

1868 'The system stores entries in different "buckets" of a hash table. ' 

1869 "We parse the directory bucket by bucket. " 

1870 "All buckets are numbered in hexadecimal, and typically there are " 

1871 "32 buckets, so 2-digit hex numbers.", 

1872 )( 

1873 "Info message", 

1874 "Decrypting bucket {bucket_number}.", 

1875 flags="python-brace-format", 

1876 ) 

1877 """""" 

1878 PARSING_MASTER_KEYS_DATA = commented( 

1879 "This message is emitted by the vault configuration exporter " 

1880 'for "storeroom"-type configuration directories. ' 

1881 "`.keys` is a filename, from which data about the master keys " 

1882 "for this configuration are loaded.", 

1883 )( 

1884 "Info message", 

1885 "Parsing master keys data from `.keys`.", 

1886 ) 

1887 """""" 

1888 PIP_INSTALL_EXTRA = commented( 

1889 "This message immediately follows an error message about " 

1890 "a missing library that needs to be installed. " 

1891 "The Python Package Index (PyPI) supports declaring sets of " 

1892 'optional dependencies as "extras", so users installing from PyPI ' 

1893 'can request reinstallation with a named "extra" being enabled. ' 

1894 "This would then let the installer take care of the " 

1895 "missing libraries automatically, " 

1896 "hence this suggestion to PyPI users.", 

1897 )( 

1898 "Info message", 

1899 "For users installing from PyPI, see the {extra_name!r} extra.", 

1900 flags="python-brace-format", 

1901 ) 

1902 """""" 

1903 SUCCESSFULLY_MIGRATED = commented( 

1904 "This info message immediately follows the " 

1905 '"Using deprecated v0.1-style ..." deprecation warning.', 

1906 )( 

1907 "Info message", 

1908 "Successfully migrated to {path!r}.", 

1909 flags="python-brace-format", 

1910 ) 

1911 """""" 

1912 VAULT_NATIVE_CHECKING_MAC = commented( 

1913 "", 

1914 )( 

1915 "Info message", 

1916 "Checking MAC.", 

1917 ) 

1918 """""" 

1919 VAULT_NATIVE_DECRYPTING_CONTENTS = commented( 

1920 "", 

1921 )( 

1922 "Info message", 

1923 "Decrypting contents.", 

1924 ) 

1925 """""" 

1926 VAULT_NATIVE_DERIVING_KEYS = commented( 

1927 "", 

1928 )( 

1929 "Info message", 

1930 "Deriving an encryption and signing key.", 

1931 ) 

1932 """""" 

1933 VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC = commented( 

1934 "This message is emitted by the vault configuration exporter " 

1935 'for "native"-type configuration directories. ' 

1936 '"IV" means "initialization vector", and "MAC" means ' 

1937 '"message authentication code". ' 

1938 'They are specifically cryptographic terms, as is "payload". ' 

1939 'The acronyms "IV" and "MAC" are assumed to be well-known to the ' 

1940 "English target audience, or at least discoverable by them; " 

1941 "they *are* asking for debug output, after all. " 

1942 "Please use your judgement as to whether to translate these terms " 

1943 "or not, expanded or not.", 

1944 )( 

1945 "Info message", 

1946 "Parsing IV, payload and MAC from the file contents.", 

1947 ) 

1948 """""" 

1949 

1950 

1951class WarnMsgTemplate(enum.Enum): 

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

1953 

1954 EDITING_NOTES_BUT_NOT_STORING_CONFIG = commented( 

1955 "", 

1956 )( 

1957 "Warning message", 

1958 "Specifying --notes without --config is ineffective. " 

1959 "No notes will be edited.", 

1960 ) 

1961 EMPTY_SERVICE_NOT_SUPPORTED = commented( 

1962 "", 

1963 )( 

1964 "Warning message", 

1965 "An empty {service_metavar} is not supported by vault(1). " 

1966 "For compatibility, this will be treated as if " 

1967 "{service_metavar} was not supplied, i.e., it will error out, " 

1968 "or operate on global settings.", 

1969 flags="python-brace-format", 

1970 ) 

1971 """""" 

1972 EMPTY_SERVICE_SETTINGS_INACCESSIBLE = commented( 

1973 "", 

1974 )( 

1975 "Warning message", 

1976 "An empty {service_metavar} is not supported by vault(1). " 

1977 "The empty-string service settings will be inaccessible " 

1978 "and ineffective. " 

1979 "To ensure that vault(1) and {PROG_NAME} see the settings, " # noqa: RUF027 

1980 'move them into the "global" section.', 

1981 flags="python-brace-format", 

1982 ) 

1983 """""" 

1984 FAILED_TO_MIGRATE_CONFIG = commented( 

1985 '"error" is supplied by the operating system (errno/strerror).', 

1986 )( 

1987 "Warning message", 

1988 "Failed to migrate to {path!r}: {error}: {filename!r}.", 

1989 flags="python-brace-format", 

1990 ) 

1991 """""" 

1992 GLOBAL_PASSPHRASE_INEFFECTIVE = commented( 

1993 "", 

1994 )( 

1995 "Warning message", 

1996 "Setting a global passphrase is ineffective " 

1997 "because a key is also set.", 

1998 ) 

1999 """""" 

2000 LEGACY_EDITOR_INTERFACE_NOTES_BACKUP = commented( 

2001 "", 

2002 )( 

2003 "Warning message", 

2004 "A backup copy of the old notes was saved to {filename!r}. " 

2005 "This is a safeguard against editing mistakes, because the " 

2006 "vault(1)-compatible legacy editor interface does not allow " 

2007 "aborting mid-edit, and because the notes were actually changed.", 

2008 flags="python-brace-format", 

2009 ) 

2010 NO_UNIX_DOMAIN_SOCKETS = commented( 

2011 'This is one of several "Cannot connect to an SSH agent via ' 

2012 "<communication_channel_type> because this Python version " 

2013 'does not support them." messages issued ' 

2014 "when searching for an applicable SSH agent socket provider. " 

2015 "(WarnMsgTemplate.NO_WINDOWS_NAMED_PIPES, " 

2016 "ErrMsgTemplate.NO_AGENT_SUPPORT.) " 

2017 "The user may see multiple such messages in immediate succession " 

2018 "as socket providers are tried out one by one. " 

2019 "If feasible, translations of these messages should therefore " 

2020 "be grammatically similar, so that the user can clearly " 

2021 "recognize them as variants of essentially the same message.", 

2022 )( 

2023 "Warning message", 

2024 "Cannot connect to an SSH agent via UNIX domain sockets " 

2025 "because this Python version does not support them.", 

2026 ) 

2027 """""" 

2028 NO_WINDOWS_NAMED_PIPES = commented( 

2029 'This is one of several "Cannot connect to an SSH agent via ' 

2030 "<communication_channel_type> because this Python version " 

2031 'does not support them." messages issued ' 

2032 "when searching for an applicable SSH agent socket provider. " 

2033 "(WarnMsgTemplate.NO_UNIX_DOMAIN_SOCKETS, " 

2034 "ErrMsgTemplate.NO_AGENT_SUPPORT.) " 

2035 "The user may see multiple such messages in immediate succession " 

2036 "as socket providers are tried out one by one. " 

2037 "If feasible, translations of these messages should therefore " 

2038 "be grammatically similar, so that the user can clearly " 

2039 "recognize them as variants of essentially the same message.", 

2040 )( 

2041 "Warning message", 

2042 "Cannot connect to an SSH agent via Windows named pipes " 

2043 "because this Python version does not support them.", 

2044 ) 

2045 """""" 

2046 PASSPHRASE_NOT_NORMALIZED = commented( 

2047 "The key is a (vault) configuration key, in JSONPath syntax, " 

2048 'typically "$.global" for the global passphrase or ' 

2049 '"$.services.service_name" or "$.services["service with spaces"]" ' 

2050 'for the services "service_name" and "service with spaces", ' 

2051 "respectively. " 

2052 "Alternatively, it may be the value of " 

2053 "Label.SETTINGS_ORIGIN_INTERACTIVE if the passphrase was " 

2054 "entered interactively. " 

2055 "The form is one of the four Unicode normalization forms: " 

2056 "NFC, NFD, NFKC, NFKD. " 

2057 "The asterisks are not special. " 

2058 "Please feel free to substitute any other appropriate way to " 

2059 'mark up emphasis of the word "displays".', 

2060 )( 

2061 "Warning message", 

2062 "The {key} passphrase is not {form}-normalized. " 

2063 "Its serialization as a byte string may not be what you " 

2064 "expect it to be, even if it *displays* correctly. " 

2065 "Please make sure to double-check any derived passphrases " 

2066 "for unexpected results.", 

2067 flags="python-brace-format", 

2068 ) 

2069 """""" 

2070 SERVICE_NAME_INCOMPLETABLE = commented( 

2071 "", 

2072 )( 

2073 "Warning message", 

2074 "The service name {service!r} contains an ASCII control character, " 

2075 "which is not supported by our shell completion code. " 

2076 "This service name will therefore not be available for completion " 

2077 "on the command-line. " 

2078 "You may of course still type it in manually in whatever format " 

2079 "your shell accepts, but we highly recommend choosing a different " 

2080 "service name instead.", 

2081 flags="python-brace-format", 

2082 ) 

2083 """""" 

2084 SERVICE_PASSPHRASE_INEFFECTIVE = commented( 

2085 "The key that is set need not necessarily be set at the " 

2086 "service level; it may be a global key as well.", 

2087 )( 

2088 "Warning message", 

2089 "Setting a service passphrase is ineffective " 

2090 "because a key is also set: {service}.", 

2091 flags="python-brace-format", 

2092 ) 

2093 """""" 

2094 STEP_REMOVE_INEFFECTIVE_VALUE = commented( 

2095 "", 

2096 )( 

2097 "Warning message", 

2098 "Removing ineffective setting {path} = {old}.", 

2099 flags="python-brace-format", 

2100 ) 

2101 """""" 

2102 STEP_REPLACE_INVALID_VALUE = commented( 

2103 "", 

2104 )( 

2105 "Warning message", 

2106 "Replacing invalid value {old} for key {path} with {new}.", 

2107 flags="python-brace-format", 

2108 ) 

2109 """""" 

2110 V01_STYLE_CONFIG = commented( 

2111 "", 

2112 )( 

2113 "Warning message :: Deprecation", 

2114 "Using deprecated v0.1-style config file {old!r}, " 

2115 "instead of v0.2-style {new!r}. " 

2116 "Support for v0.1-style config filenames will be removed in v1.0.", 

2117 flags="python-brace-format", 

2118 ) 

2119 """""" 

2120 V10_SUBCOMMAND_REQUIRED = commented( 

2121 "This deprecation warning may be issued at any level, " 

2122 "i.e. we may actually be talking about subcommands, " 

2123 "or sub-subcommands, or sub-sub-subcommands, etc., " 

2124 'which is what the "here" is supposed to indicate.', 

2125 )( 

2126 "Warning message :: Deprecation", 

2127 "A subcommand will be required here in v1.0. " 

2128 "See --help for available subcommands. " 

2129 'Defaulting to subcommand "vault".', 

2130 ) 

2131 """""" 

2132 

2133 

2134class ErrMsgTemplate(enum.Enum): 

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

2136 

2137 AGENT_REFUSED_LIST_KEYS = commented( 

2138 '"loaded keys" being keys loaded into the agent.', 

2139 )( 

2140 "Error message", 

2141 "The SSH agent failed to or refused to supply a list of loaded keys.", 

2142 ) 

2143 """""" 

2144 AGENT_REFUSED_SIGNATURE = commented( 

2145 "The message to be signed is the vault UUID, " 

2146 "but there's no space to explain that here, " 

2147 "so ideally the error message does not go into detail.", 

2148 )( 

2149 "Error message", 

2150 "The SSH agent failed to or refused to issue a signature " 

2151 "with the selected key, necessary for deriving a service passphrase.", 

2152 ) 

2153 """""" 

2154 CANNOT_CONNECT_TO_AGENT = commented( 

2155 '"error" is supplied by the operating system (errno/strerror).', 

2156 )( 

2157 "Error message", 

2158 "Cannot connect to the SSH agent: {error}: {filename!r}.", 

2159 flags="python-brace-format", 

2160 ) 

2161 """""" 

2162 CANNOT_DECODEIMPORT_VAULT_SETTINGS = commented( 

2163 '"error" is supplied by the operating system (errno/strerror).', 

2164 )( 

2165 "Error message", 

2166 "Cannot import vault settings: cannot decode JSON: {error}.", 

2167 flags="python-brace-format", 

2168 ) 

2169 """""" 

2170 CANNOT_EXPORT_VAULT_SETTINGS = commented( 

2171 '"error" is supplied by the operating system (errno/strerror).', 

2172 )( 

2173 "Error message", 

2174 "Cannot export vault settings: {error}: {filename!r}.", 

2175 flags="python-brace-format", 

2176 ) 

2177 """""" 

2178 CANNOT_IMPORT_VAULT_SETTINGS = commented( 

2179 '"error" is supplied by the operating system (errno/strerror).', 

2180 )( 

2181 "Error message", 

2182 "Cannot import vault settings: {error}: {filename!r}.", 

2183 flags="python-brace-format", 

2184 ) 

2185 """""" 

2186 CANNOT_LOAD_USER_CONFIG = commented( 

2187 '"error" is supplied by the operating system (errno/strerror).', 

2188 )( 

2189 "Error message", 

2190 "Cannot load user config: {error}: {filename!r}.", 

2191 flags="python-brace-format", 

2192 ) 

2193 """""" 

2194 CANNOT_LOAD_VAULT_SETTINGS = commented( 

2195 '"error" is supplied by the operating system (errno/strerror).', 

2196 )( 

2197 "Error message", 

2198 "Cannot load vault settings: {error}: {filename!r}.", 

2199 flags="python-brace-format", 

2200 ) 

2201 """""" 

2202 CANNOT_PARSE_AS_VAULT_CONFIG = commented( 

2203 'Unlike the "Cannot load {path!r} as a {fmt} ' 

2204 'vault configuration." message, *this* error message is emitted ' 

2205 "when we have tried loading the path in each of our " 

2206 "supported formats, and failed. " 

2207 'The user will thus see the above "Cannot load ..." warning message ' 

2208 "potentially multiple times, " 

2209 "and this error message at the very bottom.", 

2210 )( 

2211 "Error message", 

2212 "Cannot parse {path!r} as a valid vault-native " 

2213 "configuration file/directory.", 

2214 flags="python-brace-format", 

2215 ) 

2216 """""" 

2217 CANNOT_PARSE_AS_VAULT_CONFIG_OSERROR = commented( 

2218 '"error" is supplied by the operating system (errno/strerror).', 

2219 )( 

2220 "Error message", 

2221 r"Cannot parse {path!r} as a valid vault-native " 

2222 "configuration file/directory: {error}: {filename!r}.", 

2223 flags="python-brace-format", 

2224 ) 

2225 """""" 

2226 CANNOT_STORE_VAULT_SETTINGS = commented( 

2227 '"error" is supplied by the operating system (errno/strerror).', 

2228 )( 

2229 "Error message", 

2230 "Cannot store vault settings: {error}: {filename!r}.", 

2231 flags="python-brace-format", 

2232 ) 

2233 """""" 

2234 CANNOT_UNDERSTAND_AGENT = commented( 

2235 "This error message is used whenever we cannot make " 

2236 "any sense of a response from the SSH agent " 

2237 "because the response is ill-formed " 

2238 "(truncated, improperly encoded, etc.) " 

2239 "or otherwise violates the communications protocol. " 

2240 "Well-formed responses that adhere to the protocol, " 

2241 "even if they indicate that the requested operation failed, " 

2242 "are handled with a different error message.", 

2243 )( 

2244 "Error message", 

2245 "Cannot understand the SSH agent's response because it " 

2246 "violates the communication protocol.", 

2247 ) 

2248 """""" 

2249 CANNOT_UPDATE_SETTINGS_NO_SETTINGS = commented( 

2250 "The settings_type metavar contains translations for " 

2251 'either "global settings" or "service-specific settings"; ' 

2252 "see the CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL and " 

2253 "CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE entries. " 

2254 "The first sentence will thus read either " 

2255 '"Cannot update the global settings without any given settings." or ' 

2256 '"Cannot update the service-specific settings without any ' 

2257 'given settings.". ' 

2258 "You may update this entry, and the two metavar entries, " 

2259 "in any way you see fit that achieves the desired translations " 

2260 "of the first sentence.", 

2261 )( 

2262 "Error message", 

2263 "Cannot update the {settings_type} without any given settings. " 

2264 "You must specify at least one of --lower, ..., --symbol, --notes, " 

2265 "or --phrase or --key.", 

2266 flags="python-brace-format", 

2267 ) 

2268 """""" 

2269 INVALID_USER_CONFIG = commented( 

2270 '"error" is supplied by the operating system (errno/strerror).', 

2271 )( 

2272 "Error message", 

2273 "The user configuration file is invalid. {error}: {filename!r}.", 

2274 flags="python-brace-format", 

2275 ) 

2276 """""" 

2277 INVALID_VAULT_CONFIG = commented( 

2278 "This error message is a reaction to a validator function " 

2279 "saying *that* the configuration is not valid, " 

2280 "but not *how* it is not valid. " 

2281 "The configuration file is principally parsable, however.", 

2282 )( 

2283 "Error message", 

2284 "Invalid vault config: {config!r}.", 

2285 flags="python-brace-format", 

2286 ) 

2287 """""" 

2288 MISSING_MODULE = commented( 

2289 "", 

2290 )( 

2291 "Error message", 

2292 "Cannot load the required Python module {module!r}.", 

2293 flags="python-brace-format", 

2294 ) 

2295 """""" 

2296 NO_AGENT_SUPPORT = commented( 

2297 "This error message works in tandem with, " 

2298 "and is usually preceded by, one of several " 

2299 '"Cannot connect to an SSH agent via <communication_channel_type> ' 

2300 'because this Python version does not support them." ' 

2301 "warning messages issued while searching for an applicable " 

2302 "SSH agent socket provider. " 

2303 "(WarnMsgTemplate.NO_UNIX_DOMAIN_SOCKETS, " 

2304 "WarnMsgTemplate.NO_WINDOWS_NAMED_PIPES.) " 

2305 "If feasible, translations of these messages should therefore " 

2306 "be grammatically similar, so that the user can clearly " 

2307 "recognize them as variants of essentially the same message.", 

2308 )( 

2309 "Error message", 

2310 "Cannot connect to an SSH agent because this Python version " 

2311 "does not support communicating with it.", 

2312 ) 

2313 """""" 

2314 NO_KEY_OR_PHRASE = commented( 

2315 "", 

2316 )( 

2317 "Error message", 

2318 "No passphrase or key was given in the configuration. " 

2319 "In this case, the --phrase or --key argument is required.", 

2320 ) 

2321 """""" 

2322 NO_SSH_AGENT_FOUND = commented( 

2323 "", 

2324 )( 

2325 "Error message", 

2326 "Cannot find any running SSH agent because SSH_AUTH_SOCK is not set.", 

2327 ) 

2328 """""" 

2329 NO_SUITABLE_SSH_KEYS = commented( 

2330 "", 

2331 )( 

2332 "Error message", 

2333 "The SSH agent contains no keys suitable for {PROG_NAME}.", # noqa: RUF027 

2334 flags="python-brace-format", 

2335 ) 

2336 """""" 

2337 PARAMS_MUTUALLY_EXCLUSIVE = commented( 

2338 "The params are long-form command-line option names. " 

2339 'Typical example: "--key is mutually exclusive with --phrase."', 

2340 )( 

2341 "Error message", 

2342 "{param1} is mutually exclusive with {param2}.", 

2343 flags="python-brace-format", 

2344 ) 

2345 """""" 

2346 PARAMS_NEEDS_SERVICE = commented( 

2347 "The param is a long-form command-line option name, " 

2348 "the metavar is Label.VAULT_METAVAR_SERVICE.", 

2349 )( 

2350 "Error message", 

2351 "{param} requires a {service_metavar}.", 

2352 flags="python-brace-format", 

2353 ) 

2354 """""" 

2355 PARAMS_NEEDS_SERVICE_OR_CONFIG = commented( 

2356 "The param is a long-form command-line option name, " 

2357 "the metavar is Label.VAULT_METAVAR_SERVICE.", 

2358 )( 

2359 "Error message", 

2360 "{param} requires a {service_metavar} or --config.", 

2361 flags="python-brace-format", 

2362 ) 

2363 """""" 

2364 PARAMS_NO_SERVICE = commented( 

2365 "The param is a long-form command-line option name, " 

2366 "the metavar is Label.VAULT_METAVAR_SERVICE.", 

2367 )( 

2368 "Error message", 

2369 "{param} does not take a {service_metavar} argument.", 

2370 flags="python-brace-format", 

2371 ) 

2372 """""" 

2373 SERVICE_REQUIRED = commented( 

2374 "The metavar is Label.VAULT_METAVAR_SERVICE.", 

2375 )( 

2376 "Error message", 

2377 "Deriving a passphrase requires a {service_metavar}.", 

2378 flags="python-brace-format", 

2379 ) 

2380 """""" 

2381 SET_AND_UNSET_SAME_SETTING = commented( 

2382 "The rephrasing " 

2383 '"Attempted to unset and set the same setting ' 

2384 '(--unset={setting} --{setting}=...) at the same time." ' 

2385 "may or may not be more suitable as a basis for translation instead.", 

2386 )( 

2387 "Error message", 

2388 "Attempted to unset and set --{setting} at the same time.", 

2389 flags="python-brace-format", 

2390 ) 

2391 """""" 

2392 SSH_KEY_NOT_LOADED = commented( 

2393 "", 

2394 )( 

2395 "Error message", 

2396 "The requested SSH key is not loaded into the agent.", 

2397 ) 

2398 """""" 

2399 UNKNOWN_SSH_AGENT_SOCKET_PROVIDER = commented( 

2400 "The provider is already put in ASCII quotes, and sanitized " 

2401 "so that it is safe to emit to the terminal.", 

2402 )( 

2403 "Error message", 

2404 "The SSH agent socket provider {provider} is not in " 

2405 "derivepassphrase's provider registry.", 

2406 flags="python-brace-format", 

2407 ) 

2408 """""" 

2409 USER_ABORTED_EDIT = commented( 

2410 "The user requested to edit the notes for a service, " 

2411 "but aborted the request mid-editing.", 

2412 )( 

2413 "Error message", 

2414 "Not saving any new notes: the user aborted the request.", 

2415 ) 

2416 """""" 

2417 USER_ABORTED_PASSPHRASE = commented( 

2418 "The user was prompted for a master passphrase, " 

2419 "but aborted the request.", 

2420 )( 

2421 "Error message", 

2422 "No passphrase was given; the user aborted the request.", 

2423 ) 

2424 """""" 

2425 USER_ABORTED_SSH_KEY_SELECTION = commented( 

2426 "The user was prompted to select a master SSH key, " 

2427 "but aborted the request.", 

2428 )( 

2429 "Error message", 

2430 "No SSH key was selected; the user aborted the request.", 

2431 ) 

2432 """""" 

2433 

2434 

2435MsgTemplate: TypeAlias = Union[ 

2436 Label, 

2437 DebugMsgTemplate, 

2438 InfoMsgTemplate, 

2439 WarnMsgTemplate, 

2440 ErrMsgTemplate, 

2441] 

2442"""A type alias for all enums containing translatable strings as values.""" 

2443MSG_TEMPLATE_CLASSES = ( 

2444 Label, 

2445 DebugMsgTemplate, 

2446 InfoMsgTemplate, 

2447 WarnMsgTemplate, 

2448 ErrMsgTemplate, 

2449) 

2450"""A collection all enums containing translatable strings as values.""" 

2451 

2452DebugTranslations._load_cache() # noqa: SLF001 

2453 

2454 

2455def _write_po_file( # noqa: C901,PLR0912 

2456 fileobj: TextIO, 

2457 /, 

2458 *, 

2459 is_template: bool = True, 

2460 version: str = VERSION, 

2461 build_time: datetime.datetime | None = None, 

2462) -> None: # pragma: no cover [interactive] 

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

2464 

2465 Assumes the file object is opened for writing and accepts string 

2466 inputs. The file will *not* be closed when writing is complete. 

2467 The file *must* be opened in UTF-8 encoding, lest the file will 

2468 declare an incorrect encoding. 

2469 

2470 This function crucially depends on all translatable strings 

2471 appearing in the enums of this module. Certain parts of the 

2472 .po header are hard-coded, as is the source filename. 

2473 

2474 """ # noqa: DOC501 

2475 entries: dict[str, dict[str, MsgTemplate]] = {} 

2476 for enum_class in MSG_TEMPLATE_CLASSES: 

2477 for member in enum_class.__members__.values(): 

2478 value = member.value 

2479 ctx = value.l10n_context 

2480 msg = value.singular 

2481 if ( 

2482 msg in entries.setdefault(ctx, {}) 

2483 and entries[ctx][msg] != member 

2484 ): 

2485 raise AssertionError( # noqa: TRY003 

2486 f"Duplicate entry for ({ctx!r}, {msg!r}): " # noqa: EM102 

2487 f"{entries[ctx][msg]!r} and {member!r}" 

2488 ) 

2489 entries[ctx][msg] = member 

2490 if build_time is None and os.environ.get("SOURCE_DATE_EPOCH"): 

2491 try: 

2492 source_date_epoch = int(os.environ["SOURCE_DATE_EPOCH"]) 

2493 except ValueError as exc: 

2494 err_msg = "Cannot parse SOURCE_DATE_EPOCH" 

2495 raise RuntimeError(err_msg) from exc 

2496 else: 

2497 build_time = datetime.datetime.fromtimestamp( 

2498 source_date_epoch, 

2499 tz=datetime.timezone.utc, 

2500 ) 

2501 elif build_time is None: 

2502 build_time = datetime.datetime.now().astimezone() 

2503 if is_template: 

2504 header = ( 

2505 inspect.cleandoc(rf""" 

2506 # English translation for {PROG_NAME}. 

2507 # Copyright (C) {build_time.strftime("%Y")} AUTHOR 

2508 # This file is distributed under the same license as {PROG_NAME}. 

2509 # AUTHOR <someone@example.com>, {build_time.strftime("%Y")}. 

2510 # 

2511 msgid "" 

2512 msgstr "" 

2513 """).removesuffix("\n") 

2514 + "\n" 

2515 ) 

2516 else: 

2517 header = ( 

2518 inspect.cleandoc(rf""" 

2519 # English debug translation for {PROG_NAME}. 

2520 # Copyright (C) {build_time.strftime("%Y")} {AUTHOR} 

2521 # This file is distributed under the same license as {PROG_NAME}. 

2522 # 

2523 msgid "" 

2524 msgstr "" 

2525 """).removesuffix("\n") 

2526 + "\n" 

2527 ) 

2528 fileobj.write(header) 

2529 po_info = { 

2530 "Project-Id-Version": f"{PROG_NAME} {version}", 

2531 "Report-Msgid-Bugs-To": "software@the13thletter.info", 

2532 "PO-Revision-Date": build_time.strftime("%Y-%m-%d %H:%M%z"), 

2533 "MIME-Version": "1.0", 

2534 "Content-Type": "text/plain; charset=UTF-8", 

2535 "Content-Transfer-Encoding": "8bit", 

2536 "Plural-Forms": "nplurals=2; plural=(n != 1);", 

2537 } 

2538 if is_template: 

2539 po_info.update({ 

2540 "POT-Creation-Date": build_time.strftime("%Y-%m-%d %H:%M%z"), 

2541 "Last-Translator": "AUTHOR <someone@example.com>", 

2542 "Language": "en", 

2543 "Language-Team": "English", 

2544 }) 

2545 else: 

2546 po_info.update({ 

2547 "Last-Translator": AUTHOR, 

2548 "Language": "en_US@DEBUG", 

2549 "Language-Team": "English", 

2550 }) 

2551 print(*_format_po_info(po_info), sep="\n", end="\n", file=fileobj) 

2552 

2553 context_class = { 

2554 "Label": Label, 

2555 "Debug message": DebugMsgTemplate, 

2556 "Info message": InfoMsgTemplate, 

2557 "Warning message": WarnMsgTemplate, 

2558 "Error message": ErrMsgTemplate, 

2559 } 

2560 

2561 def _sort_position_msg_template_class( 

2562 item: tuple[str, Any], 

2563 /, 

2564 ) -> tuple[int, str]: 

2565 context_type = item[0].split(" :: ")[0] 

2566 return ( 

2567 MSG_TEMPLATE_CLASSES.index(context_class[context_type]), 

2568 item[0], 

2569 ) 

2570 

2571 for _ctx, subdict in sorted( 

2572 entries.items(), key=_sort_position_msg_template_class 

2573 ): 

2574 for _msg, enum_value in sorted( 

2575 subdict.items(), key=lambda kv: str(kv[1]) 

2576 ): 

2577 value = enum_value.value 

2578 value2 = value.maybe_without_filename() 

2579 fileobj.writelines( 

2580 _format_po_entry( 

2581 enum_value, is_debug_translation=not is_template 

2582 ) 

2583 ) 

2584 if value != value2: 

2585 fileobj.writelines( 

2586 _format_po_entry( 

2587 enum_value, 

2588 is_debug_translation=not is_template, 

2589 transformed_string=value2, 

2590 ) 

2591 ) 

2592 

2593 

2594def _format_po_info( 

2595 data: Mapping[str, Any], 

2596 /, 

2597) -> Generator[str, None, None]: # pragma: no cover [internal] 

2598 """""" # noqa: D419 

2599 sortorder = [ 

2600 "project-id-version", 

2601 "report-msgid-bugs-to", 

2602 "pot-creation-date", 

2603 "po-revision-date", 

2604 "last-translator", 

2605 "language", 

2606 "language-team", 

2607 "mime-version", 

2608 "content-type", 

2609 "content-transfer-encoding", 

2610 "plural-forms", 

2611 ] 

2612 

2613 def _sort_position(s: str, /) -> int: 

2614 n = len(sortorder) 

2615 for i, x in enumerate(sortorder): 

2616 if s.lower().rstrip(":") == x: 

2617 return i 

2618 return n 

2619 

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

2621 value = data[key] 

2622 line = f"{key}: {value}\n" 

2623 yield _cstr(line) 

2624 

2625 

2626def _format_po_entry( 

2627 enum_value: MsgTemplate, 

2628 /, 

2629 *, 

2630 is_debug_translation: bool = False, 

2631 transformed_string: TranslatableString | None = None, 

2632) -> tuple[str, ...]: # pragma: no cover [internal] 

2633 """""" # noqa: D419 

2634 ret: list[str] = ["\n"] 

2635 ts = transformed_string or enum_value.value 

2636 if ts.translator_comments: 

2637 comments = ts.translator_comments.splitlines(False) # noqa: FBT003 

2638 comments.extend(["", f"Message-ID: {enum_value}"]) 

2639 else: 

2640 comments = [f"TRANSLATORS: Message-ID: {enum_value}"] 

2641 ret.extend(f"#. {line}\n" for line in comments) 

2642 if ts.flags: 

2643 ret.append(f"#, {', '.join(sorted(ts.flags))}\n") 

2644 if ts.l10n_context: 

2645 ret.append(f"msgctxt {_cstr(ts.l10n_context)}\n") 

2646 ret.append(f"msgid {_cstr(ts.singular)}\n") 

2647 if ts.plural: 

2648 ret.append(f"msgid_plural {_cstr(ts.plural)}\n") 

2649 value = ( 

2650 DebugTranslations().pgettext(ts.l10n_context, ts.singular) 

2651 if is_debug_translation 

2652 else "" 

2653 ) 

2654 ret.append(f"msgstr {_cstr(value)}\n") 

2655 return tuple(ret) 

2656 

2657 

2658def _cstr(s: str) -> str: # pragma: no cover [internal] 

2659 """""" # noqa: D419 

2660 

2661 def escape(string: str) -> str: 

2662 return string.translate({ 

2663 0: r"\000", 

2664 1: r"\001", 

2665 2: r"\002", 

2666 3: r"\003", 

2667 4: r"\004", 

2668 5: r"\005", 

2669 6: r"\006", 

2670 7: r"\007", 

2671 8: r"\b", 

2672 9: r"\t", 

2673 10: r"\n", 

2674 11: r"\013", 

2675 12: r"\f", 

2676 13: r"\r", 

2677 14: r"\016", 

2678 15: r"\017", 

2679 ord('"'): r"\"", 

2680 ord("\\"): r"\\", 

2681 127: r"\177", 

2682 }) 

2683 

2684 return "\n".join( 

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

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

2687 ) 

2688 

2689 

2690if __name__ == "__main__": 

2691 import argparse 

2692 

2693 def validate_build_time(value: str | None) -> datetime.datetime | None: 

2694 if value is None: 

2695 return None 

2696 ret = datetime.datetime.fromisoformat(value) 

2697 if ret.isoformat(sep=" ", timespec="seconds") != value: 

2698 raise ValueError(f"invalid time specification: {value}") # noqa: EM102,TRY003 

2699 return ret 

2700 

2701 ap = argparse.ArgumentParser() 

2702 ex = ap.add_mutually_exclusive_group() 

2703 ex.add_argument( 

2704 "--template", 

2705 action="store_true", 

2706 dest="is_template", 

2707 default=True, 

2708 help="Generate a template file (default)", 

2709 ) 

2710 ex.add_argument( 

2711 "--debug-translation", 

2712 action="store_false", 

2713 dest="is_template", 

2714 default=True, 

2715 help='Generate a "debug" translation file', 

2716 ) 

2717 ap.add_argument( 

2718 "--set-version", 

2719 action="store", 

2720 dest="version", 

2721 default=VERSION, 

2722 help="Override declared software version", 

2723 ) 

2724 ap.add_argument( 

2725 "--set-build-time", 

2726 action="store", 

2727 dest="build_time", 

2728 default=None, 

2729 type=validate_build_time, 

2730 help="Override the time of build (YYYY-MM-DD HH:MM:SS+HH:MM format, " 

2731 "default: $SOURCE_DATE_EPOCH, or the current time)", 

2732 ) 

2733 args = ap.parse_args() 

2734 _write_po_file( 

2735 sys.stdout, 

2736 version=args.version, 

2737 is_template=args.is_template, 

2738 build_time=args.build_time, 

2739 )