[Zope-Checkins] CVS: Zope/lib/python/TAL - DummyEngine.py:1.37 TALDefs.py:1.36 TALGenerator.py:1.63 TALInterpreter.py:1.78 driver.py:1.30

Fred L. Drake, Jr. fred@zope.com
Mon, 7 Apr 2003 13:38:58 -0400


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

Modified Files:
	DummyEngine.py TALDefs.py TALGenerator.py TALInterpreter.py 
	driver.py 
Log Message:
Back-port two features from the Zope 3 version of Page Templates:
- avoid normalizing whitespace when using the default text when there is not
  a matching translation
- added support for explicit msgids in the i18n:attributes syntax


=== Zope/lib/python/TAL/DummyEngine.py 1.36 => 1.37 ===
--- Zope/lib/python/TAL/DummyEngine.py:1.36	Thu Jan 30 13:18:43 2003
+++ Zope/lib/python/TAL/DummyEngine.py	Mon Apr  7 13:38:27 2003
@@ -206,12 +206,18 @@
     def getDefault(self):
         return Default
 
-    def translate(self, domain, msgid, mapping):
-        return self.translationService.translate(domain, msgid, mapping)
-    
+    def translate(self, domain, msgid, mapping, default=None):
+        return self.translationService.translate(domain, msgid, mapping,
+                                                 default=default)
+
 
 class Iterator:
 
+    # This is not an implementation of a Python iterator.  The next()
+    # method returns true or false to indicate whether another item is
+    # available; if there is another item, the iterator instance calls
+    # setLocal() on the evaluation engine passed to the constructor.
+
     def __init__(self, name, seq, engine):
         self.name = name
         self.seq = seq
@@ -232,26 +238,33 @@
     __implements__ = IDomain
 
     def translate(self, msgid, mapping=None, context=None,
-                  target_language=None):
+                  target_language=None, default=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.
+
+        # simulate an unknown msgid by returning None
+        if msgid == "don't translate me":
+            text = default
+        else:
+            text = msgid.upper()
+
         def repl(m, mapping=mapping):
             return ustr(mapping[m.group(m.lastindex).lower()])
-        cre = re.compile(r'\$(?:([_A-Z]\w*)|\{([_A-Z]\w*)\})')
-        return cre.sub(repl, msgid.upper())
+        cre = re.compile(r'\$(?:(%s)|\{(%s)\})' % (NAME_RE, NAME_RE))
+        return cre.sub(repl, text)
 
 class DummyTranslationService:
     __implements__ = ITranslationService
 
     def translate(self, domain, msgid, mapping=None, context=None,
-                  target_language=None):
-        # Ignore domain
+                  target_language=None, default=None):
         return self.getDomain(domain).translate(msgid, mapping, context,
-                                                target_language)
+                                                target_language,
+                                                default=default)
 
     def getDomain(self, domain):
         return DummyDomain()


=== Zope/lib/python/TAL/TALDefs.py 1.35 => 1.36 ===
--- Zope/lib/python/TAL/TALDefs.py:1.35	Thu Mar 20 14:58:27 2003
+++ Zope/lib/python/TAL/TALDefs.py	Mon Apr  7 13:38:27 2003
@@ -28,7 +28,9 @@
 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_]*"
+# This RE must exactly match the expression of the same name in the
+# zope.i18n.simpletranslationservice module:
+NAME_RE = "[a-zA-Z_][-a-zA-Z0-9_]*"
 
 KNOWN_METAL_ATTRIBUTES = [
     "define-macro",
@@ -118,12 +120,12 @@
     for part in splitParts(arg):
         m = _attr_re.match(part)
         if not m:
-            raise TALError("Bad syntax in attributes:" + `part`)
+            raise TALError("Bad syntax in attributes: " + `part`)
         name, expr = m.group(1, 2)
         if not xml:
             name = name.lower()
         if dict.has_key(name):
-            raise TALError("Duplicate attribute name in attributes:" + `part`)
+            raise TALError("Duplicate attribute name in attributes: " + `part`)
         dict[name] = expr
     return dict
 


=== Zope/lib/python/TAL/TALGenerator.py 1.62 => 1.63 ===
--- Zope/lib/python/TAL/TALGenerator.py:1.62	Thu Mar 20 14:58:27 2003
+++ Zope/lib/python/TAL/TALGenerator.py	Mon Apr  7 13:38:27 2003
@@ -29,6 +29,9 @@
 I18N_CONTENT = 2
 I18N_EXPRESSION = 3
 
+_name_rx = re.compile(NAME_RE)
+
+
 class TALGenerator:
 
     inMacroUse = 0
@@ -329,6 +332,9 @@
         # calculate the contents of the variable, e.g.
         # "I live in <span i18n:name="country"
         #                  tal:replace="here/countryOfOrigin" />"
+        m = _name_rx.match(varname)
+        if m is None or m.group() != varname:
+            raise TALError("illegal i18n:name: %r" % varname, self.position)
         key = cexpr = None
         program = self.popProgram()
         if action == I18N_REPLACE:
@@ -458,13 +464,13 @@
         for item in attrlist:
             key = item[0]
             if repldict.has_key(key):
-                expr, xlat = repldict[key]
-                item = item[:2] + ("replace", expr, xlat)
+                expr, xlat, msgid = repldict[key]
+                item = item[:2] + ("replace", expr, xlat, msgid)
                 del repldict[key]
             newlist.append(item)
         # Add dynamic-only attributes
-        for key, (expr, xlat) in repldict.items():
-            newlist.append((key, None, "insert", expr, xlat))
+        for key, (expr, xlat, msgid) in repldict.items():
+            newlist.append((key, None, "insert", expr, xlat, msgid))
         return newlist
 
     def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
@@ -651,16 +657,18 @@
             else:
                 repldict = {}
             if i18nattrs:
-                i18nattrs = i18nattrs.split()
+                i18nattrs = _parseI18nAttributes(i18nattrs, self.position,
+                                                 self.xml)
             else:
-                i18nattrs = ()
+                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), key in i18nattrs
+                ce = self.compileExpression(value)
+                repldict[key] = ce, key in i18nattrs, i18nattrs.get(key)
             for key in i18nattrs:
                 if not repldict.has_key(key):
