Automatically check and build the translations on master via hatch-gettext
Marco Ricci

Marco Ricci commited on 2025-02-11 16:30:37
Zeige 7 geänderte Dateien mit 1254 Einfügungen und 13 Löschungen.


Include the configuration for `hatch-gettext` to automatically build the
translations during build time, and include them in the wheel.  Further
support a quality control check that tests the PO template for
reproducability.  The PO template generator now supports passing
a specific build timestamp, and realistically, the reproducability check
needs this timestamp to be specified (via `SOURCE_DATE_EPOCH`).

Actually *using* these translations directly from the installed wheel is
not yet supported, because it is actually somewhat difficult to use the
`gettext` MO-finding machinery here directly: `gettext.find` requires
the directories it searches to be on-disk directories, whereas the wheel
installer does not guarantee that installed packages are present as
files on the system (instead of, say, as a ZIP archive).  The sanest way
forward seems to be reimplementing `gettext.find` with support for
`importlib.metadata.as_file`, and then constructing
`gettext.GNUTranslations` objects directly.  This would also be the
correct time to introduce a new enum for all these automatically
calculated platform-specific special locale directories.  (TODO.)
... ...
@@ -0,0 +1,1167 @@
1
+# English translation for derivepassphrase.
2
+# Copyright (C) 2025 AUTHOR
3
+# This file is distributed under the same license as derivepassphrase.
4
+# AUTHOR <someone@example.com>, 2025.
5
+#
6
+msgid ""
7
+msgstr ""
8
+"Project-Id-Version: derivepassphrase 0.5a1.dev1\n"
9
+"Report-Msgid-Bugs-To: software@the13thletter.info\n"
10
+"POT-Creation-Date: 2025-02-11 15:30+0000\n"
11
+"PO-Revision-Date: 2025-02-11 15:30+0000\n"
12
+"Last-Translator: AUTHOR <someone@example.com>\n"
13
+"Language: en\n"
14
+"Language-Team: English\n"
15
+"MIME-Version: 1.0\n"
16
+"Content-Type: text/plain; charset=UTF-8\n"
17
+"Content-Transfer-Encoding: 8bit\n"
18
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
19
+
20
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "storeroom"-type configuration directories.  The system stores entries in different "buckets" of a hash table.  Here, we report on a single item (path and value) we discovered after decrypting the whole bucket.  (We ensure the path and value are printable as-is.)
21
+#. 
22
+#. Message-ID: DebugMsgTemplate.BUCKET_ITEM_FOUND
23
+#, python-brace-format
24
+msgctxt "Debug message"
25
+msgid "Found bucket item: {path} -> {value}"
26
+msgstr ""
27
+
28
+#. TRANSLATORS: "AES256-CBC" and "PKCS#7" are, in essence, names of formats, and should not be translated.  "IV" means "initialization vector", and is specifically a cryptographic term, as are "plaintext" and "ciphertext".
29
+#. 
30
+#. Message-ID: DebugMsgTemplate.DECRYPT_BUCKET_ITEM_INFO
31
+#, python-brace-format
32
+msgctxt "Debug message"
33
+msgid "Decrypt bucket item contents:\n"
34
+"\n"
35
+"Encryption key (master key): {enc_key}\n"
36
+"Encryption cipher: AES256-CBC with PKCS#7 padding\n"
37
+"Encryption IV: {iv}\n"
38
+"Encrypted ciphertext: {ciphertext}\n"
39
+"Plaintext: {plaintext}"
40
+msgstr ""
41
+
42
+#. TRANSLATORS: Message-ID: DebugMsgTemplate.DECRYPT_BUCKET_ITEM_KEY_INFO
43
+#, python-brace-format
44
+msgctxt "Debug message"
45
+msgid "Decrypt bucket item:\n"
46
+"\n"
47
+"Plaintext: {plaintext}\n"
48
+"Encryption key (master key): {enc_key}\n"
49
+"Signing key (master key): {sign_key}"
50
+msgstr ""
51
+
52
+#. TRANSLATORS: The MAC stands for "message authentication code", which guarantees the authenticity of the message to anyone who holds the corresponding key, similar to a digital signature.  The acronym "MAC" is assumed to be well-known to the English target audience, or at least discoverable by them; they *are* asking for debug output, after all.  Please use your judgement as to whether to translate this term or not, expanded or not.
53
+#. 
54
+#. Message-ID: DebugMsgTemplate.DECRYPT_BUCKET_ITEM_MAC_INFO
55
+#, python-brace-format
56
+msgctxt "Debug message"
57
+msgid "Decrypt bucket item contents:\n"
58
+"\n"
59
+"MAC key: {sign_key}\n"
60
+"Authenticated content: {ciphertext}\n"
61
+"Claimed MAC value: {claimed_mac}\n"
62
+"Computed MAC value: {actual_mac}"
63
+msgstr ""
64
+
65
+#. TRANSLATORS: "AES256-CBC" and "PKCS#7" are, in essence, names of formats, and should not be translated.  "IV" means "initialization vector", and is specifically a cryptographic term, as are "plaintext" and "ciphertext".
66
+#. 
67
+#. Message-ID: DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_INFO
68
+#, python-brace-format
69
+msgctxt "Debug message"
70
+msgid "Decrypt bucket item session keys:\n"
71
+"\n"
72
+"Encryption key (master key): {enc_key}\n"
73
+"Encryption cipher: AES256-CBC with PKCS#7 padding\n"
74
+"Encryption IV: {iv}\n"
75
+"Encrypted ciphertext: {ciphertext}\n"
76
+"Plaintext: {plaintext}\n"
77
+"Parsed plaintext: {code}"
78
+msgstr ""
79
+
80
+#. TRANSLATORS: The MAC stands for "message authentication code", which guarantees the authenticity of the message to anyone who holds the corresponding key, similar to a digital signature.  The acronym "MAC" is assumed to be well-known to the English target audience, or at least discoverable by them; they *are* asking for debug output, after all.  Please use your judgement as to whether to translate this term or not, expanded or not.
81
+#. 
82
+#. Message-ID: DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_MAC_INFO
83
+#, python-brace-format
84
+msgctxt "Debug message"
85
+msgid "Decrypt bucket item session keys:\n"
86
+"\n"
87
+"MAC key (master key): {sign_key}\n"
88
+"Authenticated content: {ciphertext}\n"
89
+"Claimed MAC value: {claimed_mac}\n"
90
+"Computed MAC value: {actual_mac}"
91
+msgstr ""
92
+
93
+#. TRANSLATORS: Message-ID: DebugMsgTemplate.DERIVED_MASTER_KEYS_KEYS
94
+#, python-brace-format
95
+msgctxt "Debug message"
96
+msgid "Derived master keys' keys:\n"
97
+"\n"
98
+"Encryption key: {enc_key}\n"
99
+"Signing key: {sign_key}\n"
100
+"Password: {pw_bytes}\n"
101
+"Function call: pbkdf2(algorithm={algorithm!r}, length={length!r}, salt={salt!r}, iterations={iterations!r})"
102
+msgstr ""
103
+
104
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "storeroom"-type configuration directories, while "assembling" the items stored in the configuration according to the item's "path".  Each "directory" in the path contains a list of children it claims to contain, and this list must be matched against the actual discovered items.  Now, at the end, we actually confirm the claim.  (We would have already thrown an error here otherwise.)
105
+#. 
106
+#. Message-ID: DebugMsgTemplate.DIRECTORY_CONTENTS_CHECK_OK
107
+#, python-brace-format
108
+msgctxt "Debug message"
109
+msgid "Directory contents check OK: {path} -> {contents}"
110
+msgstr ""
111
+
112
+#. TRANSLATORS: The MAC stands for "message authentication code", which guarantees the authenticity of the message to anyone who holds the corresponding key, similar to a digital signature.  The acronym "MAC" is assumed to be well-known to the English target audience, or at least discoverable by them; they *are* asking for debug output, after all.  Please use your judgement as to whether to translate this term or not, expanded or not.
113
+#. 
114
+#. Message-ID: DebugMsgTemplate.MASTER_KEYS_DATA_MAC_INFO
115
+#, python-brace-format
116
+msgctxt "Debug message"
117
+msgid "Master keys data:\n"
118
+"\n"
119
+"MAC key: {sign_key}\n"
120
+"Authenticated content: {ciphertext}\n"
121
+"Claimed MAC value: {claimed_mac}\n"
122
+"Computed MAC value: {actual_mac}"
123
+msgstr ""
124
+
125
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "storeroom"-type configuration directories, while "assembling" the items stored in the configuration according to the item's "path".  Each "directory" in the path contains a list of children it claims to contain, and this list must be matched against the actual discovered items.  When emitting this message, we merely indicate that we saved the "claimed" list for this directory for later.
126
+#. 
127
+#. Message-ID: DebugMsgTemplate.POSTPONING_DIRECTORY_CONTENTS_CHECK
128
+#, python-brace-format
129
+msgctxt "Debug message"
130
+msgid "Postponing directory contents check: {path} -> {contents}"
131
+msgstr ""
132
+
133
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "storeroom"-type configuration directories, while "assembling" the items stored in the configuration according to the item's "path".  We confirm that we set the entry at the given path to the given value.
134
+#. 
135
+#. Message-ID: DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS
136
+#, python-brace-format
137
+msgctxt "Debug message"
138
+msgid "Setting contents: {path} -> {value}"
139
+msgstr ""
140
+
141
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "storeroom"-type configuration directories, while "assembling" the items stored in the configuration according to the item's "path".  We confirm that we set up a currently empty directory at the given path.
142
+#. 
143
+#. Message-ID: DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS_EMPTY_DIRECTORY
144
+#, python-brace-format
145
+msgctxt "Debug message"
146
+msgid "Setting contents (empty directory): {path}"
147
+msgstr ""
148
+
149
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "native"-type configuration directories.  It is preceded by the info message VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC; see the commentary there concerning the terms and thoughts on translating them.
150
+#. 
151
+#. Message-ID: DebugMsgTemplate.VAULT_NATIVE_CHECKING_MAC_DETAILS
152
+#, python-brace-format
153
+msgctxt "Debug message"
154
+msgid "MAC details:\n"
155
+"\n"
156
+"MAC input: {mac_input}\n"
157
+"Expected MAC: {mac}"
158
+msgstr ""
159
+
160
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "native"-type configuration directories: in v0.2, the non-standard and deprecated "EVP_bytestokey" function from OpenSSL must be reimplemented from scratch.  The terms "salt" and "IV" (initialization vector) are cryptographic terms.
161
+#. 
162
+#. Message-ID: DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_INIT
163
+#, python-brace-format
164
+msgctxt "Debug message"
165
+msgid "evp_bytestokey_md5 (initialization):\n"
166
+"\n"
167
+"Input: {data}\n"
168
+"Salt: {salt}\n"
169
+"Key size: {key_size}\n"
170
+"IV size: {iv_size}\n"
171
+"Buffer length: {buffer_length}\n"
172
+"Buffer: {buffer}"
173
+msgstr ""
174
+
175
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "native"-type configuration directories: in v0.2, the non-standard and deprecated "EVP_bytestokey" function from OpenSSL must be reimplemented from scratch.  The terms "salt" and "IV" (initialization vector) are cryptographic terms.This function reports on the final results.
176
+#. 
177
+#. Message-ID: DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_RESULT
178
+#, python-brace-format
179
+msgctxt "Debug message"
180
+msgid "evp_bytestokey_md5 (result):\n"
181
+"\n"
182
+"Encryption key: {enc_key}\n"
183
+"IV: {iv}"
184
+msgstr ""
185
+
186
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "native"-type configuration directories: in v0.2, the non-standard and deprecated "EVP_bytestokey" function from OpenSSL must be reimplemented from scratch.  The terms "salt" and "IV" (initialization vector) are cryptographic terms.This function reports on the updated buffer length and contents after executing one round of hashing.
187
+#. 
188
+#. Message-ID: DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_ROUND
189
+#, python-brace-format
190
+msgctxt "Debug message"
191
+msgid "evp_bytestokey_md5 (round update):\n"
192
+"\n"
193
+"Buffer length: {buffer_length}\n"
194
+"Buffer: {buffer}"
195
+msgstr ""
196
+
197
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "native"-type configuration directories.  "padding" and "plaintext" are cryptographic terms.
198
+#. 
199
+#. Message-ID: DebugMsgTemplate.VAULT_NATIVE_PADDED_PLAINTEXT
200
+#, python-brace-format
201
+msgctxt "Debug message"
202
+msgid "Padded plaintext: {contents}"
203
+msgstr ""
204
+
205
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "native"-type configuration directories.  It is preceded by the info message VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC; see the commentary there concerning the terms and thoughts on translating them.
206
+#. 
207
+#. Message-ID: DebugMsgTemplate.VAULT_NATIVE_PARSE_BUFFER
208
+#, python-brace-format
209
+msgctxt "Debug message"
210
+msgid "Buffer: {contents}\n"
211
+"\n"
212
+"IV: {iv}\n"
213
+"Payload: {payload}\n"
214
+"MAC: {mac}"
215
+msgstr ""
216
+
217
+#. TRANSLATORS: Message-ID: DebugMsgTemplate.VAULT_NATIVE_PBKDF2_CALL
218
+#, python-brace-format
219
+msgctxt "Debug message"
220
+msgid "Master key derivation:\n"
221
+"\n"
222
+"PBKDF2 call: PBKDF2-HMAC(password={password!r}, salt={salt!r}, iterations={iterations!r}, key_size={key_size!r}, algorithm={algorithm!r})\n"
223
+"Result (binary): {raw_result}\n"
224
+"Result (hex key): {result_key!r}"
225
+msgstr ""
226
+
227
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "native"-type configuration directories.  "plaintext" is a cryptographic term.
228
+#. 
229
+#. Message-ID: DebugMsgTemplate.VAULT_NATIVE_PLAINTEXT
230
+#, python-brace-format
231
+msgctxt "Debug message"
232
+msgid "Plaintext: {contents}"
233
+msgstr ""
234
+
235
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "native"-type configuration directories.  It is preceded by the info message VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC and the debug message PARSING_NATIVE_PARSE_BUFFER; see the commentary there concerning the terms and thoughts on translating them.
236
+#. 
237
+#. Message-ID: DebugMsgTemplate.VAULT_NATIVE_V02_PAYLOAD_MAC_POSTPROCESSING
238
+#, python-brace-format
239
+msgctxt "Debug message"
240
+msgid "Postprocessing buffer (v0.2):\n"
241
+"\n"
242
+"Payload: {payload} (decoded from base64)\n"
243
+"MAC: {mac} (decoded from hex)"
244
+msgstr ""
245
+
246
+#. TRANSLATORS: "loaded keys" being keys loaded into the agent.
247
+#. 
248
+#. Message-ID: ErrMsgTemplate.AGENT_REFUSED_LIST_KEYS
249
+msgctxt "Error message"
250
+msgid "The SSH agent failed to or refused to supply a list of loaded keys."
251
+msgstr ""
252
+
253
+#. TRANSLATORS: The message to be signed is the vault UUID, but there's no space to explain that here, so ideally the error message does not go into detail.
254
+#. 
255
+#. Message-ID: ErrMsgTemplate.AGENT_REFUSED_SIGNATURE
256
+msgctxt "Error message"
257
+msgid "The SSH agent failed to or refused to issue a signature with the selected key, necessary for deriving a service passphrase."
258
+msgstr ""
259
+
260
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
261
+#. 
262
+#. Message-ID: ErrMsgTemplate.CANNOT_CONNECT_TO_AGENT
263
+#, python-brace-format
264
+msgctxt "Error message"
265
+msgid "Cannot connect to the SSH agent: {error!s}: {filename!r}."
266
+msgstr ""
267
+
268
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
269
+#. 
270
+#. Message-ID: ErrMsgTemplate.CANNOT_CONNECT_TO_AGENT
271
+#, python-brace-format
272
+msgctxt "Error message"
273
+msgid "Cannot connect to the SSH agent: {error!s}."
274
+msgstr ""
275
+
276
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
277
+#. 
278
+#. Message-ID: ErrMsgTemplate.CANNOT_DECODEIMPORT_VAULT_SETTINGS
279
+#, python-brace-format
280
+msgctxt "Error message"
281
+msgid "Cannot import vault settings: cannot decode JSON: {error!s}."
282
+msgstr ""
283
+
284
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
285
+#. 
286
+#. Message-ID: ErrMsgTemplate.CANNOT_EXPORT_VAULT_SETTINGS
287
+#, python-brace-format
288
+msgctxt "Error message"
289
+msgid "Cannot export vault settings: {error!s}: {filename!r}."
290
+msgstr ""
291
+
292
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
293
+#. 
294
+#. Message-ID: ErrMsgTemplate.CANNOT_EXPORT_VAULT_SETTINGS
295
+#, python-brace-format
296
+msgctxt "Error message"
297
+msgid "Cannot export vault settings: {error!s}."
298
+msgstr ""
299
+
300
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
301
+#. 
302
+#. Message-ID: ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS
303
+#, python-brace-format
304
+msgctxt "Error message"
305
+msgid "Cannot import vault settings: {error!s}: {filename!r}."
306
+msgstr ""
307
+
308
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
309
+#. 
310
+#. Message-ID: ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS
311
+#, python-brace-format
312
+msgctxt "Error message"
313
+msgid "Cannot import vault settings: {error!s}."
314
+msgstr ""
315
+
316
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
317
+#. 
318
+#. Message-ID: ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG
319
+#, python-brace-format
320
+msgctxt "Error message"
321
+msgid "Cannot load user config: {error!s}: {filename!r}."
322
+msgstr ""
323
+
324
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
325
+#. 
326
+#. Message-ID: ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG
327
+#, python-brace-format
328
+msgctxt "Error message"
329
+msgid "Cannot load user config: {error!s}."
330
+msgstr ""
331
+
332
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
333
+#. 
334
+#. Message-ID: ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS
335
+#, python-brace-format
336
+msgctxt "Error message"
337
+msgid "Cannot load vault settings: {error!s}: {filename!r}."
338
+msgstr ""
339
+
340
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
341
+#. 
342
+#. Message-ID: ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS
343
+#, python-brace-format
344
+msgctxt "Error message"
345
+msgid "Cannot load vault settings: {error!s}."
346
+msgstr ""
347
+
348
+#. TRANSLATORS: Unlike the "Cannot load {path!r} as a {fmt!s} vault configuration." message, *this* error message is emitted when we have tried loading the path in each of our supported formats, and failed.  The user will thus see the above "Cannot load ..." warning message potentially multiple times, and this error message at the very bottom.
349
+#. 
350
+#. Message-ID: ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG
351
+#, python-brace-format
352
+msgctxt "Error message"
353
+msgid "Cannot parse {path!r} as a valid vault-native configuration file/directory."
354
+msgstr ""
355
+
356
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
357
+#. 
358
+#. Message-ID: ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG_OSERROR
359
+#, python-brace-format
360
+msgctxt "Error message"
361
+msgid "Cannot parse {path!r} as a valid vault-native configuration file/directory: {error!s}: {filename!r}."
362
+msgstr ""
363
+
364
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
365
+#. 
366
+#. Message-ID: ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG_OSERROR
367
+#, python-brace-format
368
+msgctxt "Error message"
369
+msgid "Cannot parse {path!r} as a valid vault-native configuration file/directory: {error!s}."
370
+msgstr ""
371
+
372
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
373
+#. 
374
+#. Message-ID: ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS
375
+#, python-brace-format
376
+msgctxt "Error message"
377
+msgid "Cannot store vault settings: {error!s}: {filename!r}."
378
+msgstr ""
379
+
380
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
381
+#. 
382
+#. Message-ID: ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS
383
+#, python-brace-format
384
+msgctxt "Error message"
385
+msgid "Cannot store vault settings: {error!s}."
386
+msgstr ""
387
+
388
+#. TRANSLATORS: This error message is used whenever we cannot make any sense of a response from the SSH agent because the response is ill-formed (truncated, improperly encoded, etc.) or otherwise violates the communications protocol.  Well-formed responses that adhere to the protocol, even if they indicate that the requested operation failed, are handled with a different error message.
389
+#. 
390
+#. Message-ID: ErrMsgTemplate.CANNOT_UNDERSTAND_AGENT
391
+msgctxt "Error message"
392
+msgid "Cannot understand the SSH agent's response because it violates the communication protocol."
393
+msgstr ""
394
+
395
+#. TRANSLATORS: The settings_type metavar contains translations for either "global settings" or "service-specific settings"; see the CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL and CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE entries.  The first sentence will thus read either "Cannot update the global settings without any given settings." or "Cannot update the service-specific settings without any given settings.".  You may update this entry, and the two metavar entries, in any way you see fit that achieves the desired translations of the first sentence.
396
+#. 
397
+#. Message-ID: ErrMsgTemplate.CANNOT_UPDATE_SETTINGS_NO_SETTINGS
398
+#, python-brace-format
399
+msgctxt "Error message"
400
+msgid "Cannot update the {settings_type!s} without any given settings.  You must specify at least one of --lower, ..., --symbol, --notes, or --phrase or --key."
401
+msgstr ""
402
+
403
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
404
+#. 
405
+#. Message-ID: ErrMsgTemplate.INVALID_USER_CONFIG
406
+#, python-brace-format
407
+msgctxt "Error message"
408
+msgid "The user configuration file is invalid.  {error!s}: {filename!r}."
409
+msgstr ""
410
+
411
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
412
+#. 
413
+#. Message-ID: ErrMsgTemplate.INVALID_USER_CONFIG
414
+#, python-brace-format
415
+msgctxt "Error message"
416
+msgid "The user configuration file is invalid.  {error!s}."
417
+msgstr ""
418
+
419
+#. TRANSLATORS: This error message is a reaction to a validator function saying *that* the configuration is not valid, but not *how* it is not valid.  The configuration file is principally parsable, however.
420
+#. 
421
+#. Message-ID: ErrMsgTemplate.INVALID_VAULT_CONFIG
422
+#, python-brace-format
423
+msgctxt "Error message"
424
+msgid "Invalid vault config: {config!r}."
425
+msgstr ""
426
+
427
+#. TRANSLATORS: Message-ID: ErrMsgTemplate.MISSING_MODULE
428
+#, python-brace-format
429
+msgctxt "Error message"
430
+msgid "Cannot load the required Python module {module!r}."
431
+msgstr ""
432
+
433
+#. TRANSLATORS: Message-ID: ErrMsgTemplate.NO_AF_UNIX
434
+msgctxt "Error message"
435
+msgid "Cannot connect to an SSH agent because this Python version does not support UNIX domain sockets."
436
+msgstr ""
437
+
438
+#. TRANSLATORS: Message-ID: ErrMsgTemplate.NO_KEY_OR_PHRASE
439
+msgctxt "Error message"
440
+msgid "No passphrase or key was given in the configuration.  In this case, the --phrase or --key argument is required."
441
+msgstr ""
442
+
443
+#. TRANSLATORS: Message-ID: ErrMsgTemplate.NO_SSH_AGENT_FOUND
444
+msgctxt "Error message"
445
+msgid "Cannot find any running SSH agent because SSH_AUTH_SOCK is not set."
446
+msgstr ""
447
+
448
+#. TRANSLATORS: Message-ID: ErrMsgTemplate.NO_SUITABLE_SSH_KEYS
449
+#, python-brace-format
450
+msgctxt "Error message"
451
+msgid "The SSH agent contains no keys suitable for {PROG_NAME!s}."
452
+msgstr ""
453
+
454
+#. TRANSLATORS: The params are long-form command-line option names.  Typical example: "--key is mutually exclusive with --phrase."
455
+#. 
456
+#. Message-ID: ErrMsgTemplate.PARAMS_MUTUALLY_EXCLUSIVE
457
+#, python-brace-format
458
+msgctxt "Error message"
459
+msgid "{param1!s} is mutually exclusive with {param2!s}."
460
+msgstr ""
461
+
462
+#. TRANSLATORS: The param is a long-form command-line option name, the metavar is Label.VAULT_METAVAR_SERVICE.
463
+#. 
464
+#. Message-ID: ErrMsgTemplate.PARAMS_NEEDS_SERVICE
465
+#, python-brace-format
466
+msgctxt "Error message"
467
+msgid "{param!s} requires a {service_metavar!s}."
468
+msgstr ""
469
+
470
+#. TRANSLATORS: The param is a long-form command-line option name, the metavar is Label.VAULT_METAVAR_SERVICE.
471
+#. 
472
+#. Message-ID: ErrMsgTemplate.PARAMS_NEEDS_SERVICE_OR_CONFIG
473
+#, python-brace-format
474
+msgctxt "Error message"
475
+msgid "{param!s} requires a {service_metavar!s} or --config."
476
+msgstr ""
477
+
478
+#. TRANSLATORS: The param is a long-form command-line option name, the metavar is Label.VAULT_METAVAR_SERVICE.
479
+#. 
480
+#. Message-ID: ErrMsgTemplate.PARAMS_NO_SERVICE
481
+#, python-brace-format
482
+msgctxt "Error message"
483
+msgid "{param!s} does not take a {service_metavar!s} argument."
484
+msgstr ""
485
+
486
+#. TRANSLATORS: The metavar is Label.VAULT_METAVAR_SERVICE.
487
+#. 
488
+#. Message-ID: ErrMsgTemplate.SERVICE_REQUIRED
489
+#, python-brace-format
490
+msgctxt "Error message"
491
+msgid "Deriving a passphrase requires a {service_metavar!s}."
492
+msgstr ""
493
+
494
+#. TRANSLATORS: The rephrasing "Attempted to unset and set the same setting (--unset={setting!s} --{setting!s}=...) at the same time."may or may not be more suitable as a basis for translation instead.
495
+#. 
496
+#. Message-ID: ErrMsgTemplate.SET_AND_UNSET_SAME_SETTING
497
+#, python-brace-format
498
+msgctxt "Error message"
499
+msgid "Attempted to unset and set --{setting!s} at the same time."
500
+msgstr ""
501
+
502
+#. TRANSLATORS: Message-ID: ErrMsgTemplate.SSH_KEY_NOT_LOADED
503
+msgctxt "Error message"
504
+msgid "The requested SSH key is not loaded into the agent."
505
+msgstr ""
506
+
507
+#. TRANSLATORS: The user requested to edit the notes for a service, but aborted the request mid-editing.
508
+#. 
509
+#. Message-ID: ErrMsgTemplate.USER_ABORTED_EDIT
510
+msgctxt "Error message"
511
+msgid "Not saving any new notes: the user aborted the request."
512
+msgstr ""
513
+
514
+#. TRANSLATORS: The user was prompted for a master passphrase, but aborted the request.
515
+#. 
516
+#. Message-ID: ErrMsgTemplate.USER_ABORTED_PASSPHRASE
517
+msgctxt "Error message"
518
+msgid "No passphrase was given; the user aborted the request."
519
+msgstr ""
520
+
521
+#. TRANSLATORS: The user was prompted to select a master SSH key, but aborted the request.
522
+#. 
523
+#. Message-ID: ErrMsgTemplate.USER_ABORTED_SSH_KEY_SELECTION
524
+msgctxt "Error message"
525
+msgid "No SSH key was selected; the user aborted the request."
526
+msgstr ""
527
+
528
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "storeroom"-type configuration directories.  The system stores entries in different "buckets" of a hash table.  After the respective items in the buckets have been decrypted, we then have a list of item paths plus contents to populate.  This must be done in a certain order (we don't yet have an existing directory tree to rely on, but rather must build it on-the-fly), hence the term "assembling".
529
+#. 
530
+#. Message-ID: InfoMsgTemplate.ASSEMBLING_CONFIG_STRUCTURE
531
+msgctxt "Info message"
532
+msgid "Assembling config structure"
533
+msgstr ""
534
+
535
+#. TRANSLATORS: "fmt" is a string such as "v0.2" or "storeroom", indicating the format which we tried to load the vault configuration as.
536
+#. 
537
+#. Message-ID: InfoMsgTemplate.CANNOT_LOAD_AS_VAULT_CONFIG
538
+#, python-brace-format
539
+msgctxt "Info message"
540
+msgid "Cannot load {path!r} as a {fmt!s} vault configuration."
541
+msgstr ""
542
+
543
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "storeroom"-type configuration directories.  Having "assembled" the configuration items according to their claimed paths and contents, we then check if the assembled structure is internally consistent.
544
+#. 
545
+#. Message-ID: InfoMsgTemplate.CHECKING_CONFIG_STRUCTURE_CONSISTENCY
546
+msgctxt "Info message"
547
+msgid "Checking config structure consistency"
548
+msgstr ""
549
+
550
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "storeroom"-type configuration directories.  The system stores entries in different "buckets" of a hash table.  We parse the directory bucket by bucket.  All buckets are numbered in hexadecimal, and typically there are 32 buckets, so 2-digit hex numbers.
551
+#. 
552
+#. Message-ID: InfoMsgTemplate.DECRYPTING_BUCKET
553
+#, python-brace-format
554
+msgctxt "Info message"
555
+msgid "Decrypting bucket {bucket_number}"
556
+msgstr ""
557
+
558
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "storeroom"-type configuration directories.  `.keys` is a filename, from which data about the master keys for this configuration are loaded.
559
+#. 
560
+#. Message-ID: InfoMsgTemplate.PARSING_MASTER_KEYS_DATA
561
+msgctxt "Info message"
562
+msgid "Parsing master keys data from .keys"
563
+msgstr ""
564
+
565
+#. TRANSLATORS: This message immediately follows an error message about a missing library that needs to be installed.  The Python Package Index (PyPI) supports declaring sets of optional dependencies as "extras", so users installing from PyPI can request reinstallation with a named "extra" being enabled.  This would then let the installer take care of the missing libraries automatically, hence this suggestion to PyPI users.
566
+#. 
567
+#. Message-ID: InfoMsgTemplate.PIP_INSTALL_EXTRA
568
+#, python-brace-format
569
+msgctxt "Info message"
570
+msgid "For users installing from PyPI, see the {extra_name!r} extra."
571
+msgstr ""
572
+
573
+#. TRANSLATORS: This info message immediately follows the "Using deprecated v0.1-style ..." deprecation warning.
574
+#. 
575
+#. Message-ID: InfoMsgTemplate.SUCCESSFULLY_MIGRATED
576
+#, python-brace-format
577
+msgctxt "Info message"
578
+msgid "Successfully migrated to {path!r}."
579
+msgstr ""
580
+
581
+#. TRANSLATORS: Message-ID: InfoMsgTemplate.VAULT_NATIVE_CHECKING_MAC
582
+msgctxt "Info message"
583
+msgid "Checking MAC"
584
+msgstr ""
585
+
586
+#. TRANSLATORS: Message-ID: InfoMsgTemplate.VAULT_NATIVE_DECRYPTING_CONTENTS
587
+msgctxt "Info message"
588
+msgid "Decrypting contents"
589
+msgstr ""
590
+
591
+#. TRANSLATORS: Message-ID: InfoMsgTemplate.VAULT_NATIVE_DERIVING_KEYS
592
+msgctxt "Info message"
593
+msgid "Deriving an encryption and signing key"
594
+msgstr ""
595
+
596
+#. TRANSLATORS: This message is emitted by the vault configuration exporter for "native"-type configuration directories.  "IV" means "initialization vector", and "MAC" means "message authentication code".  They are specifically cryptographic terms, as is "payload".  The acronyms "IV" and "MAC" are assumed to be well-known to the English target audience, or at least discoverable by them; they *are* asking for debug output, after all.  Please use your judgement as to whether to translate these terms or not, expanded or not.
597
+#. 
598
+#. Message-ID: InfoMsgTemplate.VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC
599
+msgctxt "Info message"
600
+msgid "Parsing IV, payload and MAC from the file contents"
601
+msgstr ""
602
+
603
+#. TRANSLATORS: This is a short label that will be prepended to a warning message, e.g., "Deprecation warning: A subcommand will be required in v1.0."
604
+#. 
605
+#. Message-ID: Label.DEPRECATION_WARNING_LABEL
606
+msgctxt "Label :: Diagnostics :: Marker"
607
+msgid "Deprecation warning"
608
+msgstr ""
609
+
610
+#. TRANSLATORS: This is a short label that will be prepended to a warning message, e.g., "Warning: An empty service name is not supported by vault(1)."
611
+#. 
612
+#. Message-ID: Label.WARNING_LABEL
613
+msgctxt "Label :: Diagnostics :: Marker"
614
+msgid "Warning"
615
+msgstr ""
616
+
617
+#. TRANSLATORS: This is one of two values of the settings_type metavar used in the CANNOT_UPDATE_SETTINGS_NO_SETTINGS entry.  It is only used there.  The full sentence then reads: "Cannot update the global settings without any given settings."
618
+#. 
619
+#. Message-ID: Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL
620
+msgctxt "Label :: Error message :: Metavar"
621
+msgid "global settings"
622
+msgstr ""
623
+
624
+#. TRANSLATORS: This is one of two values of the settings_type metavar used in the CANNOT_UPDATE_SETTINGS_NO_SETTINGS entry.  It is only used there.  The full sentence then reads: "Cannot update the service-specific settings without any given settings."
625
+#. 
626
+#. Message-ID: Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE
627
+msgctxt "Label :: Error message :: Metavar"
628
+msgid "service-specific settings"
629
+msgstr ""
630
+
631
+#. TRANSLATORS: Message-ID: Label.CONFIGURATION_EPILOG
632
+msgctxt "Label :: Help text :: Explanation"
633
+msgid "Use $VISUAL or $EDITOR to configure the spawned editor."
634
+msgstr ""
635
+
636
+#. TRANSLATORS: This is the first paragraph of the command help text, but it also appears (in truncated form, if necessary) as one-line help text for this command.  The translation should thus be as meaningful as possible even if truncated.
637
+#. 
638
+#. Message-ID: Label.DERIVEPASSPHRASE_01
639
+msgctxt "Label :: Help text :: Explanation"
640
+msgid "Derive a strong passphrase, deterministically, from a master secret."
641
+msgstr ""
642
+
643
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_02
644
+msgctxt "Label :: Help text :: Explanation"
645
+msgid "The currently implemented subcommands are \"vault\" (for the scheme used by vault) and \"export\" (for exporting foreign configuration data).  See the respective `--help` output for instructions.  If no subcommand is given, we default to \"vault\"."
646
+msgstr ""
647
+
648
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_03
649
+msgctxt "Label :: Help text :: Explanation"
650
+msgid "Deprecation notice: Defaulting to \"vault\" is deprecated.  Starting in v1.0, the subcommand must be specified explicitly."
651
+msgstr ""
652
+
653
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_EPILOG_01
654
+msgctxt "Label :: Help text :: Explanation"
655
+msgid "Configuration is stored in a directory according to the `DERIVEPASSPHRASE_PATH` variable, which defaults to `~/.derivepassphrase` on UNIX-like systems and `C:\\Users\\<user>\\AppData\\Roaming\\Derivepassphrase` on Windows."
656
+msgstr ""
657
+
658
+#. TRANSLATORS: This is the first paragraph of the command help text, but it also appears (in truncated form, if necessary) as one-line help text for this command.  The translation should thus be as meaningful as possible even if truncated.
659
+#. 
660
+#. Message-ID: Label.DERIVEPASSPHRASE_EXPORT_01
661
+msgctxt "Label :: Help text :: Explanation"
662
+msgid "Export a foreign configuration to standard output."
663
+msgstr ""
664
+
665
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_EXPORT_02
666
+msgctxt "Label :: Help text :: Explanation"
667
+msgid "The only available subcommand is \"vault\", which implements the vault-native configuration scheme.  If no subcommand is given, we default to \"vault\"."
668
+msgstr ""
669
+
670
+#. TRANSLATORS: This is the first paragraph of the command help text, but it also appears (in truncated form, if necessary) as one-line help text for this command.  The translation should thus be as meaningful as possible even if truncated.
671
+#. 
672
+#. Message-ID: Label.DERIVEPASSPHRASE_EXPORT_VAULT_01
673
+msgctxt "Label :: Help text :: Explanation"
674
+msgid "Export a vault-native configuration to standard output."
675
+msgstr ""
676
+
677
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_EXPORT_VAULT_02
678
+#, python-brace-format
679
+msgctxt "Label :: Help text :: Explanation"
680
+msgid "Depending on the configuration format, {path_metavar!s} may either be a file or a directory.  We support the vault \"v0.2\", \"v0.3\" and \"storeroom\" formats."
681
+msgstr ""
682
+
683
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_EXPORT_VAULT_03
684
+#, python-brace-format
685
+msgctxt "Label :: Help text :: Explanation"
686
+msgid "If {path_metavar!s} is explicitly given as `VAULT_PATH`, then use the `VAULT_PATH` environment variable to determine the correct path.  (Use `./VAULT_PATH` or similar to indicate a file/directory actually named `VAULT_PATH`.)"
687
+msgstr ""
688
+
689
+#. TRANSLATORS: This is the first paragraph of the command help text, but it also appears (in truncated form, if necessary) as one-line help text for this command.  The translation should thus be as meaningful as possible even if truncated.
690
+#. 
691
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_01
692
+msgctxt "Label :: Help text :: Explanation"
693
+msgid "Derive a passphrase using the vault derivation scheme."
694
+msgstr ""
695
+
696
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_02
697
+#, python-brace-format
698
+msgctxt "Label :: Help text :: Explanation"
699
+msgid "If operating on global settings, or importing/exporting settings, then {service_metavar!s} must be omitted.  Otherwise it is required."
700
+msgstr ""
701
+
702
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_EPILOG_01
703
+msgctxt "Label :: Help text :: Explanation"
704
+msgid "WARNING: There is NO WAY to retrieve the generated passphrases if the master passphrase, the SSH key, or the exact passphrase settings are lost, short of trying out all possible combinations.  You are STRONGLY advised to keep independent backups of the settings and the SSH key, if any."
705
+msgstr ""
706
+
707
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_EPILOG_02
708
+msgctxt "Label :: Help text :: Explanation"
709
+msgid "The configuration is NOT encrypted, and you are STRONGLY discouraged from using a stored passphrase."
710
+msgstr ""
711
+
712
+#. TRANSLATORS: This instruction text is shown above the user's old stored notes for this service, if any, if the recommended "modern" editor interface is used.  The next line is the cut marking defined in Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER.
713
+#. 
714
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT
715
+msgctxt "Label :: Help text :: Explanation"
716
+msgid "# Enter notes below the line with the cut mark (ASCII scissors and\n"
717
+"# dashes).  Lines above the cut mark (such as this one) will be ignored.\n"
718
+"#\n"
719
+"# If you wish to clear the notes, leave everything beyond the cut mark\n"
720
+"# blank.  However, if you leave the *entire* file blank, also removing\n"
721
+"# the cut mark, then the edit is aborted, and the old notes contents are\n"
722
+"# retained.\n"
723
+"#"
724
+msgstr ""
725
+
726
+#. TRANSLATORS: This instruction text is shown if the vault(1)-compatible "legacy" editor interface is used and no previous notes exist.  The interface does not support commentary in the notes, so we fill this with obvious placeholder text instead.  (Please replace this with what *your* language/culture would obviously recognize as placeholder text.)
727
+#. 
728
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT
729
+msgctxt "Label :: Help text :: Explanation"
730
+msgid "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
731
+msgstr ""
732
+
733
+#. TRANSLATORS: The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.
734
+#. 
735
+#. Message-ID: Label.PASSPHRASE_GENERATION_EPILOG
736
+#, python-brace-format
737
+msgctxt "Label :: Help text :: Explanation"
738
+msgid "Use {metavar!s}=0 to exclude a character type from the output."
739
+msgstr ""
740
+
741
+#. TRANSLATORS: The metavar is Label.STORAGE_MANAGEMENT_METAVAR_PATH.
742
+#. 
743
+#. Message-ID: Label.STORAGE_MANAGEMENT_EPILOG
744
+#, python-brace-format
745
+msgctxt "Label :: Help text :: Explanation"
746
+msgid "Using \"-\" as {metavar!s} for standard input/standard output is supported."
747
+msgstr ""
748
+
749
+#. TRANSLATORS: We use this format string to indicate, at the beginning of a command's help text, that this command is deprecated.
750
+#. 
751
+#. Message-ID: Label.DEPRECATED_COMMAND_LABEL
752
+#, python-brace-format
753
+msgctxt "Label :: Help text :: Marker"
754
+msgid "(Deprecated) {text}"
755
+msgstr ""
756
+
757
+#. TRANSLATORS: The marker for separating the text from Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT from the user's input (below the marker).  The first line starting with this label marks the separation point.
758
+#. 
759
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER
760
+msgctxt "Label :: Help text :: Marker"
761
+msgid "# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -"
762
+msgstr ""
763
+
764
+#. TRANSLATORS: Message-ID: Label.EXPORT_VAULT_FORMAT_METAVAR_FMT
765
+msgctxt "Label :: Help text :: Metavar :: export vault"
766
+msgid "FMT"
767
+msgstr ""
768
+
769
+#. TRANSLATORS: See Label.EXPORT_VAULT_KEY_HELP_TEXT.
770
+#. 
771
+#. Message-ID: Label.EXPORT_VAULT_KEY_METAVAR_K
772
+msgctxt "Label :: Help text :: Metavar :: export vault"
773
+msgid "K"
774
+msgstr ""
775
+
776
+#. TRANSLATORS: Used as "path_metavar" in Label.DERIVEPASSPHRASE_EXPORT_VAULT_02 and others.
777
+#. 
778
+#. Message-ID: Label.EXPORT_VAULT_METAVAR_PATH
779
+msgctxt "Label :: Help text :: Metavar :: export vault"
780
+msgid "PATH"
781
+msgstr ""
782
+
783
+#. TRANSLATORS: This metavar is also used in a matching epilog.
784
+#. 
785
+#. Message-ID: Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
786
+msgctxt "Label :: Help text :: Metavar :: vault"
787
+msgid "NUMBER"
788
+msgstr ""
789
+
790
+#. TRANSLATORS: This metavar is also used in multiple one-line help texts.
791
+#. 
792
+#. Message-ID: Label.STORAGE_MANAGEMENT_METAVAR_PATH
793
+msgctxt "Label :: Help text :: Metavar :: vault"
794
+msgid "PATH"
795
+msgstr ""
796
+
797
+#. TRANSLATORS: This metavar is also used in multiple one-line help texts.
798
+#. 
799
+#. Message-ID: Label.VAULT_METAVAR_SERVICE
800
+msgctxt "Label :: Help text :: Metavar :: vault"
801
+msgid "SERVICE"
802
+msgstr ""
803
+
804
+#. TRANSLATORS: Message-ID: Label.DEBUG_OPTION_HELP_TEXT
805
+msgctxt "Label :: Help text :: One-line description"
806
+msgid "also emit debug information (implies --verbose)"
807
+msgstr ""
808
+
809
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT
810
+#, python-brace-format
811
+msgctxt "Label :: Help text :: One-line description"
812
+msgid "save the given settings for {service_metavar!s}, or global"
813
+msgstr ""
814
+
815
+#. TRANSLATORS: The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.
816
+#. 
817
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT
818
+#, python-brace-format
819
+msgctxt "Label :: Help text :: One-line description"
820
+msgid "ensure at least {metavar!s} \"-\" or \"_\" characters"
821
+msgstr ""
822
+
823
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT
824
+msgctxt "Label :: Help text :: One-line description"
825
+msgid "delete all settings"
826
+msgstr ""
827
+
828
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT
829
+msgctxt "Label :: Help text :: One-line description"
830
+msgid "delete the global settings"
831
+msgstr ""
832
+
833
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT
834
+#, python-brace-format
835
+msgctxt "Label :: Help text :: One-line description"
836
+msgid "delete the settings for {service_metavar!s}"
837
+msgstr ""
838
+
839
+#. TRANSLATORS: The corresponding option is displayed as "--modern-editor-interface / --vault-legacy-editor-interface", so you may want to hint that the default (legacy) is the second of those options.  Though the vault(1) legacy editor interface clearly has deficiencies and (in my opinion) should only be used for compatibility purposes, the one-line help text should try not to sound too judgmental, if possible.
840
+#. 
841
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT
842
+msgctxt "Label :: Help text :: One-line description"
843
+msgid "edit notes using the modern editor interface or the vault-like legacy one (default)"
844
+msgstr ""
845
+
846
+#. TRANSLATORS: The corresponding option is displayed as "--export-as=json|sh", so json refers to the JSON format (default) and sh refers to the POSIX sh format.
847
+#. 
848
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT
849
+msgctxt "Label :: Help text :: One-line description"
850
+msgid "when exporting, export as JSON (default) or POSIX sh"
851
+msgstr ""
852
+
853
+#. TRANSLATORS: The metavar is Label.STORAGE_MANAGEMENT_METAVAR_SERVICE.
854
+#. 
855
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT
856
+#, python-brace-format
857
+msgctxt "Label :: Help text :: One-line description"
858
+msgid "export all saved settings to {metavar!s}"
859
+msgstr ""
860
+
861
+#. TRANSLATORS: The metavar is Label.STORAGE_MANAGEMENT_METAVAR_SERVICE.
862
+#. 
863
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT
864
+#, python-brace-format
865
+msgctxt "Label :: Help text :: One-line description"
866
+msgid "import saved settings from {metavar!s}"
867
+msgstr ""
868
+
869
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT
870
+msgctxt "Label :: Help text :: One-line description"
871
+msgid "select a suitable SSH key from the SSH agent"
872
+msgstr ""
873
+
874
+#. TRANSLATORS: The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.
875
+#. 
876
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT
877
+#, python-brace-format
878
+msgctxt "Label :: Help text :: One-line description"
879
+msgid "ensure a passphrase length of {metavar!s} characters"
880
+msgstr ""
881
+
882
+#. TRANSLATORS: The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.
883
+#. 
884
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT
885
+#, python-brace-format
886
+msgctxt "Label :: Help text :: One-line description"
887
+msgid "ensure at least {metavar!s} lowercase characters"
888
+msgstr ""
889
+
890
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT
891
+#, python-brace-format
892
+msgctxt "Label :: Help text :: One-line description"
893
+msgid "spawn an editor to edit notes for {service_metavar!s}"
894
+msgstr ""
895
+
896
+#. TRANSLATORS: The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.
897
+#. 
898
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT
899
+#, python-brace-format
900
+msgctxt "Label :: Help text :: One-line description"
901
+msgid "ensure at least {metavar!s} digits"
902
+msgstr ""
903
+
904
+#. TRANSLATORS: The corresponding option is displayed as "--overwrite-existing / --merge-existing", so you may want to hint that the default (merge) is the second of those options.
905
+#. 
906
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT
907
+msgctxt "Label :: Help text :: One-line description"
908
+msgid "overwrite or merge (default) the existing configuration"
909
+msgstr ""
910
+
911
+#. TRANSLATORS: Message-ID: Label.DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT
912
+msgctxt "Label :: Help text :: One-line description"
913
+msgid "prompt for a master passphrase"
914
+msgstr ""
915
+
916
+#. TRANSLATORS: The corresponding option is displayed as "--print-notes-before / --print-notes-after", so you may want to hint that the default (after) is the second of those options.
917
+#. 
918
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT
919
+msgctxt "Label :: Help text :: One-line description"
920
+msgid "print the service notes (if any) before or after (default) the existing configuration"
921
+msgstr ""
922
+
923
+#. TRANSLATORS: The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.
924
+#. 
925
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT
926
+#, python-brace-format
927
+msgctxt "Label :: Help text :: One-line description"
928
+msgid "forbid any run of {metavar!s} identical characters"
929
+msgstr ""
930
+
931
+#. TRANSLATORS: The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.
932
+#. 
933
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT
934
+#, python-brace-format
935
+msgctxt "Label :: Help text :: One-line description"
936
+msgid "ensure at least {metavar!s} spaces"
937
+msgstr ""
938
+
939
+#. TRANSLATORS: The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.
940
+#. 
941
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT
942
+#, python-brace-format
943
+msgctxt "Label :: Help text :: One-line description"
944
+msgid "ensure at least {metavar!s} symbol characters"
945
+msgstr ""
946
+
947
+#. TRANSLATORS: The corresponding option is displayed as "--unset=phrase|key|...|symbol", so the "given setting" is referring to "phrase", "key", "lower", ..., or "symbol", respectively.  "with --config" here means that the user must also specify "--config" for this option to have any effect.
948
+#. 
949
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT
950
+msgctxt "Label :: Help text :: One-line description"
951
+msgid "with --config, also unsets the given setting; may be specified multiple times"
952
+msgstr ""
953
+
954
+#. TRANSLATORS: The metavar is Label.PASSPHRASE_GENERATION_METAVAR_NUMBER.
955
+#. 
956
+#. Message-ID: Label.DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT
957
+#, python-brace-format
958
+msgctxt "Label :: Help text :: One-line description"
959
+msgid "ensure at least {metavar!s} uppercase characters"
960
+msgstr ""
961
+
962
+#. TRANSLATORS: See EXPORT_VAULT_FORMAT_HELP_TEXT.  The format names/labels "v0.3", "v0.2" and "storeroom" should not be translated.
963
+#. 
964
+#. Message-ID: Label.EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT
965
+msgctxt "Label :: Help text :: One-line description"
966
+msgid "(default: v0.3, v0.2, storeroom)"
967
+msgstr ""
968
+
969
+#. TRANSLATORS: The defaults_hint is Label.EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT, the metavar is Label.EXPORT_VAULT_FORMAT_METAVAR_FMT.
970
+#. 
971
+#. Message-ID: Label.EXPORT_VAULT_FORMAT_HELP_TEXT
972
+#, python-brace-format
973
+msgctxt "Label :: Help text :: One-line description"
974
+msgid "try the following storage format {metavar!s}; may be specified multiple times, formats will be tried in order {defaults_hint!s}"
975
+msgstr ""
976
+
977
+#. TRANSLATORS: See EXPORT_VAULT_KEY_HELP_TEXT.
978
+#. 
979
+#. Message-ID: Label.EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT
980
+msgctxt "Label :: Help text :: One-line description"
981
+msgid "(default: check the `VAULT_KEY`, `LOGNAME`, `USER`, or `USERNAME` environment variables)"
982
+msgstr ""
983
+
984
+#. TRANSLATORS: The defaults_hint is Label.EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT, the metavar is Label.EXPORT_VAULT_KEY_METAVAR_K.
985
+#. 
986
+#. Message-ID: Label.EXPORT_VAULT_KEY_HELP_TEXT
987
+#, python-brace-format
988
+msgctxt "Label :: Help text :: One-line description"
989
+msgid "use {metavar!s} as the storage master key {defaults_hint!s}"
990
+msgstr ""
991
+
992
+#. TRANSLATORS: Message-ID: Label.HELP_OPTION_HELP_TEXT
993
+msgctxt "Label :: Help text :: One-line description"
994
+msgid "show this help text, then exit"
995
+msgstr ""
996
+
997
+#. TRANSLATORS: Message-ID: Label.QUIET_OPTION_HELP_TEXT
998
+msgctxt "Label :: Help text :: One-line description"
999
+msgid "suppress even warnings, emit only errors"
1000
+msgstr ""
1001
+
1002
+#. TRANSLATORS: Message-ID: Label.VERBOSE_OPTION_HELP_TEXT
1003
+msgctxt "Label :: Help text :: One-line description"
1004
+msgid "emit extra/progress information to standard error"
1005
+msgstr ""
1006
+
1007
+#. TRANSLATORS: Message-ID: Label.VERSION_OPTION_HELP_TEXT
1008
+msgctxt "Label :: Help text :: One-line description"
1009
+msgid "show applicable version information, then exit"
1010
+msgstr ""
1011
+
1012
+#. TRANSLATORS: Message-ID: Label.COMMANDS_LABEL
1013
+msgctxt "Label :: Help text :: Option group name"
1014
+msgid "Commands"
1015
+msgstr ""
1016
+
1017
+#. TRANSLATORS: Message-ID: Label.COMPATIBILITY_OPTION_LABEL
1018
+msgctxt "Label :: Help text :: Option group name"
1019
+msgid "Compatibility and extension options"
1020
+msgstr ""
1021
+
1022
+#. TRANSLATORS: Message-ID: Label.CONFIGURATION_LABEL
1023
+msgctxt "Label :: Help text :: Option group name"
1024
+msgid "Configuration"
1025
+msgstr ""
1026
+
1027
+#. TRANSLATORS: Message-ID: Label.LOGGING_LABEL
1028
+msgctxt "Label :: Help text :: Option group name"
1029
+msgid "Logging"
1030
+msgstr ""
1031
+
1032
+#. TRANSLATORS: Message-ID: Label.OPTIONS_LABEL
1033
+msgctxt "Label :: Help text :: Option group name"
1034
+msgid "Options"
1035
+msgstr ""
1036
+
1037
+#. TRANSLATORS: Message-ID: Label.OTHER_OPTIONS_LABEL
1038
+msgctxt "Label :: Help text :: Option group name"
1039
+msgid "Other options"
1040
+msgstr ""
1041
+
1042
+#. TRANSLATORS: Message-ID: Label.PASSPHRASE_GENERATION_LABEL
1043
+msgctxt "Label :: Help text :: Option group name"
1044
+msgid "Passphrase generation"
1045
+msgstr ""
1046
+
1047
+#. TRANSLATORS: Message-ID: Label.STORAGE_MANAGEMENT_LABEL
1048
+msgctxt "Label :: Help text :: Option group name"
1049
+msgid "Storage management"
1050
+msgstr ""
1051
+
1052
+#. TRANSLATORS: Message-ID: Label.VERSION_INFO_TEXT
1053
+#, python-brace-format
1054
+msgctxt "Label :: Info Message"
1055
+msgid "{PROG_NAME!s} {VERSION}"
1056
+msgstr ""
1057
+
1058
+#. TRANSLATORS: There is no support for "yes" or "no" in other languages than English, so it is advised that your translation makes it clear that only the strings "y", "yes", "n" or "no" are supported, even if the prompt becomes a bit longer.
1059
+#. 
1060
+#. Message-ID: Label.CONFIRM_THIS_CHOICE_PROMPT_TEXT
1061
+msgctxt "Label :: Interactive prompt"
1062
+msgid "Confirm this choice?  (y/N)"
1063
+msgstr ""
1064
+
1065
+#. TRANSLATORS: This label is the heading of the list of suitable SSH keys.
1066
+#. 
1067
+#. Message-ID: Label.SUITABLE_SSH_KEYS_LABEL
1068
+msgctxt "Label :: Interactive prompt"
1069
+msgid "Suitable SSH keys:"
1070
+msgstr ""
1071
+
1072
+#. TRANSLATORS: Message-ID: Label.YOUR_SELECTION_PROMPT_TEXT
1073
+#, python-brace-format
1074
+msgctxt "Label :: Interactive prompt"
1075
+msgid "Your selection?  (1-{n}, leave empty to abort)"
1076
+msgstr ""
1077
+
1078
+#. TRANSLATORS: Message-ID: WarnMsgTemplate.EDITING_NOTES_BUT_NOT_STORING_CONFIG
1079
+msgctxt "Warning message"
1080
+msgid "Specifying --notes without --config is ineffective.  No notes will be edited."
1081
+msgstr ""
1082
+
1083
+#. TRANSLATORS: Message-ID: WarnMsgTemplate.EMPTY_SERVICE_NOT_SUPPORTED
1084
+#, python-brace-format
1085
+msgctxt "Warning message"
1086
+msgid "An empty {service_metavar!s} is not supported by vault(1).  For compatibility, this will be treated as if {service_metavar!s} was not supplied, i.e., it will error out, or operate on global settings."
1087
+msgstr ""
1088
+
1089
+#. TRANSLATORS: Message-ID: WarnMsgTemplate.EMPTY_SERVICE_SETTINGS_INACCESSIBLE
1090
+#, python-brace-format
1091
+msgctxt "Warning message"
1092
+msgid "An empty {service_metavar!s} is not supported by vault(1).  The empty-string service settings will be inaccessible and ineffective.  To ensure that vault(1) and {PROG_NAME!s} see the settings, move them into the \"global\" section."
1093
+msgstr ""
1094
+
1095
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
1096
+#. 
1097
+#. Message-ID: WarnMsgTemplate.FAILED_TO_MIGRATE_CONFIG
1098
+#, python-brace-format
1099
+msgctxt "Warning message"
1100
+msgid "Failed to migrate to {path!r}: {error!s}: {filename!r}."
1101
+msgstr ""
1102
+
1103
+#. TRANSLATORS: "error" is supplied by the operating system (errno/strerror).
1104
+#. 
1105
+#. Message-ID: WarnMsgTemplate.FAILED_TO_MIGRATE_CONFIG
1106
+#, python-brace-format
1107
+msgctxt "Warning message"
1108
+msgid "Failed to migrate to {path!r}: {error!s}."
1109
+msgstr ""
1110
+
1111
+#. TRANSLATORS: Message-ID: WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE
1112
+msgctxt "Warning message"
1113
+msgid "Setting a global passphrase is ineffective because a key is also set."
1114
+msgstr ""
1115
+
1116
+#. TRANSLATORS: Message-ID: WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP
1117
+#, python-brace-format
1118
+msgctxt "Warning message"
1119
+msgid "A backup copy of the old notes was saved to {filename!r}.  This is a safeguard against editing mistakes, because the vault(1)-compatible legacy editor interface does not allow aborting mid-edit, and because the notes were actually changed."
1120
+msgstr ""
1121
+
1122
+#. TRANSLATORS: The key is a (vault) configuration key, in JSONPath syntax, typically "$.global" for the global passphrase or "$.services.service_name" or "$.services["service with spaces"]" for the services "service_name" and "service with spaces", respectively.  The form is one of the four Unicode normalization forms: NFC, NFD, NFKC, NFKD.  The asterisks are not special.  Please feel free to substitute any other appropriate way to mark up emphasis of the word "displays".
1123
+#. 
1124
+#. Message-ID: WarnMsgTemplate.PASSPHRASE_NOT_NORMALIZED
1125
+#, python-brace-format
1126
+msgctxt "Warning message"
1127
+msgid "The {key!s} passphrase is not {form!s}-normalized.  Its serialization as a byte string may not be what you expect it to be, even if it *displays* correctly.  Please make sure to double-check any derived passphrases for unexpected results."
1128
+msgstr ""
1129
+
1130
+#. TRANSLATORS: Message-ID: WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE
1131
+#, python-brace-format
1132
+msgctxt "Warning message"
1133
+msgid "The service name {service!r} contains an ASCII control character, which is not supported by our shell completion code.  This service name will therefore not be available for completion on the command-line.  You may of course still type it in manually in whatever format your shell accepts, but we highly recommend choosing a different service name instead."
1134
+msgstr ""
1135
+
1136
+#. TRANSLATORS: The key that is set need not necessarily be set at the service level; it may be a global key as well.
1137
+#. 
1138
+#. Message-ID: WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE
1139
+#, python-brace-format
1140
+msgctxt "Warning message"
1141
+msgid "Setting a service passphrase is ineffective because a key is also set: {service!s}."
1142
+msgstr ""
1143
+
1144
+#. TRANSLATORS: Message-ID: WarnMsgTemplate.STEP_REMOVE_INEFFECTIVE_VALUE
1145
+#, python-brace-format
1146
+msgctxt "Warning message"
1147
+msgid "Removing ineffective setting {path!s} = {old!s}."
1148
+msgstr ""
1149
+
1150
+#. TRANSLATORS: Message-ID: WarnMsgTemplate.STEP_REPLACE_INVALID_VALUE
1151
+#, python-brace-format
1152
+msgctxt "Warning message"
1153
+msgid "Replacing invalid value {old!s} for key {path!s} with {new!s}."
1154
+msgstr ""
1155
+
1156
+#. TRANSLATORS: Message-ID: WarnMsgTemplate.V01_STYLE_CONFIG
1157
+#, python-brace-format
1158
+msgctxt "Warning message :: Deprecation"
1159
+msgid "Using deprecated v0.1-style config file {old!r}, instead of v0.2-style {new!r}.  Support for v0.1-style config filenames will be removed in v1.0."
1160
+msgstr ""
1161
+
1162
+#. TRANSLATORS: This deprecation warning may be issued at any level, i.e. we may actually be talking about subcommands, or sub-subcommands, or sub-sub-subcommands, etc., which is what the "here" is supposed to indicate.
1163
+#. 
1164
+#. Message-ID: WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED
1165
+msgctxt "Warning message :: Deprecation"
1166
+msgid "A subcommand will be required here in v1.0.  See --help for available subcommands.  Defaulting to subcommand \"vault\"."
1167
+msgstr ""
... ...
@@ -1,5 +1,5 @@
1 1
 [build-system]
2
-requires = ["hatchling"]
2
+requires = ["hatchling", "hatch-gettext"]
3 3
 build-backend = "hatchling.build"
4 4
 
5 5
 [project]
... ...
@@ -229,6 +229,17 @@ omit = [
229 229
 ]
230 230
 dynamic_context = 'test_function'
231 231
 
232
+[tool.hatch.build.hooks.gettext]
233
+# Leave out 'i18n-name', which already defaults to the project.name key,
234
+# and 'po-directory', which defaults to 'po'.
235
+locale-directory = 'share/locale'
236
+# The following would need extra tooling, which we cannot necessarily
237
+# assume.  'regenerate-template' in particular doesn't work here because
238
+# we don't use xgettext to build the template files.
239
+identify-left-out = false
240
+regenerate-template = false
241
+show-report = false
242
+
232 243
 [tool.hatch.build.targets.sdist]
233 244
 exclude = [
234 245
     'docs/changelog.d/*.md',
... ...
@@ -237,6 +248,7 @@ exclude = [
237 248
 [tool.hatch.build.targets.wheel]
238 249
 include = [
239 250
     'src/derivepassphrase',
251
+    'share/locale/',
240 252
     'share/man/',
241 253
 ]
242 254
 sources = ['src']
... ...
@@ -8,23 +8,27 @@
8 8
 """Run various quality control checks automatically.
9 9
 
10 10
 Distinguish between the master branch and other branches: run the full
11
-test suite and build the documentation only on the master branch,
12
-otherwise use only a reduced set of test environments and don't build
13
-the documentation at all.  In both cases, run the linter, the formatter,
14
-and the type checker.
11
+test suite and build the translations and the documentation only on the
12
+master branch, otherwise use only a reduced set of test environments and
13
+don't build anything.  In both cases, run the linter, the formatter, and
14
+the type checker.
15 15
 
16 16
 If we are currently in a Stacked Git patch queue, do not run any tests,
17
-do not run the type checker and do not build the documentation.  These
18
-all slow down patch refreshing to a grinding halt, and will be checked
19
-afterwards anyway when merging the patch queue back into the master
20
-branch.  Stick to formatting and linting only.
17
+do not run the type checker and do not build anything.  These all slow
18
+down patch refreshing to a grinding halt, and will be checked afterwards
19
+anyway when merging the patch queue back into the master branch.  Stick
20
+to formatting and linting only.
21 21
 
22 22
 """
23 23
 
24
+import hashlib
24 25
 import os
26
+import pathlib
25 27
 import subprocess
26 28
 import sys
27 29
 
30
+BLOCK_SIZE = 4096
31
+
28 32
 envs = ['3.9', '3.11', '3.13', 'pypy3.10']
29 33
 opts = ['-py', ','.join(envs)]
30 34
 
... ...
@@ -70,19 +74,47 @@ try:
70 74
         subprocess.run(
71 75
             ['hatch', 'env', 'run', '-e', 'types', '--', 'check'], check=True
72 76
         )
77
+        try:
78
+            h = hashlib.sha256(
79
+                pathlib.Path('po/derivepassphrase.pot').read_bytes(),
80
+                usedforsecurity=True,
81
+            )
82
+        except FileNotFoundError:
83
+            pass
84
+        else:
85
+            h2 = hashlib.sha256(
73 86
                 subprocess.run(
74
-            ['hatch', 'test', '-acpqr', '--', '--maxfail', '1'],
87
+                    [
88
+                        'hatch',
89
+                        'run',
90
+                        'python3',
91
+                        '-m',
92
+                        'derivepassphrase._internals.cli_messages',
93
+                    ],
75 94
                     check=True,
95
+                    stdout=subprocess.PIPE,
96
+                    input=b'',
97
+                ).stdout,
98
+                usedforsecurity=True,
99
+            )
100
+            if h.digest() != h2.digest():
101
+                sys.exit(
102
+                    'ERROR: po/derivepassphrase.pot '
103
+                    'has unreproducible contents'
76 104
                 )
77 105
         # fmt: off
78 106
         subprocess.run(
79 107
             [
80 108
                 'hatch', 'env', 'run', '-e', 'docs', '--',
81
-                'build', '-f', 'mkdocs_devsetup.yaml',
109
+                'build', '-f', 'mkdocs_devsetup.yml',
82 110
             ],
83 111
             check=True,
84 112
         )
85 113
         # fmt: on
114
+        subprocess.run(
115
+            ['hatch', 'test', '-acpqr', '--', '--maxfail', '1'],
116
+            check=True,
117
+        )
86 118
     elif not is_stgit_patch:
87 119
         subprocess.run(
88 120
             ['hatch', 'env', 'run', '-e', 'types', '--', 'check'], check=True
... ...
@@ -0,0 +1 @@
1
+*/LC_MESSAGES/derivepassphrase.mo
... ...
@@ -79,6 +79,15 @@ def load_translations(
79 79
 
80 80
     """
81 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`.
82 91
         if sys.platform.startswith('win'):
83 92
             xdg_data_home = (
84 93
                 pathlib.Path(os.environ['APPDATA'])
... ...
@@ -2228,6 +2237,7 @@ def _write_po_file(  # noqa: C901,PLR0912
2228 2237
     *,
2229 2238
     is_template: bool = True,
2230 2239
     version: str = VERSION,
2240
+    build_time: datetime.datetime | None = None,
2231 2241
 ) -> None:  # pragma: no cover
2232 2242
     r"""Write a .po file to the given file object.
2233 2243
 
... ...
@@ -2256,8 +2266,7 @@ def _write_po_file(  # noqa: C901,PLR0912
2256 2266
                     f'{entries[ctx][msg]!r} and {member!r}'
2257 2267
                 )
2258 2268
             entries[ctx][msg] = member
2259
-    build_time = datetime.datetime.now().astimezone()
2260
-    if os.environ.get('SOURCE_DATE_EPOCH'):
2269
+    if build_time is None and os.environ.get('SOURCE_DATE_EPOCH'):
2261 2270
         try:
2262 2271
             source_date_epoch = int(os.environ['SOURCE_DATE_EPOCH'])
2263 2272
         except ValueError as exc:
... ...
@@ -2268,6 +2277,8 @@ def _write_po_file(  # noqa: C901,PLR0912
2268 2277
                 source_date_epoch,
2269 2278
                 tz=datetime.timezone.utc,
2270 2279
             )
2280
+    elif build_time is None:
2281
+        build_time = datetime.datetime.now().astimezone()
2271 2282
     if is_template:
2272 2283
         header = (
2273 2284
             inspect.cleandoc(rf"""
... ...
@@ -2434,6 +2445,14 @@ def _cstr(s: str) -> str:  # pragma: no cover
2434 2445
 if __name__ == '__main__':
2435 2446
     import argparse
2436 2447
 
2448
+    def validate_build_time(value: str | None) -> datetime.datetime | None:
2449
+        if value is None:
2450
+            return None
2451
+        ret = datetime.datetime.fromisoformat(value)
2452
+        if ret.isoformat(sep=' ', timespec='seconds') != value:
2453
+            raise ValueError(f'invalid time specification: {value}')  # noqa: EM102,TRY003
2454
+        return ret
2455
+
2437 2456
     ap = argparse.ArgumentParser()
2438 2457
     ex = ap.add_mutually_exclusive_group()
2439 2458
     ex.add_argument(
... ...
@@ -2457,9 +2476,19 @@ if __name__ == '__main__':
2457 2476
         default=VERSION,
2458 2477
         help='Override declared software version',
2459 2478
     )
2479
+    ap.add_argument(
2480
+        '--set-build-time',
2481
+        action='store',
2482
+        dest='build_time',
2483
+        default=None,
2484
+        type=validate_build_time,
2485
+        help='Override the time of build (YYYY-MM-DD HH:MM:SS+HH:MM format, '
2486
+        'default: $SOURCE_DATE_EPOCH, or the current time)',
2487
+    )
2460 2488
     args = ap.parse_args()
2461 2489
     _write_po_file(
2462 2490
         sys.stdout,
2463 2491
         version=args.version,
2464 2492
         is_template=args.is_template,
2493
+        build_time=args.build_time,
2465 2494
     )
2466 2495