[Zope-Checkins] CVS: Zope/lib/python/TAL - ITALES.py:1.1.8.1 TranslationContext.py:1.1.10.1 DummyEngine.py:1.32.8.1 HTMLTALParser.py:1.34.4.1 TALDefs.py:1.28.8.1 TALGenerator.py:1.55.8.1 TALInterpreter.py:1.69.6.1.2.1 TALParser.py:1.19.8.1 XMLParser.py:1.9.8.1 driver.py:1.28.8.1 runtest.py:1.22.8.1

Florent Guillaume fg@nuxeo.com
Sun, 15 Sep 2002 18:43:35 -0400


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

Modified Files:
      Tag: Zope-2_6-i18n-branch
	DummyEngine.py HTMLTALParser.py TALDefs.py TALGenerator.py 
	TALInterpreter.py TALParser.py XMLParser.py driver.py 
	runtest.py 
Added Files:
      Tag: Zope-2_6-i18n-branch
	ITALES.py TranslationContext.py 
Log Message:
Zope-2_6-i18n-branch is a branch off Zope-2_6-branch to integrate TAL
i18n from Zope27. This branch is based at tag Zope-2_6-i18n-base.

This first checkin is simply a direct copy of TAL and PageTemplates
tagged at baseline-for-Zope-2_6-i18n-branch in the Zope27 module, and
adapted to not get ImportErrors on interfaces. All tests pass (except
the usual failure due to expat bugs).



=== Added File Zope/lib/python/TAL/ITALES.py ===
"""Interface that a TALES engine provides to the METAL/TAL implementation."""

try:
    from Interface import Interface
    from Interface.Attribute import Attribute
except:
    # Before 2.7
    class Interface: pass
    def Attribute(*args): pass


class ITALESCompiler(Interface):
    """Compile-time interface provided by a TALES implementation.

    The TAL compiler needs an instance of this interface to support
    compilation of TALES expressions embedded in documents containing
    TAL and METAL constructs.
    """

    def getCompilerError():
        """Return the exception class raised for compilation errors.
        """

    def compile(expression):
        """Return a compiled form of 'expression' for later evaluation.

        'expression' is the source text of the expression.

        The return value may be passed to the various evaluate*()
        methods of the ITALESEngine interface.  No compatibility is
        required for the values of the compiled expression between
        different ITALESEngine implementations.
        """


class ITALESEngine(Interface):
    """Render-time interface provided by a TALES implementation.

    The TAL interpreter uses this interface to TALES to support
    evaluation of the compiled expressions returned by
    ITALESCompiler.compile().
    """

    def getDefault():
        """Return the value of the 'default' TALES expression.

        Checking a value for a match with 'default' should be done
        using the 'is' operator in Python.
        """

    def setPosition((lineno, offset)):
        """Inform the engine of the current position in the source file.

        This is used to allow the evaluation engine to report
        execution errors so that site developers can more easily
        locate the offending expression.
        """

    def setSourceFile(filename):
        """Inform the engine of the name of the current source file.

        This is used to allow the evaluation engine to report
        execution errors so that site developers can more easily
        locate the offending expression.
        """

    def beginScope():
        """Push a new scope onto the stack of open scopes.
        """

    def endScope():
        """Pop one scope from the stack of open scopes.
        """

    def evaluate(compiled_expression):
        """Evaluate an arbitrary expression.

        No constraints are imposed on the return value.
        """

    def evaluateBoolean(compiled_expression):
        """Evaluate an expression that must return a Boolean value.
        """

    def evaluateMacro(compiled_expression):
        """Evaluate an expression that must return a macro program.
        """

    def evaluateStructure(compiled_expression):
        """Evaluate an expression that must return a structured
        document fragment.

        The result of evaluating 'compiled_expression' must be a
        string containing a parsable HTML or XML fragment.  Any TAL
        markup cnotained in the result string will be interpreted.
        """

    def evaluateText(compiled_expression):
        """Evaluate an expression that must return text.

        The returned text should be suitable for direct inclusion in
        the output: any HTML or XML escaping or quoting is the
        responsibility of the expression itself.
        """

    def evaluateValue(compiled_expression):
        """Evaluate an arbitrary expression.

        No constraints are imposed on the return value.
        """

    def createErrorInfo(exception, (lineno, offset)):
        """Returns an ITALESErrorInfo object.

        The returned object is used to provide information about the
        error condition for the on-error handler.
        """

    def setGlobal(name, value):
        """Set a global variable.

        The variable will be named 'name' and have the value 'value'.
        """

    def setLocal(name, value):
        """Set a local variable in the current scope.

        The variable will be named 'name' and have the value 'value'.
        """

    def setRepeat(name, compiled_expression):
        """
        """

    def translate(domain, msgid, mapping):
        """
        See ITranslationService.translate()
        """


class ITALESErrorInfo(Interface):
    
    type = Attribute("type",
                     "The exception class.")

    value = Attribute("value",
                      "The exception instance.")

    lineno = Attribute("lineno",
                       "The line number the error occurred on in the source.")

    offset = Attribute("offset",
                       "The character offset at which the error occurred.")


=== Added File Zope/lib/python/TAL/TranslationContext.py ===
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Translation context object for the TALInterpreter's I18N support.

The translation context provides a container for the information
needed to perform translation of a marked string from a page template.

$Id: TranslationContext.py,v 1.1.10.1 2002/09/15 22:43:34 efge Exp $
"""

DEFAULT_DOMAIN = "default"

class TranslationContext:
    """Information about the I18N settings of a TAL processor."""

    def __init__(self, parent=None, domain=None, target=None, source=None):
        if parent:
            if not domain:
                domain = parent.domain
            if not target:
                target = parent.target
            if not source:
                source = parent.source
        elif domain is None:
            domain = DEFAULT_DOMAIN

        self.parent = parent
        self.domain = domain
        self.target = target
        self.source = source


=== Zope/lib/python/TAL/DummyEngine.py 1.32 => 1.32.8.1 ===
--- Zope/lib/python/TAL/DummyEngine.py:1.32	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/DummyEngine.py	Sun Sep 15 18:43:34 2002
@@ -8,7 +8,7 @@
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
+# FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
 """
@@ -18,13 +18,19 @@
 import re
 import sys
 
-import driver
-
 from TALDefs import NAME_RE, TALESError, ErrorInfo
+from ITALES import ITALESCompiler, ITALESEngine
+try:
+    from Zope.I18n.ITranslationService import ITranslationService
+    from Zope.I18n.IDomain import IDomain
+except ImportError:
+    # Before 2.7
+    class ITranslationService: pass
+    class IDomain: pass
 
-class Default:
+class _Default:
     pass
-Default = Default()
+Default = _Default()
 
 name_match = re.compile(r"(?s)(%s):(.*)\Z" % NAME_RE).match
 
@@ -36,6 +42,8 @@
     position = None
     source_file = None
 
+    __implements__ = ITALESCompiler, ITALESEngine
+
     def __init__(self, macros=None):
         if macros is None:
             macros = {}
@@ -43,6 +51,7 @@
         dict = {'nothing': None, 'default': Default}
         self.locals = self.globals = dict
         self.stack = [dict]
+        self.translationService = DummyTranslationService()
 
     def getCompilerError(self):
         return CompilerError
@@ -90,13 +99,7 @@
         if type in ("string", "str"):
             return expr
         if type in ("path", "var", "global", "local"):
