[Zope-Checkins] CVS: Zope3/lib/python/Zope/TAL - TALInterpreter.py:1.63.10.3.4.6

Barry Warsaw barry@wooz.org
Mon, 10 Jun 2002 15:49:09 -0400


Update of /cvs-repository/Zope3/lib/python/Zope/TAL
In directory cvs.zope.org:/tmp/cvs-serv11158/lib/python/Zope/TAL

Modified Files:
      Tag: fdrake-tal-i18n-branch
	TALInterpreter.py 
Log Message:
Support for i18n:name, specifically:

TALInterpreter.__init__(): Added the i18nInterpolate flag which is
helpful for debugging (default is on, but reset by the driver's new -i
flag).

interpret(): Added an optional tmpstream argument.  When this is set
to None (default), all output goes to the normal stream.  When given,
it must be an object that supports the .write() method for output
streams (i.e. StringIO's work great :), and all output for the
interpreter program go to this temporary stream instead.  This is
really handy for for stuff like do_i18nVariable() where we want to
capture the contents of a tag with i18n:name but no tal:replace.

We also refactor do_insertTranslation() to use this feature, since
it's also quite handy for implicit message ids.

do_i18nVariable(): Handle the i18nVariable opcode.  Depending on the
arguments, we do different tasks.  If the value of the interpolation
variable is implicitly the contents of the tag, then stuff[1] will be
a list -- i.e. the mini sub-program that defines the tag contents.
Otherwise, the tag also has a tal:replace attribute, in which case
it's the evaluation of that attribute that defines where the
interpolation value comes from.

do_insertTranslation(): We can use the new interpret() method
tmpstream argument.  Also, I'm backing out of the cgi.escape() of the
translated message.  It causes havoc for implicit substitution values,
but might end up be necessary nonetheless.  See the XXX comment in the
code.

translate(): This now takes a second argument, which is the dictionary
of interpolation values.  We're still not hooked up to the real
translation service (although this is coming), so for now, I've
implemented a transformation from $-strings to %-strings so we can
piggyback on Python's interpolation mechanism.


=== Zope3/lib/python/Zope/TAL/TALInterpreter.py 1.63.10.3.4.5 => 1.63.10.3.4.6 ===
 import sys
 import getopt
+import re
+from types import ListType
 
 from cgi import escape
 
@@ -85,7 +87,7 @@
 
     def __init__(self, program, macros, engine, stream=None,
                  debug=0, wrap=60, metal=1, tal=1, showtal=-1,
-                 strictinsert=1, stackLimit=100):
+                 strictinsert=1, stackLimit=100, i18nInterpolate=1):
         self.program = program
         self.macros = macros
         self.engine = engine
@@ -115,6 +117,8 @@
         self.col = 0
         self.level = 0
         self.scopeLevel = 0
+        self.i18nStack = []
+        self.i18nInterpolate = i18nInterpolate
         self.i18nContext = TranslationContext(domain="default")
 
     def saveState(self):
@@ -175,10 +179,15 @@
 
     bytecode_handlers = {}
 
-    def interpret(self, program):
+    def interpret(self, program, tmpstream=None):
         oldlevel = self.level
         self.level = oldlevel + 1
         handlers = self.dispatch
+        if tmpstream:
+            ostream = self.stream
+            owrite = self._stream_write
+            self.stream = mfp = tmpstream
+            self._stream_write = tmpstream.write
         try:
             if self.debug:
                 for (opcode, args) in program:
@@ -193,6 +202,9 @@
                     handlers[opcode](self, args)
         finally:
             self.level = oldlevel
+            if tmpstream:
+                self.stream = ostream
+                self._stream_write = owrite
 
     def do_version(self, version):
         assert version == TAL_VERSION
@@ -454,33 +466,56 @@
             self.col = len(s) - (i + 1)
     bytecode_handlers["insertText"] = do_insertText
 
