47
47
import operator
48
48
import os
49
49
import sys
50
+ from functools import cache
51
+ from string .templatelib import Interpolation , Template , convert
52
+ from typing import Any
50
53
51
54
52
55
__all__ = ['NullTranslations' , 'GNUTranslations' , 'Catalog' ,
@@ -291,11 +294,23 @@ def add_fallback(self, fallback):
291
294
def gettext (self , message ):
292
295
if self ._fallback :
293
296
return self ._fallback .gettext (message )
297
+ if isinstance (message , Template ):
298
+ message , values = _template_to_format (message )
299
+ return message .format (** values )
294
300
return message
295
301
296
302
def ngettext (self , msgid1 , msgid2 , n ):
297
303
if self ._fallback :
298
304
return self ._fallback .ngettext (msgid1 , msgid2 , n )
305
+ msgid1_is_template = isinstance (msgid1 , Template )
306
+ msgid2_is_template = isinstance (msgid2 , Template )
307
+ if msgid1_is_template and msgid2_is_template :
308
+ message , values = _template_to_format (
309
+ msgid1 if n == 1 else msgid2
310
+ )
311
+ return message .format (** values )
312
+ elif msgid1_is_template or msgid2_is_template :
313
+ raise TypeError ('msgids cannot mix strings and t-strings' )
299
314
n = _as_int2 (n )
300
315
if n == 1 :
301
316
return msgid1
@@ -305,11 +320,23 @@ def ngettext(self, msgid1, msgid2, n):
305
320
def pgettext (self , context , message ):
306
321
if self ._fallback :
307
322
return self ._fallback .pgettext (context , message )
323
+ if isinstance (message , Template ):
324
+ message , values = _template_to_format (message )
325
+ return message .format (** values )
308
326
return message
309
327
310
328
def npgettext (self , context , msgid1 , msgid2 , n ):
311
329
if self ._fallback :
312
330
return self ._fallback .npgettext (context , msgid1 , msgid2 , n )
331
+ msgid1_is_template = isinstance (msgid1 , Template )
332
+ msgid2_is_template = isinstance (msgid2 , Template )
333
+ if msgid1_is_template and msgid2_is_template :
334
+ message , values = _template_to_format (
335
+ msgid1 if n == 1 else msgid2
336
+ )
337
+ return message .format (** values )
338
+ elif msgid1_is_template or msgid2_is_template :
339
+ raise TypeError ('msgids cannot mix strings and t-strings' )
313
340
n = _as_int2 (n )
314
341
if n == 1 :
315
342
return msgid1
@@ -438,50 +465,104 @@ def _parse(self, fp):
438
465
439
466
def gettext (self , message ):
440
467
missing = object ()
468
+ orig_message = message
469
+ t_values = None
470
+ if isinstance (message , Template ):
471
+ message , t_values = _template_to_format (message )
441
472
tmsg = self ._catalog .get (message , missing )
442
473
if tmsg is missing :
443
474
tmsg = self ._catalog .get ((message , self .plural (1 )), missing )
444
475
if tmsg is not missing :
476
+ if t_values is not None :
477
+ return tmsg .format (** t_values )
445
478
return tmsg
446
479
if self ._fallback :
447
- return self ._fallback .gettext (message )
480
+ return self ._fallback .gettext (orig_message )
481
+ if t_values is not None :
482
+ return message .format (** t_values )
448
483
return message
449
484
450
485
def ngettext (self , msgid1 , msgid2 , n ):
486
+ orig_msgid1 = msgid1
487
+ orig_msgid2 = msgid2
488
+ msgid1_is_template = isinstance (msgid1 , Template )
489
+ msgid2_is_template = isinstance (msgid2 , Template )
490
+ t_values1 = t_values2 = None
491
+ if msgid1_is_template and msgid2_is_template :
492
+ msgid1 , t_values1 = _template_to_format (msgid1 )
493
+ msgid2 , t_values2 = _template_to_format (msgid2 )
494
+ elif msgid1_is_template or msgid2_is_template :
495
+ raise TypeError ('msgids cannot mix strings and t-strings' )
496
+ plural = self .plural (n )
497
+ t_values = t_values2 if plural else t_values1
451
498
try :
452
- tmsg = self ._catalog [(msgid1 , self . plural ( n ) )]
499
+ tmsg = self ._catalog [(msgid1 , plural )]
453
500
except KeyError :
454
501
if self ._fallback :
455
- return self ._fallback .ngettext (msgid1 , msgid2 , n )
502
+ return self ._fallback .ngettext (orig_msgid1 , orig_msgid2 , n )
456
503
if n == 1 :
457
- tmsg = msgid1
504
+ if t_values1 is not None :
505
+ return msgid1 .format (** t_values1 )
506
+ return msgid1
458
507
else :
459
- tmsg = msgid2
508
+ if t_values2 is not None :
509
+ return msgid2 .format (** t_values2 )
510
+ return msgid2
511
+ if t_values is not None :
512
+ return tmsg .format (** t_values )
460
513
return tmsg
461
514
462
515
def pgettext (self , context , message ):
516
+ orig_message = message
517
+ t_values = None
518
+ if isinstance (message , Template ):
519
+ message , t_values = _template_to_format (message )
463
520
ctxt_msg_id = self .CONTEXT % (context , message )
464
521
missing = object ()
465
522
tmsg = self ._catalog .get (ctxt_msg_id , missing )
466
523
if tmsg is missing :
467
524
tmsg = self ._catalog .get ((ctxt_msg_id , self .plural (1 )), missing )
468
525
if tmsg is not missing :
526
+ if t_values is not None :
527
+ return tmsg .format (** t_values )
469
528
return tmsg
470
529
if self ._fallback :
471
- return self ._fallback .pgettext (context , message )
530
+ return self ._fallback .pgettext (context , orig_message )
531
+ if t_values is not None :
532
+ return message .format (** t_values )
472
533
return message
473
534
474
535
def npgettext (self , context , msgid1 , msgid2 , n ):
536
+ orig_msgid1 = msgid1
537
+ orig_msgid2 = msgid2
538
+ msgid1_is_template = isinstance (msgid1 , Template )
539
+ msgid2_is_template = isinstance (msgid2 , Template )
540
+ t_values1 = t_values2 = None
541
+ if msgid1_is_template and msgid2_is_template :
542
+ msgid1 , t_values1 = _template_to_format (msgid1 )
543
+ msgid2 , t_values2 = _template_to_format (msgid2 )
544
+ elif msgid1_is_template or msgid2_is_template :
545
+ raise TypeError ('msgids cannot mix strings and t-strings' )
546
+ plural = self .plural (n )
547
+ t_values = t_values2 if plural else t_values1
475
548
ctxt_msg_id = self .CONTEXT % (context , msgid1 )
476
549
try :
477
- tmsg = self ._catalog [ctxt_msg_id , self . plural ( n ) ]
550
+ tmsg = self ._catalog [ctxt_msg_id , plural ]
478
551
except KeyError :
479
552
if self ._fallback :
480
- return self ._fallback .npgettext (context , msgid1 , msgid2 , n )
553
+ return self ._fallback .npgettext (
554
+ context , orig_msgid1 , orig_msgid2 , n
555
+ )
481
556
if n == 1 :
482
- tmsg = msgid1
557
+ if t_values1 is not None :
558
+ return msgid1 .format (** t_values1 )
559
+ return msgid1
483
560
else :
484
- tmsg = msgid2
561
+ if t_values2 is not None :
562
+ return msgid2 .format (** t_values2 )
563
+ return msgid2
564
+ if t_values is not None :
565
+ return tmsg .format (** t_values )
485
566
return tmsg
486
567
487
568
@@ -749,3 +830,51 @@ def _template_node_to_format(node: ast.TemplateStr) -> str:
749
830
interpolation_format_names [name ] = expr
750
831
parts .append (f'{{{ name } }}' )
751
832
return '' .join (parts )
833
+
834
+
835
+ def _template_to_format (template : Template ) -> tuple [str , dict [str , Any ]]:
836
+ """Convert a template to a format string and its value dict.
837
+
838
+ This takes a :class:`~string.templatelib.Template`, and converts all the
839
+ interpolations with format string placeholders derived from the original
840
+ expression.
841
+
842
+ This fails with a :exc:`_NameTooComplexError` in case the expression is
843
+ not suitable for conversion.
844
+ """
845
+ parts = []
846
+ interpolation_format_names = {}
847
+ values = {}
848
+ for item in template :
849
+ match item :
850
+ case str () as s :
851
+ parts .append (s .replace ('{' , '{{' ).replace ('}' , '}}' ))
852
+ case Interpolation (value , expr , conversion , format_spec ):
853
+ value = convert (value , conversion )
854
+ value = format (value , format_spec )
855
+ name = _expr_to_format_field_name (expr )
856
+ if (
857
+ existing_expr := interpolation_format_names .get (name )
858
+ ) and existing_expr != expr :
859
+ raise _NameTooComplexError (
860
+ f'Interpolations of { existing_expr } and { expr } cannot '
861
+ 'be mixed in the same gettext call; assign one of '
862
+ 'them to a variable and use that instead'
863
+ )
864
+ interpolation_format_names [name ] = expr
865
+ values [name ] = value
866
+ parts .append (f'{{{ name } }}' )
867
+ return '' .join (parts ), values
868
+
869
+
870
+ @cache
871
+ def _expr_to_format_field_name (expr : str ) -> str :
872
+ # handle simple cases w/o the overhead of dealing with an ast
873
+ if expr .isidentifier ():
874
+ return expr
875
+ if all (x .isidentifier () for x in expr .split ('.' )):
876
+ return '__' .join (expr .split ('.' ))
877
+ expr_node = ast .parse (expr , mode = 'eval' ).body
878
+ visitor = _ExtractNamesVisitor ()
879
+ visitor .visit (expr_node )
880
+ return visitor .name
0 commit comments