-            expr = expr.strip()
-            if self.locals.has_key(expr):
-                return self.locals[expr]
-            elif self.globals.has_key(expr):
-                return self.globals[expr]
-            else:
-                raise TALESError("unknown variable: %s" % `expr`)
+            return self.evaluatePathOrVar(expr)
         if type == "not":
             return not self.evaluate(expr)
         if type == "exists":
@@ -116,6 +119,15 @@
             return '%s (%s,%s)' % (self.source_file, lineno, offset)
         raise TALESError("unrecognized expression: " + `expression`)
 
+    def evaluatePathOrVar(self, expr):
+        expr = expr.strip()
+        if self.locals.has_key(expr):
+            return self.locals[expr]
+        elif self.globals.has_key(expr):
+            return self.globals[expr]
+        else:
+            raise TALESError("unknown variable: %s" % `expr`)
+
     def evaluateValue(self, expr):
         return self.evaluate(expr)
 
@@ -146,6 +158,7 @@
             macro = self.macros[localName]
         else:
             # External macro
+            import driver
             program, macros = driver.compilefile(file)
             macro = macros.get(localName)
             if not macro:
@@ -157,6 +170,7 @@
         file, localName = self.findMacroFile(macroName)
         if not file:
             return file, localName
+        import driver
         doc = driver.parsefile(file)
         return doc, localName
 
@@ -183,6 +197,10 @@
     def getDefault(self):
         return Default
 
+    def translate(self, domain, msgid, mapping):
+        return self.translationService.translate(domain, msgid, mapping)
+    
+
 class Iterator:
 
     def __init__(self, name, seq, engine):
@@ -200,3 +218,31 @@
         self.nextIndex = i+1
         self.engine.setLocal(self.name, item)
         return 1
+
+class DummyDomain:
+    __implements__ = IDomain
+
+    def translate(self, msgid, mapping=None, context=None,
+                  target_language=None):
+        # This is a fake translation service which simply uppercases non
+        # ${name} placeholder text in the message id.
+        #
+        # First, transform a string with ${name} placeholders into a list of
+        # substrings.  Then upcase everything but the placeholders, then glue
+        # things back together.
+        def repl(m, mapping=mapping):
+            return mapping[m.group(m.lastindex).lower()]
+        cre = re.compile(r'\$(?:([_A-Z]\w*)|\{([_A-Z]\w*)\})')
+        return cre.sub(repl, msgid.upper())
+
+class DummyTranslationService:
+    __implements__ = ITranslationService
+
+    def translate(self, domain, msgid, mapping=None, context=None,
+                  target_language=None):
+        # Ignore domain
+        return self.getDomain(domain).translate(msgid, mapping, context,
+                                                target_language)
+
+    def getDomain(self, domain):
+        return DummyDomain()


=== Zope/lib/python/TAL/HTMLTALParser.py 1.34 => 1.34.4.1 ===
--- Zope/lib/python/TAL/HTMLTALParser.py:1.34	Tue Aug 27 14:31:17 2002
+++ Zope/lib/python/TAL/HTMLTALParser.py	Sun Sep 15 18:43:34 2002
@@ -8,7 +8,7 @@
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
+# FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
 """
@@ -18,8 +18,9 @@
 import sys
 
 from TALGenerator import TALGenerator
-from TALDefs import ZOPE_METAL_NS, ZOPE_TAL_NS, METALError, TALError
 from HTMLParser import HTMLParser, HTMLParseError
+from TALDefs import \
+     ZOPE_METAL_NS, ZOPE_TAL_NS, ZOPE_I18N_NS, METALError, TALError
 
 BOOLEAN_HTML_ATTRS = [
     # List of Boolean attributes in HTML that may be given in
@@ -106,13 +107,20 @@
         self.gen = gen
         self.tagstack = []
         self.nsstack = []
-        self.nsdict = {'tal': ZOPE_TAL_NS, 'metal': ZOPE_METAL_NS}
+        self.nsdict = {'tal': ZOPE_TAL_NS,
+                       'metal': ZOPE_METAL_NS,
+                       'i18n': ZOPE_I18N_NS,
+                       }
 
     def parseFile(self, file):
         f = open(file)
         data = f.read()
         f.close()
-        self.parseString(data)
+        try:
+            self.parseString(data)
+        except TALError, e:
+            e.setFile(file)
+            raise
 
     def parseString(self, data):
         self.feed(data)
@@ -132,9 +140,10 @@
     def handle_starttag(self, tag, attrs):
         self.close_para_tags(tag)
         self.scan_xmlns(attrs)
-        tag, attrlist, taldict, metaldict = self.process_ns(tag, attrs)
+        tag, attrlist, taldict, metaldict, i18ndict \
+             = self.process_ns(tag, attrs)
         self.tagstack.append(tag)
-        self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
+        self.gen.emitStartElement(tag, attrlist, taldict, metaldict, i18ndict,
                                   self.getpos())
         if tag in EMPTY_HTML_TAGS:
             self.implied_endtag(tag, -1)
@@ -142,14 +151,15 @@
     def handle_startendtag(self, tag, attrs):
         self.close_para_tags(tag)
         self.scan_xmlns(attrs)
-        tag, attrlist, taldict, metaldict = self.process_ns(tag, attrs)
+        tag, attrlist, taldict, metaldict, i18ndict \
+             = self.process_ns(tag, attrs)
         if taldict.get("content"):
             self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
-                                      self.getpos())
+                                      i18ndict, self.getpos())
             self.gen.emitEndElement(tag, implied=-1)
         else:
             self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
-                                      self.getpos(), isend=1)
+                                      i18ndict, self.getpos(), isend=1)
         self.pop_xmlns()
 
     def handle_endtag(self, tag):
@@ -252,7 +262,7 @@
             prefix, suffix = name.split(':', 1)
             if prefix == 'xmlns':
                 nsuri = self.nsdict.get(suffix)
-                if nsuri in (ZOPE_TAL_NS, ZOPE_METAL_NS):
+                if nsuri in (ZOPE_TAL_NS, ZOPE_METAL_NS, ZOPE_I18N_NS):
                     return name, name, prefix
             else:
                 nsuri = self.nsdict.get(prefix)
@@ -260,20 +270,20 @@
                     return name, suffix, 'tal'
                 elif nsuri == ZOPE_METAL_NS:
                     return name, suffix,  'metal'
+                elif nsuri == ZOPE_I18N_NS:
+                    return name, suffix, 'i18n'
         return name, name, 0
 
     def process_ns(self, name, attrs):
         attrlist = []
         taldict = {}
         metaldict = {}
+        i18ndict = {}
         name, namebase, namens = self.fixname(name)
         for item in attrs:
             key, value = item
             key, keybase, keyns = self.fixname(key)
-            if ':' in key and not keyns:
-                ns = 0
-            else:
-                ns = keyns or namens # default to tag namespace
+            ns = keyns or namens # default to tag namespace
             if ns and ns != 'unknown':
                 item = (key, value, ns)
             if ns == 'tal':
@@ -286,7 +296,12 @@
                     raise METALError("duplicate METAL attribute " +
                                      `keybase`, self.getpos())
                 metaldict[keybase] = value
+            elif ns == 'i18n':
+                if i18ndict.has_key(keybase):
+                    raise I18NError("duplicate i18n attribute " +
+                                    `keybase`, self.getpos())
+                i18ndict[keybase] = value
             attrlist.append(item)
         if namens in ('metal', 'tal'):
             taldict['tal tag'] = namens
-        return name, attrlist, taldict, metaldict
+        return name, attrlist, taldict, metaldict, i18ndict


=== Zope/lib/python/TAL/TALDefs.py 1.28 => 1.28.8.1 ===
--- Zope/lib/python/TAL/TALDefs.py:1.28	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/TALDefs.py	Sun Sep 15 18:43:34 2002
@@ -8,7 +8,7 @@
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
+# FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
 """