-                    repldict[key] = None, 1
+                    repldict[key] = None, 1, i18nattrs.get(key)
         else:
             repldict = {}
         if replace:
@@ -781,6 +789,30 @@
             self.emitUseMacro(useMacro)
         if defineMacro:
             self.emitDefineMacro(defineMacro)
+
+
+def _parseI18nAttributes(i18nattrs, position, xml):
+    d = {}
+    for spec in i18nattrs.split(";"):
+        parts = spec.split()
+        if len(parts) > 2:
+            raise TALError("illegal i18n:attributes specification: %r" % spec,
+                           position)
+        if len(parts) == 2:
+            attr, msgid = parts
+        else:
+            # len(parts) == 1
+            attr = parts[0]
+            msgid = None
+        if not xml:
+            attr = attr.lower()
+        if attr in d:
+            raise TALError(
+                "attribute may only be specified once in i18n:attributes: %r"
+                % attr,
+                position)
+        d[attr] = msgid
+    return d
 
 def test():
     t = TALGenerator()


=== Zope/lib/python/TAL/TALInterpreter.py 1.77 => 1.78 ===
--- Zope/lib/python/TAL/TALInterpreter.py:1.77	Thu Feb 27 11:18:40 2003
+++ Zope/lib/python/TAL/TALInterpreter.py	Mon Apr  7 13:38:27 2003
@@ -349,7 +349,7 @@
             return self.attrAction(item)
         name, value, action = item[:3]
         ok = 1
-        expr, msgid = item[3:]
+        expr, xlat, msgid = item[3:]
         if self.html and name.lower() in BOOLEAN_HTML_ATTRS:
             evalue = self.engine.evaluateBoolean(item[3])
             if evalue is self.Default:
@@ -368,23 +368,18 @@
                 if evalue is None:
                     ok = 0
                 value = evalue
-        if msgid:
-            value = self.i18n_attribute(value)
-        if value is None:
-            value = name
-        value = '%s="%s"' % (name, escape(value, 1))
+
+        if ok:
+            if xlat:
+                translated = self.translate(msgid or value, value, {})
+                if translated is not None:
+                    value = translated
+            if value is None:
+                value = name
+            value = '%s="%s"' % (name, escape(value, 1))
         return ok, name, value
     bytecode_handlers["<attrAction>"] = attrAction
 
-    def i18n_attribute(self, s):
-        # s is the value of an attribute before translation
-        # it may have been computed
-        xlated = self.translate(s, {})
-        if xlated is None:
-            return s
-        else:
-            return xlated
-
     def no_tag(self, start, program):
         state = self.saveState()
         self.stream = stream = self.StringIO()
@@ -553,36 +548,19 @@
         # subnodes, which should /not/ go to the output stream.
         tmpstream = self.StringIO()
         self.interpretWithStream(stuff[1], tmpstream)
-        content = None
+        default = tmpstream.getvalue()
         # 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 == '':
-            content = tmpstream.getvalue()
-            msgid = normalize(content)
+            msgid = normalize(default)
         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)
-        # If there is no translation available, use evaluated content.
-        if xlated_msgid is None:
-            if content is None:
-                content = tmpstream.getvalue()
-            # We must do potential substitutions "by hand".
-            s = interpolate(content, i18ndict)
-        else:
-            # 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.
-        # XXX efge: actually, this is already done by the translation service.
-        self._stream_write(s)
+        xlated_msgid = self.translate(msgid, default, i18ndict, obj)
+        assert xlated_msgid is not None, self.position
+        self._stream_write(xlated_msgid)
     bytecode_handlers['insertTranslation'] = do_insertTranslation
 
     def do_insertStructure(self, stuff):
@@ -636,23 +614,14 @@
             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 = {}
+    def translate(self, msgid, default, i18ndict, obj=None):
         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)
+        return self.engine.translate(self.i18nContext.domain,
+                                     msgid, i18ndict, default=default)
 
     def do_rawtextColumn(self, (s, col)):
         self._stream_write(s)


=== Zope/lib/python/TAL/driver.py 1.29 => 1.30 ===
--- Zope/lib/python/TAL/driver.py:1.29	Wed Sep 18 11:12:48 2002
+++ Zope/lib/python/TAL/driver.py	Mon Apr  7 13:38:27 2003
@@ -53,7 +53,7 @@
 
 class TestTranslations(DummyTranslationService):
     def translate(self, domain, msgid, mapping=None, context=None,
-                  target_language=None):
+                  target_language=None, default=None):
         if msgid == 'timefmt':
             return '%(minutes)s minutes after %(hours)s %(ampm)s' % mapping
         elif msgid == 'jobnum':
@@ -67,7 +67,8 @@
             return '%(name)s was born in %(country)s' % mapping
         return DummyTranslationService.translate(self, domain, msgid,
                                                  mapping, context,
-                                                 target_language)
+                                                 target_language,
+                                                 default=default)
 
 class TestEngine(DummyEngine):
     def __init__(self, macros=None):