4747import operator
4848import os
4949import sys
50+ from functools import cache
51+ from string .templatelib import Interpolation , Template , convert
52+ from typing import Any
5053
5154
5255__all__ = ['NullTranslations' , 'GNUTranslations' , 'Catalog' ,
@@ -291,11 +294,23 @@ def add_fallback(self, fallback):
291294 def gettext (self , message ):
292295 if self ._fallback :
293296 return self ._fallback .gettext (message )
297+ if isinstance (message , Template ):
298+ message , values = _template_to_format (message )
299+ return message .format (** values )
294300 return message
295301
296302 def ngettext (self , msgid1 , msgid2 , n ):
297303 if self ._fallback :
298304 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' )
299314 n = _as_int2 (n )
300315 if n == 1 :
301316 return msgid1
@@ -305,11 +320,23 @@ def ngettext(self, msgid1, msgid2, n):
305320 def pgettext (self , context , message ):
306321 if self ._fallback :
307322 return self ._fallback .pgettext (context , message )
323+ if isinstance (message , Template ):
324+ message , values = _template_to_format (message )
325+ return message .format (** values )
308326 return message
309327
310328 def npgettext (self , context , msgid1 , msgid2 , n ):
311329 if self ._fallback :
312330 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' )
313340 n = _as_int2 (n )
314341 if n == 1 :
315342 return msgid1
@@ -438,50 +465,104 @@ def _parse(self, fp):
438465
439466 def gettext (self , message ):
440467 missing = object ()
468+ orig_message = message
469+ t_values = None
470+ if isinstance (message , Template ):
471+ message , t_values = _template_to_format (message )
441472 tmsg = self ._catalog .get (message , missing )
442473 if tmsg is missing :
443474 tmsg = self ._catalog .get ((message , self .plural (1 )), missing )
444475 if tmsg is not missing :
476+ if t_values is not None :
477+ return tmsg .format (** t_values )
445478 return tmsg
446479 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 )
448483 return message
449484
450485 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
451498 try :
452- tmsg = self ._catalog [(msgid1 , self . plural ( n ) )]
499+ tmsg = self ._catalog [(msgid1 , plural )]
453500 except KeyError :
454501 if self ._fallback :
455- return self ._fallback .ngettext (msgid1 , msgid2 , n )
502+ return self ._fallback .ngettext (orig_msgid1 , orig_msgid2 , n )
456503 if n == 1 :
457- tmsg = msgid1
504+ if t_values1 is not None :
505+ return msgid1 .format (** t_values1 )
506+ return msgid1
458507 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 )
460513 return tmsg
461514
462515 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 )
463520 ctxt_msg_id = self .CONTEXT % (context , message )
464521 missing = object ()
465522 tmsg = self ._catalog .get (ctxt_msg_id , missing )
466523 if tmsg is missing :
467524 tmsg = self ._catalog .get ((ctxt_msg_id , self .plural (1 )), missing )
468525 if tmsg is not missing :
526+ if t_values is not None :
527+ return tmsg .format (** t_values )
469528 return tmsg
470529 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 )
472533 return message
473534
474535 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
475548 ctxt_msg_id = self .CONTEXT % (context , msgid1 )
476549 try :
477- tmsg = self ._catalog [ctxt_msg_id , self . plural ( n ) ]
550+ tmsg = self ._catalog [ctxt_msg_id , plural ]
478551 except KeyError :
479552 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+ )
481556 if n == 1 :
482- tmsg = msgid1
557+ if t_values1 is not None :
558+ return msgid1 .format (** t_values1 )
559+ return msgid1
483560 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 )
485566 return tmsg
486567
487568
@@ -749,3 +830,51 @@ def _template_node_to_format(node: ast.TemplateStr) -> str:
749830 interpolation_format_names [name ] = expr
750831 parts .append (f'{{{ name } }}' )
751832 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