@@ -17,13 +17,16 @@
 
 from types import ListType, TupleType
 
-TAL_VERSION = "1.3.2"
+from ITALES import ITALESErrorInfo
+
+TAL_VERSION = "1.4"
 
 XML_NS = "http://www.w3.org/XML/1998/namespace" # URI for XML namespace
 XMLNS_NS = "http://www.w3.org/2000/xmlns/" # URI for XML NS declarations
 
 ZOPE_TAL_NS = "http://xml.zope.org/namespaces/tal"
 ZOPE_METAL_NS = "http://xml.zope.org/namespaces/metal"
+ZOPE_I18N_NS = "http://xml.zope.org/namespaces/i18n"
 
 NAME_RE = "[a-zA-Z_][a-zA-Z0-9_]*"
 
@@ -32,7 +35,6 @@
     "use-macro",
     "define-slot",
     "fill-slot",
-    "slot",
     ]
 
 KNOWN_TAL_ATTRIBUTES = [
@@ -47,6 +49,16 @@
     "tal tag",
     ]
 
+KNOWN_I18N_ATTRIBUTES = [
+    "translate",
+    "domain",
+    "target",
+    "source",
+    "attributes",
+    "data",
+    "name",
+    ]
+
 class TALError(Exception):
 
     def __init__(self, msg, position=(None, None)):
@@ -54,6 +66,10 @@
         self.msg = msg
         self.lineno = position[0]
         self.offset = position[1]
+        self.filename = None
+
+    def setFile(self, filename):
+        self.filename = filename
 
     def __str__(self):
         result = self.msg
@@ -61,6 +77,8 @@
             result = result + ", at line %d" % self.lineno
         if self.offset is not None:
             result = result + ", column %d" % (self.offset + 1)
+        if self.filename is not None:
+            result = result + ', in file %s' % self.filename
         return result
 
 class METALError(TALError):
@@ -69,8 +87,13 @@
 class TALESError(TALError):
     pass
 
+class I18NError(TALError):
+    pass
+
 
 class ErrorInfo:
+
+    __implements__ = ITALESErrorInfo
 
     def __init__(self, err, position=(None, None)):
         if isinstance(err, Exception):


