[Zope-Checkins] CVS: Zope/lib/python/TAL - ITALES.py:1.2 TranslationContext.py:1.2 DummyEngine.py:1.33 HTMLTALParser.py:1.35 TALDefs.py:1.29 TALGenerator.py:1.56 TALInterpreter.py:1.71 TALParser.py:1.20 XMLParser.py:1.10 driver.py:1.29 runtest.py:1.23

Florent Guillaume fg@nuxeo.com
Wed, 18 Sep 2002 11:12:49 -0400


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

Modified Files:
	DummyEngine.py HTMLTALParser.py TALDefs.py TALGenerator.py 
	TALInterpreter.py TALParser.py XMLParser.py driver.py 
	runtest.py 
Added Files:
	ITALES.py TranslationContext.py 
Log Message:
Merge of the Zope-2_6-i18n-branch into HEAD.

Impacted code:
- TAL: merge of the 2.7 i18n stuff, unicode fixes, tests.
- PageTemplates: addition of a global translation service and of its use
  by the TALES engine, unicode fixes, tests.
- StructuredText: unicode fixes, tests.


=== Zope/lib/python/TAL/ITALES.py 1.1 => 1.2 ===
--- /dev/null	Wed Sep 18 11:12:48 2002
+++ Zope/lib/python/TAL/ITALES.py	Wed Sep 18 11:12:48 2002
@@ -0,0 +1,153 @@
+"""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.")


=== Zope/lib/python/TAL/TranslationContext.py 1.1 => 1.2 ===
--- /dev/null	Wed Sep 18 11:12:49 2002
+++ Zope/lib/python/TAL/TranslationContext.py	Wed Sep 18 11:12:48 2002
@@ -0,0 +1,41 @@
+##############################################################################
+#
+# 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$
+"""
+
+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.33 ===
--- Zope/lib/python/TAL/DummyEngine.py:1.32	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/DummyEngine.py	Wed Sep 18 11:12:48 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,20 @@
 import re
 import sys
 
-import driver
-
 from TALDefs import NAME_RE, TALESError, ErrorInfo
+from ITALES import ITALESCompiler, ITALESEngine
+from DocumentTemplate.DT_Util import ustr
+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 +43,8 @@
     position = None
     source_file = None
 
+    __implements__ = ITALESCompiler, ITALESEngine
+
     def __init__(self, macros=None):
         if macros is None:
             macros = {}
@@ -43,6 +52,7 @@
         dict = {'nothing': None, 'default': Default}
         self.locals = self.globals = dict
         self.stack = [dict]
+        self.translationService = DummyTranslationService()
 
     def getCompilerError(self):
         return CompilerError
@@ -90,13 +100,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 +120,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)
 
@@ -125,7 +138,7 @@
     def evaluateText(self, expr):
         text = self.evaluate(expr)
         if text is not None and text is not Default:
-            text = str(text)
+            text = ustr(text)
         return text
 
     def evaluateStructure(self, expr):
@@ -146,6 +159,7 @@
             macro = self.macros[localName]
         else:
             # External macro
+            import driver
             program, macros = driver.compilefile(file)
             macro = macros.get(localName)
             if not macro:
@@ -157,6 +171,7 @@
         file, localName = self.findMacroFile(macroName)
         if not file:
             return file, localName
+        import driver
         doc = driver.parsefile(file)
         return doc, localName
 
@@ -183,6 +198,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 +219,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.35 ===
--- Zope/lib/python/TAL/HTMLTALParser.py:1.34	Tue Aug 27 14:31:17 2002
+++ Zope/lib/python/TAL/HTMLTALParser.py	Wed Sep 18 11:12:48 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, I18NError
 
 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.29 ===
--- Zope/lib/python/TAL/TALDefs.py:1.28	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/TALDefs.py	Wed Sep 18 11:12:48 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.56 ===
--- Zope/lib/python/TAL/TALGenerator.py:1.55	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/TALGenerator.py	Wed Sep 18 11:12:48 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.70 => 1.71 ===
--- Zope/lib/python/TAL/TALInterpreter.py:1.70	Sun Sep  8 22:16:26 2002
+++ Zope/lib/python/TAL/TALInterpreter.py	Wed Sep 18 11:12:48 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,17 @@
 
 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 DocumentTemplate.DT_Util import ustr
 
 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 +40,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 +61,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 +83,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 +108,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 +124,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 +146,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 +165,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 +185,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 +208,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 +248,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 +280,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 +312,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 +363,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 +383,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 +441,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 +473,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])
 
@@ -441,7 +549,7 @@
         if structure is self.Default:
             self.interpret(block)
             return
-        text = str(structure)
+        text = ustr(structure)
         if not (repldict or self.strictinsert):
             # Take a shortcut, no error checking
             self.stream_write(text)
@@ -482,6 +590,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 +630,7 @@
             if not entering:
                 macs.append(None)
                 self.interpret(macro)
+                assert macs[-1] is None
                 macs.pop()
                 return
         self.interpret(macro)
@@ -526,12 +653,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 +673,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.20 ===
--- Zope/lib/python/TAL/TALParser.py:1.19	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/TALParser.py	Wed Sep 18 11:12:48 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.10 ===
--- Zope/lib/python/TAL/XMLParser.py:1.9	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/XMLParser.py	Wed Sep 18 11:12:48 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.29 ===
--- Zope/lib/python/TAL/driver.py:1.28	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/driver.py	Wed Sep 18 11:12:48 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.23 ===
--- Zope/lib/python/TAL/runtest.py:1.22	Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/runtest.py	Wed Sep 18 11:12:48 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 = []