+    def do_i18nVariable(self, stuff):
+        varname = stuff[0]
+        if isinstance(stuff[1], ListType):
+            # The value is implicitly the contents of this tag, so we have to
+            # evaluate the mini-program
+            state = self.saveState()
+            try:
+                tmpstream = StringIO()
+                self.interpret(stuff[1], tmpstream)
+                value = tmpstream.getvalue()
+            finally:
+                self.restoreState(state)
+        else:
+            # Evaluate the value to be associated with the variable in the
+            # i18n interpolation dictionary.
+            value = self.engine.evaluate(stuff[1])
+        i18ndict, srepr = self.i18nStack[-1]
+        i18ndict[varname] = value
+        placeholder = '${%s}' % varname
+        srepr.append(placeholder)
+        self._stream_write(placeholder)
+    bytecode_handlers['i18nVariable'] = do_i18nVariable
+
     def do_insertTranslation(self, stuff):
+        i18ndict = {}
+        srepr = []
+        self.i18nStack.append((i18ndict, srepr))
         msgid = stuff[0]
         if msgid == '':
-            # The content is the implicit message id.  The easiest way to get
-            # the interpreted implicit message id is to interpret the
-            # subnodes, but we do /not/ want that going to the output stream.
-            # Fake our own into a StringIO, but be sure to always restore the
-            # original, even in the face of exceptions.
-            #
-            # XXX Do we really want to do recursive processing on the implicit
-            # message id?
-            ostream = self.stream
-            owrite = self._stream_write
-            self.stream = mfp = StringIO()
-            self._stream_write = mfp.write
-            try:
-                self.interpret(stuff[1])
-                msgid = mfp.getvalue()
-            finally:
-                self.stream = ostream
-                self._stream_write = owrite
+            # The content is the implicit message id.  Use a temporary stream
+            # to capture the interpretation of the subnodes, which should
+            # /not/ go to the output stream.
+            tmpstream = StringIO()
+            self.interpret(stuff[1], tmpstream)
+            msgid = tmpstream.getvalue()
             # Now we need to normalize the whitespace in the implicit message
             # id by stripping leading and trailing whitespace, and folding all
             # internal whitespace to a single space.
             msgid = ' '.join(msgid.split())
-        xlated_msgid = self.translate(msgid)
-        s = escape(xlated_msgid)
+        self.i18nStack.pop()
+        xlated_msgid = self.translate(msgid, i18ndict)
+        # XXX I can't decide whether we want to cgi escape the translated
+        # string or not.  OT1H not doing this could introduce a cross-site
+        # scripting vector by allowing translators to sneak JavaScript into
+        # translations.  OTOH, for implicit interpolation values, we don't
+        # want to escape stuff like ${name} <= "<b>Timmy</b>".
+        #s = escape(xlated_msgid)
+        s = xlated_msgid
+        # If there are i18n variables to interpolate into this string, better
+        # do it now.
         self._stream_write(s)
     bytecode_handlers['insertTranslation'] = do_insertTranslation
 
@@ -535,9 +570,34 @@
             self.interpret(block)
     bytecode_handlers["loop"] = do_loop
 
-    def translate(self, msgid):
-        # XXX: need to fill this in with TranslationService calls
-        return msgid.upper()
+    def translate(self, msgid, i18ndict=None):
+        # XXX: need to fill this in with TranslationService calls.  For now,
+        # we'll just do simple interpolation based on a $-strings to %-strings
+        # algorithm in Mailman.
+        if not self.i18nInterpolate:
+            return msgid
+        s = msgid.replace('%', '%%')
+        parts = re.split(r'(\${2})|\$([_a-z]\w*)|\${([_a-z]\w*)}',
+                         s, re.IGNORECASE)
+        if len(parts) == 1:
+            # There are no ${name} placeholders in the source string.  We use
+            # msgid here so we don't end up with doubled %'s.
+            return msgid.upper()
+        # Transform ${name} into %(name)s so we can use Python's built-in
+        # string interpolation feature.
+        xlateParts = []
+        for i in range(1, len(parts), 4):
+            if parts[i] is not None:
+                p = parts[i] = '$'
+            elif parts[i+1] is not None:
+                p = parts[i+1] = '%(' + parts[i+1] + ')s'
+            else:
+                p = parts[i+2] = '%(' + parts[i+2] + ')s'
+            xlateParts.append(p)
+            xlateParts.append(parts[i+3].upper())
+        if i18ndict is None:
+            i18ndict = {}
+        return ''.join(xlateParts) % i18ndict
 
     def do_rawtextColumn(self, (s, col)):
         self._stream_write(s)