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 |