Marco Ricci commited on 2025-01-13 15:49:08
Zeige 1 geänderte Dateien mit 125 Einfügungen und 33 Löschungen.
Save for a few factory functions to make enum definitions more readable, the normalization machinery for translatable strings belongs in their class. I originally coded this separately because I mistakenly believed that `typing.NamedTuple` classes cannot contain methods of their own.
... | ... |
@@ -206,6 +206,30 @@ class TranslatableString(NamedTuple): |
206 | 206 |
translator_comments: str |
207 | 207 |
flags: frozenset[str] |
208 | 208 |
|
209 |
+ @staticmethod |
|
210 |
+ def _maybe_rewrap( |
|
211 |
+ string: str, |
|
212 |
+ /, |
|
213 |
+ *, |
|
214 |
+ fix_sentence_endings: bool = True, |
|
215 |
+ ) -> str: |
|
216 |
+ string = inspect.cleandoc(string) |
|
217 |
+ if not any(s.strip() == '\b' for s in string.splitlines()): |
|
218 |
+ string = '\n'.join( |
|
219 |
+ textwrap.wrap( |
|
220 |
+ string, |
|
221 |
+ width=float('inf'), # type: ignore[arg-type] |
|
222 |
+ fix_sentence_endings=fix_sentence_endings, |
|
223 |
+ ) |
|
224 |
+ ) |
|
225 |
+ else: |
|
226 |
+ string = ''.join( |
|
227 |
+ s |
|
228 |
+ for s in string.splitlines(True) # noqa: FBT003 |
|
229 |
+ if s.strip() != '\b' |
|
230 |
+ ) |
|
231 |
+ return string |
|
232 |
+ |
|
209 | 233 |
def maybe_without_filename(self) -> Self: |
210 | 234 |
"""Return a new translatable string without the "filename" field. |
211 | 235 |
|
... | ... |
@@ -226,6 +250,83 @@ class TranslatableString(NamedTuple): |
226 | 250 |
ret = ret._replace(plural=(c + d)) |
227 | 251 |
return ret |
228 | 252 |
|
253 |
+ def rewrapped(self) -> Self: |
|
254 |
+ """Return a rewrapped version of self. |
|
255 |
+ |
|
256 |
+ Normalizes all parts assumed to contain English prose. |
|
257 |
+ |
|
258 |
+ """ |
|
259 |
+ msg = self._maybe_rewrap(self.singular, fix_sentence_endings=True) |
|
260 |
+ plural = self._maybe_rewrap(self.plural, fix_sentence_endings=True) |
|
261 |
+ context = self.l10n_context.strip() |
|
262 |
+ comments = self._maybe_rewrap( |
|
263 |
+ self.translator_comments, fix_sentence_endings=False |
|
264 |
+ ) |
|
265 |
+ return self._replace( |
|
266 |
+ singular=msg, |
|
267 |
+ plural=plural, |
|
268 |
+ l10n_context=context, |
|
269 |
+ translator_comments=comments, |
|
270 |
+ ) |
|
271 |
+ |
|
272 |
+ def with_comments(self, comments: str, /) -> Self: |
|
273 |
+ """Add or replace the string's translator comments. |
|
274 |
+ |
|
275 |
+ The comments are assumed to contain English prose, and will be |
|
276 |
+ normalized. |
|
277 |
+ |
|
278 |
+ Returns: |
|
279 |
+ A new [`TranslatableString`][] with the specified comments. |
|
280 |
+ |
|
281 |
+ """ |
|
282 |
+ if not comments.lstrip().startswith( # pragma: no cover |
|
283 |
+ 'TRANSLATORS:' |
|
284 |
+ ): |
|
285 |
+ comments = 'TRANSLATORS: ' + comments.lstrip() |
|
286 |
+ comments = self._maybe_rewrap(comments, fix_sentence_endings=False) |
|
287 |
+ return self._replace(translator_comments=comments) |
|
288 |
+ |
|
289 |
+ def validate_flags(self, *extra_flags: str) -> Self: |
|
290 |
+ """Add all flags, then validate them against the string. |
|
291 |
+ |
|
292 |
+ Returns: |
|
293 |
+ A new [`TranslatableString`][] with the extra flags added, |
|
294 |
+ and all flags validated. |
|
295 |
+ |
|
296 |
+ Raises: |
|
297 |
+ ValueError: |
|
298 |
+ The flags failed to validate. See the exact error |
|
299 |
+ message for details. |
|
300 |
+ |
|
301 |
+ """ |
|
302 |
+ all_flags = frozenset( |
|
303 |
+ f.strip() for f in self.flags.union(extra_flags) |
|
304 |
+ ) |
|
305 |
+ if '{' in self.singular and not bool( |
|
306 |
+ all_flags & {'python-brace-format', 'no-python-brace-format'} |
|
307 |
+ ): |
|
308 |
+ msg = ( |
|
309 |
+ f'Missing flag for how to deal with brace character ' |
|
310 |
+ f'in {self.singular!r}' |
|
311 |
+ ) |
|
312 |
+ raise ValueError(msg) |
|
313 |
+ if '%' in self.singular and not bool( |
|
314 |
+ all_flags & {'python-format', 'no-python-format'} |
|
315 |
+ ): |
|
316 |
+ msg = ( |
|
317 |
+ f'Missing flag for how to deal with percent character ' |
|
318 |
+ f'in {self.singular!r}' |
|
319 |
+ ) |
|
320 |
+ raise ValueError(msg) |
|
321 |
+ if ( |
|
322 |
+ all_flags & {'python-format', 'python-brace-format'} |
|
323 |
+ and '%' not in self.singular |
|
324 |
+ and '{' not in self.singular |
|
325 |
+ ): |
|
326 |
+ msg = f'Missing format string parameters in {self.singular!r}' |
|
327 |
+ raise ValueError(msg) |
|
328 |
+ return self._replace(flags=all_flags) |
|
329 |
+ |
|
229 | 330 |
|
230 | 331 |
def _prepare_translatable( |
231 | 332 |
msg: str, |
... | ... |
@@ -235,45 +336,36 @@ def _prepare_translatable( |
235 | 336 |
*, |
236 | 337 |
flags: Iterable[str] = (), |
237 | 338 |
) -> TranslatableString: |
238 |
- def maybe_rewrap(string: str) -> str: |
|
239 |
- string = inspect.cleandoc(string) |
|
240 |
- if not any(s.strip() == '\b' for s in string.splitlines()): |
|
241 |
- string = '\n'.join( |
|
242 |
- textwrap.wrap( |
|
243 |
- string, |
|
244 |
- width=float('inf'), # type: ignore[arg-type] |
|
245 |
- fix_sentence_endings=True, |
|
339 |
+ return translatable( |
|
340 |
+ context, msg, plural=plural, comments=comments, flags=flags |
|
246 | 341 |
) |
247 |
- ) |
|
248 |
- else: |
|
249 |
- string = ''.join( |
|
250 |
- s |
|
251 |
- for s in string.splitlines(True) # noqa: FBT003 |
|
252 |
- if s.strip() != '\b' |
|
253 |
- ) |
|
254 |
- return string |
|
255 | 342 |
|
256 |
- msg = maybe_rewrap(msg) |
|
257 |
- plural_msg = maybe_rewrap(plural_msg) |
|
258 |
- context = context.strip() |
|
259 |
- comments = inspect.cleandoc(comments) |
|
343 |
+ |
|
344 |
+def translatable( |
|
345 |
+ context: str, |
|
346 |
+ single: str, |
|
347 |
+ /, |
|
348 |
+ flags: Iterable[str] = (), |
|
349 |
+ plural: str = '', |
|
350 |
+ comments: str = '', |
|
351 |
+) -> TranslatableString: |
|
352 |
+ """Return a [`TranslatableString`][] with validated parts. |
|
353 |
+ |
|
354 |
+ This factory function is really only there to make the enum |
|
355 |
+ definitions more readable. |
|
356 |
+ |
|
357 |
+ """ |
|
260 | 358 |
flags = ( |
261 |
- frozenset(f.strip() for f in flags) |
|
359 |
+ frozenset(flags) |
|
262 | 360 |
if not isinstance(flags, str) |
263 | 361 |
else frozenset({flags}) |
264 | 362 |
) |
265 |
- assert '{' not in msg or bool( |
|
266 |
- flags & {'python-brace-format', 'no-python-brace-format'} |
|
267 |
- ), f'Missing flag for how to deal with brace in {msg!r}' |
|
268 |
- assert '%' not in msg or bool( |
|
269 |
- flags & {'python-format', 'no-python-format'} |
|
270 |
- ), f'Missing flag for how to deal with percent character in {msg!r}' |
|
271 |
- assert ( |
|
272 |
- not flags & {'python-format', 'python-brace-format'} |
|
273 |
- or '%' in msg |
|
274 |
- or '{' in msg |
|
275 |
- ), f'Missing format string parameters in {msg!r}' |
|
276 |
- return TranslatableString(msg, plural_msg, context, comments, flags) |
|
363 |
+ return ( |
|
364 |
+ TranslatableString(context, single, plural=plural, flags=flags) |
|
365 |
+ .rewrapped() |
|
366 |
+ .with_comments(comments) |
|
367 |
+ .validate_flags() |
|
368 |
+ ) |
|
277 | 369 |
|
278 | 370 |
|
279 | 371 |
class TranslatedString: |
280 | 372 |