=== Zope/lib/python/TAL/TALGenerator.py 1.55 => 1.55.8.1 ===
--- Zope/lib/python/TAL/TALGenerator.py:1.55	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/TALGenerator.py	Sun Sep 15 18:43:34 2002
@@ -8,7 +8,7 @@
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
+# FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
 """
@@ -18,7 +18,16 @@
 import re
 import cgi
 
-from TALDefs import *
+import TALDefs
+
+from TALDefs import NAME_RE, TAL_VERSION
+from TALDefs import I18NError, METALError, TALError
+from TALDefs import parseSubstitution
+from TranslationContext import TranslationContext, DEFAULT_DOMAIN
+
+I18N_REPLACE = 1
+I18N_CONTENT = 2
+I18N_EXPRESSION = 3
 
 class TALGenerator:
 
@@ -32,8 +41,15 @@
             expressionCompiler = DummyEngine()
         self.expressionCompiler = expressionCompiler
         self.CompilerError = expressionCompiler.getCompilerError()
+        # This holds the emitted opcodes representing the input
         self.program = []
+        # The program stack for when we need to do some sub-evaluation for an
+        # intermediate result.  E.g. in an i18n:name tag for which the
+        # contents describe the ${name} value.
         self.stack = []
+        # Another stack of postponed actions.  Elements on this stack are a
+        # dictionary; key/values contain useful information that
+        # emitEndElement needs to finish its calculations
         self.todoStack = []
         self.macros = {}
         self.slots = {}
@@ -44,6 +60,7 @@
         if source_file is not None:
             self.source_file = source_file
             self.emit("setSourceFile", source_file)
+        self.i18nContext = TranslationContext()
 
     def getCode(self):
         assert not self.stack
@@ -82,6 +99,12 @@
                 # instructions to be joined together.
                 output.append(self.optimizeArgsList(item))
                 continue
+            if opcode == 'noop':
+                # This is a spacer for end tags in the face of i18n:name
+                # attributes.  We can't let the optimizer collect immediately
+                # following end tags into the same rawtextOffset.
+                opcode = None
+                pass
             text = "".join(collect)
             if text:
                 i = text.rfind("\n")
@@ -102,9 +125,28 @@
         else:
             return item[0], tuple(item[1:])
 
-    actionIndex = {"replace":0, "insert":1, "metal":2, "tal":3, "xmlns":4,
-                   0: 0, 1: 1, 2: 2, 3: 3, 4: 4}
+    # These codes are used to indicate what sort of special actions
+    # are needed for each special attribute.  (Simple attributes don't
+    # get action codes.)
+    #
+    # The special actions (which are modal) are handled by
+    # TALInterpreter.attrAction() and .attrAction_tal().
+    #
+    # Each attribute is represented by a tuple:
+    #
+    # (name, value)                 -- a simple name/value pair, with
+    #                                  no special processing
+    #
+    # (name, value, action, *extra) -- attribute with special
+    #                                  processing needs, action is a
+    #                                  code that indicates which
+    #                                  branch to take, and *extra
+    #                                  contains additional,
+    #                                  action-specific information
+    #                                  needed by the processing
+    #
     def optimizeStartTag(self, collect, name, attrlist, end):
+        # return true if the tag can be converted to plain text
         if not attrlist:
             collect.append("<%s%s" % (name, end))
             return 1
@@ -115,18 +157,15 @@
             if len(item) > 2:
                 opt = 0
                 name, value, action = item[:3]
-                action = self.actionIndex[action]
                 attrlist[i] = (name, value, action) + item[3:]
             else:
                 if item[1] is None:
                     s = item[0]
                 else:
-                    s = "%s=%s" % (item[0], quote(item[1]))
+                    s = "%s=%s" % (item[0], TALDefs.quote(item[1]))
                 attrlist[i] = item[0], s
-            if item[1] is None:
-                new.append(" " + item[0])
-            else:
-                new.append(" %s=%s" % (item[0], quote(item[1])))
+                new.append(" " + s)
+        # if no non-optimizable attributes were found, convert to plain text
         if opt:
             new.append(end)
             collect.extend(new)
@@ -222,7 +261,7 @@
         self.emitRawText(cgi.escape(text))
 
     def emitDefines(self, defines):
-        for part in splitParts(defines):
+        for part in TALDefs.splitParts(defines):
             m = re.match(
                 r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part)
             if not m:
@@ -274,6 +313,49 @@
             assert key == "structure"
             self.emit("insertStructure", cexpr, attrDict, program)
 
+    def emitI18nVariable(self, varname, action, expression):
+        # Used for i18n:name attributes.  arg is extra information describing
+        # how the contents of the variable should get filled in, and it will
+        # either be a 1-tuple or a 2-tuple.  If arg[0] is None, then the
+        # i18n:name value is taken implicitly from the contents of the tag,
+        # e.g. "I live in <span i18n:name="country">the USA</span>".  In this
+        # case, arg[1] is the opcode sub-program describing the contents of
+        # the tag.
+        #
+        # When arg[0] is not None, it contains the tal expression used to
+        # calculate the contents of the variable, e.g.
+        # "I live in <span i18n:name="country"
+        #                  tal:replace="here/countryOfOrigin" />"
+        key = cexpr = None
+        program = self.popProgram()
+        if action == I18N_REPLACE:
+            # This is a tag with an i18n:name and a tal:replace (implicit or
+            # explicit).  Get rid of the first and last elements of the
+            # program, which are the start and end tag opcodes of the tag.
+            program = program[1:-1]
+        elif action == I18N_CONTENT:
+            # This is a tag with an i18n:name and a tal:content
+            # (explicit-only).  Keep the first and last elements of the
+            # program, so we keep the start and end tag output.
+            pass
+        else:
+            assert action == I18N_EXPRESSION
+            key, expr = parseSubstitution(expression)
+            cexpr = self.compileExpression(expr)
+        # XXX Would key be anything but 'text' or None?
+        assert key in ('text', None)
+        self.emit('i18nVariable', varname, program, cexpr)
+
+    def emitTranslation(self, msgid, i18ndata):
+        program = self.popProgram()
+        if i18ndata is None:
+            self.emit('insertTranslation', msgid, program)
+        else:
+            key, expr = parseSubstitution(i18ndata)
+            cexpr = self.compileExpression(expr)
+            assert key == 'text'
+            self.emit('insertTranslation', msgid, program, cexpr)
+
     def emitDefineMacro(self, macroName):
         program = self.popProgram()
         macroName = macroName.strip()
@@ -361,23 +443,30 @@
         return None
 
     def replaceAttrs(self, attrlist, repldict):
+        # Each entry in attrlist starts like (name, value).
+        # Result is (name, value, action, expr, xlat) if there is a
+        # tal:attributes entry for that attribute.  Additional attrs
+        # defined only by tal:attributes are added here.
+        #
+        # (name, value, action, expr, xlat)
         if not repldict:
             return attrlist
         newlist = []
         for item in attrlist:
             key = item[0]
             if repldict.has_key(key):
-                item = item[:2] + ("replace", repldict[key])
+                expr, xlat = repldict[key]
+                item = item[:2] + ("replace", expr, xlat)
                 del repldict[key]
             newlist.append(item)
-        for key, value in repldict.items(): # Add dynamic-only attributes
-            item = (key, None, "insert", value)
-            newlist.append(item)
+        # Add dynamic-only attributes
+        for key, (expr, xlat) in repldict.items():
+            newlist.append((key, None, "insert", expr, xlat))
         return newlist
 
-    def emitStartElement(self, name, attrlist, taldict, metaldict,
+    def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
                          position=(None, None), isend=0):
-        if not taldict and not metaldict:
+        if not taldict and not metaldict and not i18ndict:
             # Handle the simple, common case
             self.emitStartTag(name, attrlist, isend)
             self.todoPush({})
@@ -387,18 +476,24 @@
 
         self.position = position
         for key, value in taldict.items():
-            if key not in KNOWN_TAL_ATTRIBUTES:
+            if key not in TALDefs.KNOWN_TAL_ATTRIBUTES:
                 raise TALError("bad TAL attribute: " + `key`, position)
             if not (value or key == 'omit-tag'):
                 raise TALError("missing value for TAL attribute: " +
                                `key`, position)
         for key, value in metaldict.items():
-            if key not in KNOWN_METAL_ATTRIBUTES:
+            if key not in TALDefs.KNOWN_METAL_ATTRIBUTES:
                 raise METALError("bad METAL attribute: " + `key`,
-                position)
+                                 position)
             if not value:
                 raise TALError("missing value for METAL attribute: " +
                                `key`, position)
+        for key, value in i18ndict.items():
+            if key not in TALDefs.KNOWN_I18N_ATTRIBUTES:
+                raise I18NError("bad i18n attribute: " + `key`, position)
+            if not value and key in ("attributes", "data", "id"):
+                raise I18NError("missing value for i18n attribute: " +
+                                `key`, position)
         todo = {}
         defineMacro = metaldict.get("define-macro")
         useMacro = metaldict.get("use-macro")
@@ -413,13 +508,31 @@
         onError = taldict.get("on-error")
         omitTag = taldict.get("omit-tag")
         TALtag = taldict.get("tal tag")
+        i18nattrs = i18ndict.get("attributes")
+        # Preserve empty string if implicit msgids are used.  We'll generate
+        # code with the msgid='' and calculate the right implicit msgid during
+        # interpretation phase.
+        msgid = i18ndict.get("translate")
+        varname = i18ndict.get('name')
+        i18ndata = i18ndict.get('data')
+
+        if i18ndata and not msgid:
+            raise I18NError("i18n:data must be accompanied by i18n:translate",
+                            position)
+
         if len(metaldict) > 1 and (defineMacro or useMacro):
             raise METALError("define-macro and use-macro cannot be used "
                              "together or with define-slot or fill-slot",
                              position)
-        if content and replace:
-            raise TALError("content and replace are mutually exclusive",
-                           position)
+        if replace:
+            if content:
+                raise TALError(
+                    "tal:content and tal:replace are mutually exclusive",
+                    position)
+            if msgid is not None:
+                raise I18NError(
+                    "i18n:translate and tal:replace are mutually exclusive",
+                    position)
 
         repeatWhitespace = None
         if repeat:
@@ -437,8 +550,8 @@
                 self.inMacroUse = 0
         else:
             if fillSlot:
-                raise METALError, ("fill-slot must be within a use-macro",
-                                   position)
+                raise METALError("fill-slot must be within a use-macro",
+                                 position)
         if not self.inMacroUse:
             if defineMacro:
                 self.pushProgram()
@@ -455,13 +568,29 @@
                 self.inMacroUse = 1
             if defineSlot:
                 if not self.inMacroDef:
-                    raise METALError, (
+                    raise METALError(
                         "define-slot must be within a define-macro",
                         position)
                 self.pushProgram()
                 todo["defineSlot"] = defineSlot
 
-        if taldict:
+        if defineSlot or i18ndict:
+            
+            domain = i18ndict.get("domain") or self.i18nContext.domain
+            source = i18ndict.get("source") or self.i18nContext.source
+            target = i18ndict.get("target") or self.i18nContext.target
+            if (  domain != DEFAULT_DOMAIN
+                  or source is not None
+                  or target is not None):
+                self.i18nContext = TranslationContext(self.i18nContext,
+                                                      domain=domain,
+                                                      source=source,
+                                                      target=target)
+                self.emit("beginI18nContext",
+                          {"domain": domain, "source": source,
+                           "target": target})
+                todo["i18ncontext"] = 1
+        if taldict or i18ndict:
             dict = {}
             for item in attrlist:
                 key, value = item[:2]
@@ -487,16 +616,43 @@
         if content:
             todo["content"] = content
         if replace:
-            todo["replace"] = replace
+            # tal:replace w/ i18n:name has slightly different semantics.  What
+            # we're actually replacing then is the contents of the ${name}
+            # placeholder.
+            if varname:
+                todo['i18nvar'] = (varname, replace)
+            else:
+                todo["replace"] = replace
             self.pushProgram()
+        # i18n:name w/o tal:replace uses the content as the interpolation
+        # dictionary values
+        elif varname:
+            todo['i18nvar'] = (varname, None)
+            self.pushProgram()
+        if msgid is not None:
+            todo['msgid'] = msgid
+        if i18ndata:
+            todo['i18ndata'] = i18ndata
         optTag = omitTag is not None or TALtag
         if optTag:
             todo["optional tag"] = omitTag, TALtag
             self.pushProgram()
-        if attrsubst:
-            repldict = parseAttributeReplacements(attrsubst)
+        if attrsubst or i18nattrs:
+            if attrsubst:
+                repldict = TALDefs.parseAttributeReplacements(attrsubst)
+            else:
+                repldict = {}
+            if i18nattrs:
+                i18nattrs = i18nattrs.split()
+            else:
+                i18nattrs = ()
+            # Convert repldict's name-->expr mapping to a
+            # name-->(compiled_expr, translate) mapping
             for key, value in repldict.items():
-                repldict[key] = self.compileExpression(value)
+                repldict[key] = self.compileExpression(value), key in i18nattrs
+            for key in i18nattrs:
+                if not repldict.has_key(key):
+                    repldict[key] = None, 1
         else:
             repldict = {}
         if replace:
@@ -507,6 +663,8 @@
             self.pushProgram()
         if content:
             self.pushProgram()
+        if msgid is not None:
+            self.pushProgram()
         if todo and position != (None, None):
             todo["position"] = position
         self.todoPush(todo)
@@ -535,6 +693,10 @@
         repldict = todo.get("repldict", {})
         scope = todo.get("scope")
         optTag = todo.get("optional tag")
+        msgid = todo.get('msgid')
+        i18ncontext = todo.get("i18ncontext")
+        varname = todo.get('i18nvar')
+        i18ndata = todo.get('i18ndata')
 
         if implied > 0:
             if defineMacro or useMacro or defineSlot or fillSlot:
@@ -546,14 +708,51 @@
             raise exc("%s attributes on <%s> require explicit </%s>" %
                       (what, name, name), position)
 
+        # If there's no tal:content or tal:replace in the tag with the
+        # i18n:name, tal:replace is the default.
+        i18nNameAction = I18N_REPLACE
         if content:
+            if varname:
+                i18nNameAction = I18N_CONTENT
             self.emitSubstitution(content, {})
+        # If we're looking at an implicit msgid, emit the insertTranslation
+        # opcode now, so that the end tag doesn't become part of the implicit
+        # msgid.  If we're looking at an explicit msgid, it's better to emit
+        # the opcode after the i18nVariable opcode so we can better handle
+        # tags with both of them in them (and in the latter case, the contents
+        # would be thrown away for msgid purposes).
+        if msgid is not None and not varname:
+            self.emitTranslation(msgid, i18ndata)
         if optTag:
             self.emitOptTag(name, optTag, isend)
         elif not isend:
+            # If we're processing the end tag for a tag that contained
+            # i18n:name, we need to make sure that optimize() won't collect
+            # immediately following end tags into the same rawtextOffset, so
+            # put a spacer here that the optimizer will recognize.
+            if varname:
+                self.emit('noop')
             self.emitEndTag(name)
+        # If i18n:name appeared in the same tag as tal:replace then we're
+        # going to do the substitution a little bit differently.  The results
+        # of the expression go into the i18n substitution dictionary.
         if replace:
             self.emitSubstitution(replace, repldict)
+        elif varname:
+            if varname[1] is not None:
+                i18nNameAction = I18N_EXPRESSION
+            # o varname[0] is the variable name
+            # o i18nNameAction is either
+            #   - I18N_REPLACE for implicit tal:replace
+            #   - I18N_CONTENT for tal:content
+            #   - I18N_EXPRESSION for explicit tal:replace
+            # o varname[1] will be None for the first two actions and the
+            #   replacement tal expression for the third action.
+            self.emitI18nVariable(varname[0], i18nNameAction, varname[1])
+        # Do not test for "msgid is not None", i.e. we only want to test for
+        # explicit msgids here.  See comment above.
+        if msgid is not None and varname:
+            self.emitTranslation(msgid, i18ndata)
         if repeat:
             self.emitRepeat(repeat)
         if condition:
@@ -562,6 +761,10 @@
             self.emitOnError(name, onError)
         if scope:
             self.emit("endScope")
+        if i18ncontext:
+            self.emit("endI18nContext")
+            assert self.i18nContext.parent is not None
+            self.i18nContext = self.i18nContext.parent
         if defineSlot:
             self.emitDefineSlot(defineSlot)
         if fillSlot:


=== Zope/lib/python/TAL/TALInterpreter.py 1.69.6.1 => 1.69.6.1.2.1 ===
--- Zope/lib/python/TAL/TALInterpreter.py:1.69.6.1	Sun Sep  8 22:21:48 2002
+++ Zope/lib/python/TAL/TALInterpreter.py	Sun Sep 15 18:43:34 2002
@@ -8,7 +8,7 @@
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
+# FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
 """
