[FIX] translate: separate html_translate from xml_translate, fix serialization

The serialization gives different results for some elements.  For instance, an
empty element `i` must be serialized as `<i/>` in XML and `<i></i>` in HTML.
It must be done like that, otherwise the world (of web browsers) would break.

Add a test case for `html_translate`.
This commit is contained in:
Raphael Collet
2015-10-22 16:33:56 +02:00
parent 060a898f9f
commit 6c8e53a087
3 changed files with 50 additions and 26 deletions
+9 -1
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import unittest import unittest
from openerp.tools.translate import quote, unquote, xml_translate from openerp.tools.translate import quote, unquote, xml_translate, html_translate
class TranslationToolsTestCase(unittest.TestCase): class TranslationToolsTestCase(unittest.TestCase):
@@ -142,3 +142,11 @@ class TranslationToolsTestCase(unittest.TestCase):
self.assertEquals(result, source) self.assertEquals(result, source)
self.assertItemsEqual(terms, self.assertItemsEqual(terms,
['<span class="oe_menu_text">Blah</span>', 'More <b class="caret"/>']) ['<span class="oe_menu_text">Blah</span>', 'More <b class="caret"/>'])
def test_translate_html(self):
""" Test xml_translate() and html_translate() with <i> elements. """
source = """<i class="fa-check"></i>"""
result = xml_translate(lambda term: term, source)
self.assertEquals(result, """<i class="fa-check"/>""")
result = html_translate(lambda term: term, source)
self.assertEquals(result, source)
+3 -3
View File
@@ -15,7 +15,7 @@ import xmlrpclib
from openerp.tools import float_round, frozendict, html_sanitize, ustr, OrderedSet from openerp.tools import float_round, frozendict, html_sanitize, ustr, OrderedSet
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
from openerp.tools.translate import xml_translate from openerp.tools.translate import html_translate
DATE_LENGTH = len(date.today().strftime(DATE_FORMAT)) DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT)) DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
@@ -1245,9 +1245,9 @@ class Html(_String):
def _setup_attrs(self, model, name): def _setup_attrs(self, model, name):
super(Html, self)._setup_attrs(model, name) super(Html, self)._setup_attrs(model, name)
# Translated sanitized html fields must use xml_translate or a callable. # Translated sanitized html fields must use html_translate or a callable.
if self.translate and not callable(self.translate) and self.sanitize: if self.translate and not callable(self.translate) and self.sanitize:
self.translate = xml_translate self.translate = html_translate
_column_sanitize = property(attrgetter('sanitize')) _column_sanitize = property(attrgetter('sanitize'))
_related_sanitize = property(attrgetter('sanitize')) _related_sanitize = property(attrgetter('sanitize'))
+38 -22
View File
@@ -151,16 +151,8 @@ TRANSLATED_ATTRS = {
'string', 'help', 'sum', 'avg', 'confirm', 'placeholder', 'alt', 'title', 'string', 'help', 'sum', 'avg', 'confirm', 'placeholder', 'alt', 'title',
} }
def serialize(tag, attrib, content):
""" Return a serialized element with the given `tag`, attributes
`attrib`, and already-serialized `content`.
"""
elem = etree.tostring(etree.Element(tag, attrib))
assert elem.endswith("/>")
return "%s>%s</%s>" % (elem[:-2], content, tag) if content else elem
class XMLTranslator(object): class XMLTranslator(object):
""" A sequence of serialized xml items, with some of them to translate """ A sequence of serialized XML/HTML items, with some of them to translate
(todo) and others already translated (done). The purpose of this object (todo) and others already translated (done). The purpose of this object
is to simplify the handling of phrasing elements (like <b>) that must be is to simplify the handling of phrasing elements (like <b>) that must be
translated together with their surrounding text. translated together with their surrounding text.
@@ -177,8 +169,9 @@ class XMLTranslator(object):
</div> </div>
""" """
def __init__(self, callback): def __init__(self, callback, method):
self.callback = callback # callback function to translate terms self.callback = callback # callback function to translate terms
self.method = method # serialization method ('xml' or 'html')
self._done = [] # translated strings self._done = [] # translated strings
self._todo = [] # todo strings that come after _done self._todo = [] # todo strings that come after _done
self.needs_trans = False # whether todo needs translation self.needs_trans = False # whether todo needs translation
@@ -227,12 +220,12 @@ class XMLTranslator(object):
): ):
# do not translate the contents of the node # do not translate the contents of the node
tail, node.tail = node.tail, None tail, node.tail = node.tail, None
self.done(etree.tostring(node)) self.done(etree.tostring(node, method=self.method))
self.todo(escape(tail or "")) self.todo(escape(tail or ""))
return return
# process children nodes locally in child_trans # process children nodes locally in child_trans
child_trans = XMLTranslator(self.callback) child_trans = XMLTranslator(self.callback, self.method)
child_trans.todo(escape(node.text or "")) child_trans.todo(escape(node.text or ""))
for child in node: for child in node:
child_trans.process(child) child_trans.process(child)
@@ -241,38 +234,61 @@ class XMLTranslator(object):
node.tag in TRANSLATED_ELEMENTS and node.tag in TRANSLATED_ELEMENTS and
not any(attr.startswith("t-") for attr in node.attrib)): not any(attr.startswith("t-") for attr in node.attrib)):
# serialize the node element as todo # serialize the node element as todo
self.todo(serialize(node.tag, node.attrib, child_trans.get_todo()), self.todo(self.serialize(node.tag, node.attrib, child_trans.get_todo()),
child_trans.needs_trans) child_trans.needs_trans)
else: else:
# complete translations and serialize result as done # complete translations and serialize result as done
for attr in TRANSLATED_ATTRS: for attr in TRANSLATED_ATTRS:
if node.get(attr): if node.get(attr):
node.set(attr, self.process_text(node.get(attr))) node.set(attr, self.process_text(node.get(attr)))
self.done(serialize(node.tag, node.attrib, child_trans.get_done())) self.done(self.serialize(node.tag, node.attrib, child_trans.get_done()))
# add node tail as todo # add node tail as todo
self.todo(escape(node.tail or "")) self.todo(escape(node.tail or ""))
def serialize(self, tag, attrib, content):
""" Return a serialized element with the given `tag`, attributes
`attrib`, and already-serialized `content`.
"""
if content:
elem = etree.tostring(etree.Element(tag, attrib), method='xml')
assert elem.endswith("/>")
return "%s>%s</%s>" % (elem[:-2], content, tag)
else:
return etree.tostring(etree.Element(tag, attrib), method=self.method)
def xml_translate(callback, value): def xml_translate(callback, value):
""" Translate an XML value (string), using `callback` for translating text """ Translate an XML value (string), using `callback` for translating text
appearing in `value`. If `value` is not XML valid, it is parsed with an HTML appearing in `value`.
parser instead, and some element wrapping is done to make the parsing work.
""" """
if not value: if not value:
return value return value
trans = XMLTranslator(callback) trans = XMLTranslator(callback, 'xml')
try: try:
root = etree.fromstring(encode(value)) root = etree.fromstring(encode(value))
trans.process(root) trans.process(root)
return trans.get_done() return trans.get_done()
except etree.ParseError: except etree.ParseError:
wrapped = "<div>%s</div>" % encode(value) # fallback in case it is a translated term...
root = etree.fromstring(wrapped, etree.HTMLParser(encoding='utf-8')) root = etree.fromstring("<div>%s</div>" % encode(value))
# html > body > div trans.process(root)
trans.process(root[0][0]) return trans.get_done()[5:-6] # remove tags <div> and </div>
return trans.get_done()[5:-6]
def html_translate(callback, value):
""" Translate an HTML value (string), using `callback` for translating text
appearing in `value`.
"""
if not value:
return value
trans = XMLTranslator(callback, 'html')
wrapped = "<div>%s</div>" % encode(value)
root = etree.fromstring(wrapped, etree.HTMLParser(encoding='utf-8'))
# html > body > div
trans.process(root[0][0])
return trans.get_done()[5:-6] # remove tags <div> and </div>
# #