@@ -17,17 +17,16 @@
 
 import sys
 import getopt
-
+import re
+from types import ListType
 from cgi import escape
-
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
+# Do not use cStringIO here!  It's not unicode aware. :(
+from StringIO import StringIO
 
 from TALDefs import quote, TAL_VERSION, TALError, METALError
 from TALDefs import isCurrentVersion, getProgramVersion, getProgramMode
 from TALGenerator import TALGenerator
+from TranslationContext import TranslationContext
 
 BOOLEAN_HTML_ATTRS = [
     # List of Boolean attributes in HTML that should be rendered in
@@ -40,13 +39,12 @@
     "defer"
 ]
 
-EMPTY_HTML_TAGS = [
-    # List of HTML tags with an empty content model; these are
-    # rendered in minimized form, e.g. <img />.
-    # From http://www.w3.org/TR/xhtml1/#dtds
-    "base", "meta", "link", "hr", "br", "param", "img", "area",
-    "input", "col", "basefont", "isindex", "frame",
-]
+def normalize(text):
+    # Now we need to normalize the whitespace in implicit message ids and
+    # implicit $name substitution values by stripping leading and trailing
+    # whitespace, and folding all internal whitespace to a single space.
+    return ' '.join(text.split())
+
 
 class AltTALGenerator(TALGenerator):
 
@@ -62,14 +60,16 @@
         if self.enabled:
             apply(TALGenerator.emit, (self,) + args)
 
-    def emitStartElement(self, name, attrlist, taldict, metaldict,
+    def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
                          position=(None, None), isend=0):
         metaldict = {}
         taldict = {}
+        i18ndict = {}
         if self.enabled and self.repldict:
             taldict["attributes"] = "x x"
         TALGenerator.emitStartElement(self, name, attrlist,
-                                      taldict, metaldict, position, isend)
+                                      taldict, metaldict, i18ndict,
+                                      position, isend)
 
     def replaceAttrs(self, attrlist, repldict):
         if self.enabled and self.repldict:
@@ -82,10 +82,10 @@
 
     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
+        self.engine = engine # Execution engine (aka context)
         self.Default = engine.getDefault()
         self.stream = stream or sys.stdout
         self._stream_write = self.stream.write
@@ -107,12 +107,14 @@
         self.endsep = "/>"
         self.endlen = len(self.endsep)
         self.macroStack = []
-        self.popMacro = self.macroStack.pop
         self.position = None, None  # (lineno, offset)
         self.col = 0
         self.level = 0
         self.scopeLevel = 0
         self.sourceFile = None
+        self.i18nStack = []
+        self.i18nInterpolate = i18nInterpolate
+        self.i18nContext = TranslationContext()
 
     def StringIO(self):
         # Third-party products wishing to provide a full Unicode-aware
@@ -121,19 +123,20 @@
 
     def saveState(self):
         return (self.position, self.col, self.stream,
-                self.scopeLevel, self.level)
+                self.scopeLevel, self.level, self.i18nContext)
 
     def restoreState(self, state):
-        (self.position, self.col, self.stream, scopeLevel, level) = state
+        (self.position, self.col, self.stream, scopeLevel, level, i18n) = state
         self._stream_write = self.stream.write
         assert self.level == level
         while self.scopeLevel > scopeLevel:
             self.engine.endScope()
             self.scopeLevel = self.scopeLevel - 1
         self.engine.setPosition(self.position)
+        self.i18nContext = i18n
 
     def restoreOutputState(self, state):
-        (dummy, self.col, self.stream, scopeLevel, level) = state
+        (dummy, self.col, self.stream, scopeLevel, level, i18n) = state
         self._stream_write = self.stream.write
         assert self.level == level
         assert self.scopeLevel == scopeLevel
@@ -142,7 +145,12 @@
         if len(self.macroStack) >= self.stackLimit:
             raise METALError("macro nesting limit (%d) exceeded "
                              "by %s" % (self.stackLimit, `macroName`))
-        self.macroStack.append([macroName, slots, entering])
+        self.macroStack.append([macroName, slots, entering, self.i18nContext])
+
+    def popMacro(self):
+        stuff = self.macroStack.pop()
+        self.i18nContext = stuff[3]
+        return stuff
 
     def macroContext(self, what):
         macroStack = self.macroStack
@@ -156,9 +164,11 @@
     def __call__(self):
         assert self.level == 0
         assert self.scopeLevel == 0
+        assert self.i18nContext.parent is None
         self.interpret(self.program)
         assert self.level == 0
         assert self.scopeLevel == 0
+        assert self.i18nContext.parent is None
         if self.col > 0:
             self._stream_write("\n")
             self.col = 0
@@ -174,15 +184,20 @@
 
     bytecode_handlers = {}
 
-    def interpret(self, program, None=None):
+    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 = tmpstream
+            self._stream_write = tmpstream.write
         try:
             if self.debug:
                 for (opcode, args) in program:
-                    s = "%sdo_%s%s\n" % ("    "*self.level, opcode,
-                                      repr(args))
+                    s = "%sdo_%s(%s)\n" % ("    "*self.level, opcode,
+                                           repr(args))
                     if len(s) > 80:
                         s = s[:76] + "...\n"
                     sys.stderr.write(s)
@@ -192,6 +207,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
@@ -229,8 +247,7 @@
         # for that case.
         _stream_write = self._stream_write
         _stream_write("<" + name)
-        namelen = _len(name)
-        col = self.col + namelen + 1
+        col = self.col + _len(name) + 1
         wrap = self.wrap
         align = col + 1
         if align >= wrap/2:
@@ -262,10 +279,11 @@
 
     def attrAction(self, item):
         name, value, action = item[:3]
-        if action == 1 or (action > 1 and not self.showtal):
+        if action == 'insert' or (action in ('metal', 'tal', 'xmlns', 'i18n')
+                                  and not self.showtal):
             return 0, name, value
         macs = self.macroStack
-        if action == 2 and self.metal and macs:
+        if action == 'metal' and self.metal and macs:
             if len(macs) > 1 or not macs[-1][2]:
                 # Drop all METAL attributes at a use-depth above one.
                 return 0, name, value
@@ -293,33 +311,43 @@
 
     def attrAction_tal(self, item):
         name, value, action = item[:3]
-        if action > 1:
+        if action in ('metal', 'tal', 'xmlns', 'i18n'):
             return self.attrAction(item)
         ok = 1
+        expr, msgid = item[3:]
         if self.html and name.lower() in BOOLEAN_HTML_ATTRS:
             evalue = self.engine.evaluateBoolean(item[3])
             if evalue is self.Default:
-                if action == 1: # Cancelled insert
+                if action == 'insert': # Cancelled insert
                     ok = 0
             elif evalue:
                 value = None
             else:
                 ok = 0
         else:
-            evalue = self.engine.evaluateText(item[3])
-            if evalue is self.Default:
-                if action == 1: # Cancelled insert
-                    ok = 0
-            else:
-                if evalue is None:
-                    ok = 0
-                value = evalue
+            if expr is not None:
+                evalue = self.engine.evaluateText(item[3])
+                if evalue is self.Default:
+                    if action == 'insert': # Cancelled insert
+                        ok = 0
+                else:
+                    if evalue is None:
+                        ok = 0
+                    value = evalue
         if ok:
+            if msgid:
+                value = self.i18n_attribute(value)
             if value is None:
                 value = name
             value = "%s=%s" % (name, quote(value))
         return ok, name, value
 
+    def i18n_attribute(self, s):
+        # s is the value of an attribute before translation
+        # it may have been computed
+        return self.translate(s, {})
+
+
     bytecode_handlers["<attrAction>"] = attrAction
 
     def no_tag(self, start, program):
@@ -334,7 +362,7 @@
                   omit=0):
         if tag_ns and not self.showtal:
             return self.no_tag(start, program)
-
+            
         self.interpret(start)
         if not isend:
             self.interpret(program)
@@ -354,7 +382,7 @@
     def dumpMacroStack(self, prefix, suffix, value):
         sys.stderr.write("+---- %s%s = %s\n" % (prefix, suffix, value))
         for i in range(len(self.macroStack)):
-            what, macroName, slots = self.macroStack[i]
+            what, macroName, slots = self.macroStack[i][:3]
             sys.stderr.write("| %2d. %-12s %-12s %s\n" %
                              (i, what, macroName, slots and slots.keys()))
         sys.stderr.write("+--------------------------------------\n")
@@ -412,6 +440,19 @@
         self.engine.setGlobal(name, self.engine.evaluateValue(expr))
     bytecode_handlers["setGlobal"] = do_setLocal
 
+    def do_beginI18nContext(self, settings):
+        get = settings.get
+        self.i18nContext = TranslationContext(self.i18nContext,
+                                              domain=get("domain"),
+                                              source=get("source"),
+                                              target=get("target"))
+    bytecode_handlers["beginI18nContext"] = do_beginI18nContext
+
+    def do_endI18nContext(self, notused=None):
+        self.i18nContext = self.i18nContext.parent
+        assert self.i18nContext is not None
+    bytecode_handlers["endI18nContext"] = do_endI18nContext
+
     def do_insertText(self, stuff):
         self.interpret(stuff[1])
 
@@ -431,6 +472,72 @@
             self.col = len(s) - (i + 1)
     bytecode_handlers["insertText"] = do_insertText
 
+    def do_i18nVariable(self, stuff):
+        varname, program, expression = stuff
+        if expression is None:
+            # The value is implicitly the contents of this tag, so we have to
+            # evaluate the mini-program to get the value of the variable.
+            state = self.saveState()
+            try:
+                tmpstream = self.StringIO()
+                self.interpret(program, tmpstream)
+                value = normalize(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(expression)
+        # Either the i18n:name tag is nested inside an i18n:translate in which
+        # case the last item on the stack has the i18n dictionary and string
+        # representation, or the i18n:name and i18n:translate attributes are
+        # in the same tag, in which case the i18nStack will be empty.  In that
+        # case we can just output the ${name} to the stream
+        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 = []
+        obj = None
+        self.i18nStack.append((i18ndict, srepr))
+        msgid = stuff[0]
+        # We need to evaluate the content of the tag because that will give us
+        # several useful pieces of information.  First, the contents will
+        # include an implicit message id, if no explicit one was given.
+        # Second, it will evaluate any i18nVariable definitions in the body of
+        # the translation (necessary for $varname substitutions).
+        #
+        # Use a temporary stream to capture the interpretation of the
+        # subnodes, which should /not/ go to the output stream.
+        tmpstream = self.StringIO()
+        self.interpret(stuff[1], tmpstream)
+        # We only care about the evaluated contents if we need an implicit
+        # message id.  All other useful information will be in the i18ndict on
+        # the top of the i18nStack.
+        if msgid == '':
+            msgid = normalize(tmpstream.getvalue())
+        self.i18nStack.pop()
+        # See if there is was an i18n:data for msgid
+        if len(stuff) > 2:
+            obj = self.engine.evaluate(stuff[2])
+        xlated_msgid = self.translate(msgid, i18ndict, obj)
+        # 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
+
     def do_insertStructure(self, stuff):
         self.interpret(stuff[2])
 
@@ -482,6 +589,24 @@
             self.interpret(block)
     bytecode_handlers["loop"] = do_loop
 
+    def translate(self, msgid, i18ndict=None, obj=None):
+        # XXX is this right?
+        if i18ndict is None:
+            i18ndict = {}
+        if obj:
+            i18ndict.update(obj)
+        # 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
+        # XXX Mmmh, it seems that sometimes the msgid is None; is that really
+        # possible?
+        if msgid is None:
+            return None
+        # XXX We need to pass in one of context or target_language
+        return self.engine.translate(self.i18nContext.domain, msgid, i18ndict)
+
     def do_rawtextColumn(self, (s, col)):
         self._stream_write(s)
         self.col = col
@@ -504,6 +629,7 @@
             if not entering:
                 macs.append(None)
                 self.interpret(macro)
+                assert macs[-1] is None
                 macs.pop()
                 return
         self.interpret(macro)
@@ -526,12 +652,11 @@
                 raise METALError("macro %s has incompatible mode %s" %
                                  (`macroName`, `mode`), self.position)
         self.pushMacro(macroName, compiledSlots)
-        saved_source = self.sourceFile
-        saved_position = self.position  # Used by Boa Constructor
+        prev_source = self.sourceFile
         self.interpret(macro)
-        if self.sourceFile != saved_source:
-            self.engine.setSourceFile(saved_source)
-            self.sourceFile = saved_source
+        if self.sourceFile != prev_source:
+            self.engine.setSourceFile(prev_source)
+            self.sourceFile = prev_source
         self.popMacro()
     bytecode_handlers["useMacro"] = do_useMacro
 
@@ -547,21 +672,18 @@
             return
         macs = self.macroStack
         if macs and macs[-1] is not None:
-            saved_source = self.sourceFile
-            saved_position = self.position  # Used by Boa Constructor
             macroName, slots = self.popMacro()[:2]
             slot = slots.get(slotName)
             if slot is not None:
+                prev_source = self.sourceFile
                 self.interpret(slot)
-                if self.sourceFile != saved_source:
-                    self.engine.setSourceFile(saved_source)
-                    self.sourceFile = saved_source
+                if self.sourceFile != prev_source:
+                    self.engine.setSourceFile(prev_source)
+                    self.sourceFile = prev_source
                 self.pushMacro(macroName, slots, entering=0)
                 return
             self.pushMacro(macroName, slots)
-            if len(macs) == 1:
-                self.interpret(block)
-                return
+            # Falling out of the 'if' allows the macro to be interpreted.
         self.interpret(block)
     bytecode_handlers["defineSlot"] = do_defineSlot
 


=== Zope/lib/python/TAL/TALParser.py 1.19 => 1.19.8.1 ===
--- Zope/lib/python/TAL/TALParser.py:1.19	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/TALParser.py	Sun Sep 15 18:43:34 2002
@@ -8,7 +8,7 @@
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
+# FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
 """
@@ -16,7 +16,7 @@
 """
 
 from XMLParser import XMLParser
-from TALDefs import *
+from TALDefs import XML_NS, ZOPE_I18N_NS, ZOPE_METAL_NS, ZOPE_TAL_NS
 from TALGenerator import TALGenerator
 
 class TALParser(XMLParser):
@@ -58,13 +58,15 @@
             # attrs is a dict of {name: value}
             attrlist = attrs.items()
             attrlist.sort() # For definiteness
-        name, attrlist, taldict, metaldict = self.process_ns(name, attrlist)
+        name, attrlist, taldict, metaldict, i18ndict \
+              = self.process_ns(name, attrlist)
         attrlist = self.xmlnsattrs() + attrlist
-        self.gen.emitStartElement(name, attrlist, taldict, metaldict)
+        self.gen.emitStartElement(name, attrlist, taldict, metaldict, i18ndict)
 
     def process_ns(self, name, attrlist):
         taldict = {}
         metaldict = {}
+        i18ndict = {}
         fixedattrlist = []
         name, namebase, namens = self.fixname(name)
         for key, value in attrlist:
@@ -77,10 +79,14 @@
             elif ns == 'tal':
                 taldict[keybase] = value
                 item = item + ("tal",)
+            elif ns == 'i18n':
+                assert 0, "dealing with i18n: " + `(keybase, value)`
+                i18ndict[keybase] = value
+                item = item + ('i18n',)
             fixedattrlist.append(item)
-        if namens in ('metal', 'tal'):
+        if namens in ('metal', 'tal', 'i18n'):
             taldict['tal tag'] = namens
-        return name, fixedattrlist, taldict, metaldict
+        return name, fixedattrlist, taldict, metaldict, i18ndict
 
     def xmlnsattrs(self):
         newlist = []
@@ -89,7 +95,7 @@
                 key = "xmlns:" + prefix
             else:
                 key = "xmlns"
-            if uri in (ZOPE_METAL_NS, ZOPE_TAL_NS):
+            if uri in (ZOPE_METAL_NS, ZOPE_TAL_NS, ZOPE_I18N_NS):
                 item = (key, uri, "xmlns")
             else:
                 item = (key, uri)
@@ -109,6 +115,8 @@
                 ns = 'tal'
             elif uri == ZOPE_METAL_NS:
                 ns = 'metal'
+            elif uri == ZOPE_I18N_NS:
+                ns = 'i18n'
             return (prefixed, name, ns)
         return (name, name, None)
 


=== Zope/lib/python/TAL/XMLParser.py 1.9 => 1.9.8.1 ===
--- Zope/lib/python/TAL/XMLParser.py:1.9	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/XMLParser.py	Sun Sep 15 18:43:34 2002
@@ -50,7 +50,7 @@
             try:
                 self.parser.ordered_attributes = self.ordered_attributes
             except AttributeError:
-                zLOG.LOG("TAL.XMLParser", zLOG.INFO,
+                zLOG.LOG("TAL.XMLParser", zLOG.INFO, 
                          "Can't set ordered_attributes")
                 self.ordered_attributes = 0
         for name in self.handler_names:


=== Zope/lib/python/TAL/driver.py 1.28 => 1.28.8.1 ===
--- Zope/lib/python/TAL/driver.py:1.28	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/driver.py	Sun Sep 15 18:43:34 2002
@@ -9,11 +9,31 @@
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
+# FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
 """
 Driver program to test METAL and TAL implementation.
+
+Usage: driver.py [options] [file]
+Options:
+    -h / --help
+        Print this message and exit.
+    -H / --html
+    -x / --xml
+        Explicitly choose HTML or XML input.  The default is to automatically
+        select based on the file extension.  These options are mutually
+        exclusive.
+    -l
+        Lenient structure insertion.
+    -m
+        Macro expansion only
+    -s
+        Print intermediate opcodes only
+    -t
+        Leave TAL/METAL attributes in output
+    -i
+        Leave I18N substitution strings un-interpolated.
 """
 
 import os
@@ -26,68 +46,131 @@
 
 # Import local classes
 import TALDefs
-import DummyEngine
+from DummyEngine import DummyEngine
+from DummyEngine import DummyTranslationService
 
 FILE = "tests/input/test01.xml"
 
+class TestTranslations(DummyTranslationService):
+    def translate(self, domain, msgid, mapping=None, context=None,
+                  target_language=None):
+        if msgid == 'timefmt':
+            return '%(minutes)s minutes after %(hours)s %(ampm)s' % mapping
+        elif msgid == 'jobnum':
+            return '%(jobnum)s is the JOB NUMBER' % mapping
+        elif msgid == 'verify':
+            s = 'Your contact email address is recorded as %(email)s'
+            return s % mapping
+        elif msgid == 'mailto:${request/submitter}':
+            return 'mailto:bperson@dom.ain'
+        elif msgid == 'origin':
+            return '%(name)s was born in %(country)s' % mapping
+        return DummyTranslationService.translate(self, domain, msgid,
+                                                 mapping, context,
+                                                 target_language)
+
+class TestEngine(DummyEngine):
+    def __init__(self, macros=None):
+        DummyEngine.__init__(self, macros)
+        self.translationService = TestTranslations()
+
+    def evaluatePathOrVar(self, expr):
+        if expr == 'here/currentTime':
+            return {'hours'  : 6,
+                    'minutes': 59,
+                    'ampm'   : 'PM',
+                    }
+        elif expr == 'context/@@object_name':
+            return '7'
+        elif expr == 'request/submitter':
+            return 'aperson@dom.ain'
+        return DummyEngine.evaluatePathOrVar(self, expr)
+
+
+# This is a disgusting hack so that we can use engines that actually know
+# something about certain object paths.  TimeEngine knows about
+# here/currentTime.
+ENGINES = {'test23.html': TestEngine,
+           'test24.html': TestEngine,
+           'test26.html': TestEngine,
+           'test27.html': TestEngine,
+           'test28.html': TestEngine,
+           'test29.html': TestEngine,
+           'test30.html': TestEngine,
+           'test31.html': TestEngine,
+           'test32.html': TestEngine,
+           }
+
+def usage(code, msg=''):
+    # Python 2.1 required
+    print >> sys.stderr, __doc__
+    if msg:
+        print >> sys.stderr, msg
+    sys.exit(code)
+
 def main():
-    versionTest = 1
     macros = 0
     mode = None
     showcode = 0
     showtal = -1
     strictinsert = 1
+    i18nInterpolate = 1
     try:
-        opts, args = getopt.getopt(sys.argv[1:], "hxlmnst")
+        opts, args = getopt.getopt(sys.argv[1:], "hHxlmsti",
+                                   ['help', 'html', 'xml'])
     except getopt.error, msg:
-        sys.stderr.write("\n%s\n" % str(msg))
-        sys.stderr.write(
-            "usage: driver.py [-h|-x] [-l] [-m] [-n] [-s] [-t] [file]\n")
-        sys.stderr.write("-h/-x -- HTML/XML input (default auto)\n")
-        sys.stderr.write("-l -- lenient structure insertion\n")
-        sys.stderr.write("-m -- macro expansion only\n")
-        sys.stderr.write("-n -- turn off the Python 1.5.2 test\n")
-        sys.stderr.write("-s -- print intermediate code\n")
-        sys.stderr.write("-t -- leave tal/metal attributes in output\n")
-        sys.exit(2)
-    for o, a in opts:
-        if o == '-h':
+        usage(2, msg)
+    for opt, arg in opts:
+        if opt in ('-h', '--help'):
+            usage(0)
+        if opt in ('-H', '--html'):
+            if mode == 'xml':
+                usage(1, '--html and --xml are mutually exclusive')
             mode = "html"
-        if o == '-l':
+        if opt == '-l':
             strictinsert = 0
-        if o == '-m':
+        if opt == '-m':
             macros = 1
-        if o == '-n':
+        if opt == '-n':
             versionTest = 0
-        if o == '-x':
+        if opt in ('-x', '--xml'):
+            if mode == 'html':
+                usage(1, '--html and --xml are mutually exclusive')
             mode = "xml"
-        if o == '-s':
+        if opt == '-s':
             showcode = 1
-        if o == '-t':
+        if opt == '-t':
             showtal = 1
-    if not versionTest:
-        if sys.version[:5] != "1.5.2":
-            sys.stderr.write(
-                "Use Python 1.5.2 only; use -n to disable this test\n")
-            sys.exit(2)
+        if opt == '-i':
+            i18nInterpolate = 0
     if args:
         file = args[0]
     else:
         file = FILE
     it = compilefile(file, mode)
-    if showcode: showit(it)
-    else: interpretit(it, tal=(not macros), showtal=showtal,
-                      strictinsert=strictinsert)
+    if showcode:
+        showit(it)
+    else:
+        # See if we need a special engine for this test
+        engine = None
+        engineClass = ENGINES.get(os.path.basename(file))
+        if engineClass is not None:
+            engine = engineClass(macros)
+        interpretit(it, engine=engine,
+                    tal=(not macros), showtal=showtal,
+                    strictinsert=strictinsert,
+                    i18nInterpolate=i18nInterpolate)
 
 def interpretit(it, engine=None, stream=None, tal=1, showtal=-1,
-                strictinsert=1):
+                strictinsert=1, i18nInterpolate=1):
     from TALInterpreter import TALInterpreter
     program, macros = it
     assert TALDefs.isCurrentVersion(program)
     if engine is None:
-        engine = DummyEngine.DummyEngine(macros)
+        engine = DummyEngine(macros)
     TALInterpreter(program, macros, engine, stream, wrap=0,
-                   tal=tal, showtal=showtal, strictinsert=strictinsert)()
+                   tal=tal, showtal=showtal, strictinsert=strictinsert,
+                   i18nInterpolate=i18nInterpolate)()
 
 def compilefile(file, mode=None):
     assert mode in ("html", "xml", None)


=== Zope/lib/python/TAL/runtest.py 1.22 => 1.22.8.1 ===
--- Zope/lib/python/TAL/runtest.py:1.22	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/runtest.py	Sun Sep 15 18:43:34 2002
@@ -71,8 +71,8 @@
         htmlargs.sort()
         args = xmlargs + htmlargs
         if not args:
-            sys.stderr.write("No tests found -- please supply filenames\n")
-            sys.exit(1)
+             sys.stderr.write("No tests found -- please supply filenames\n")
+             sys.exit(1)
     errors = 0
     for arg in args:
         locopts = []