[IMP] translate: in XML translations, include phrasing elements in translations
HTML phrasing elements (like `<a>`, `<b>`, etc.) should be included in translations. This avoids splitting phrases when translating HTML content. Note that elements with qweb-related attributes (t-*) are not translated inline. Add tests on xml_translate().
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import unittest
|
||||
from openerp.tools.translate import quote, unquote
|
||||
from openerp.tools.translate import quote, unquote, xml_translate
|
||||
|
||||
class TranslationToolsTestCase(unittest.TestCase):
|
||||
|
||||
@@ -27,3 +27,55 @@ class TranslationToolsTestCase(unittest.TestCase):
|
||||
self.assertRaises(AssertionError, quote, """test \nall kinds\n\no\r
|
||||
\\\\nope\n\n"
|
||||
""")
|
||||
|
||||
def test_translate_xml_base(self):
|
||||
""" Test xml_translate() without formatting elements. """
|
||||
terms = []
|
||||
source = """<form string="Form stuff">
|
||||
<h1>Blah blah blah</h1>
|
||||
Put some more text here
|
||||
<field name="foo"/>
|
||||
</form>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['Form stuff', 'Blah blah blah', 'Put some more text here'])
|
||||
|
||||
def test_translate_xml_inline1(self):
|
||||
""" Test xml_translate() with formatting elements. """
|
||||
terms = []
|
||||
source = """<form string="Form stuff">
|
||||
<h1>Blah <i>blah</i> blah</h1>
|
||||
Put some <b>more text</b> here
|
||||
<field name="foo"/>
|
||||
</form>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['Form stuff', 'Blah <i>blah</i> blah', 'Put some <b>more text</b> here'])
|
||||
|
||||
def test_translate_xml_inline2(self):
|
||||
""" Test xml_translate() with formatting elements embedding other elements. """
|
||||
terms = []
|
||||
source = """<form string="Form stuff">
|
||||
<b><h1>Blah <i>blah</i> blah</h1></b>
|
||||
Put <em>some <b>more text</b></em> here
|
||||
<field name="foo"/>
|
||||
</form>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['Form stuff', 'Blah <i>blah</i> blah', 'Put <em>some <b>more text</b></em> here'])
|
||||
|
||||
def test_translate_xml_t(self):
|
||||
""" Test xml_translate() with t-* attributes. """
|
||||
terms = []
|
||||
source = """<t t-name="stuff">
|
||||
stuff before
|
||||
<span t-field="o.name"/>
|
||||
stuff after
|
||||
</t>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['stuff before', 'stuff after'])
|
||||
|
||||
+104
-22
@@ -18,6 +18,7 @@ from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from lxml import etree
|
||||
from os.path import join
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
import config
|
||||
import misc
|
||||
@@ -137,10 +138,110 @@ def encode(s):
|
||||
return s.encode('utf8')
|
||||
return s
|
||||
|
||||
TRANSLATED_ELEMENTS = {
|
||||
'a', 'abbr', 'audio', 'b', 'bdi', 'bdo', 'br', 'canvas', 'cite', 'code',
|
||||
'data', 'datalist', 'del', 'dfn', 'em', 'embed', 'font', 'i', 'iframe',
|
||||
'ins', 'kbd', 'keygen', 'map', 'mark', 'math', 'meter', 'object', 'output',
|
||||
'progress', 'q', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strong',
|
||||
'sub', 'sup', 'svg', 'template', 'textarea', 'time', 'u', 'var', 'video',
|
||||
'wbr', 'text',
|
||||
}
|
||||
|
||||
TRANSLATED_ATTRS = {
|
||||
'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):
|
||||
""" A sequence of serialized xml items, with some of them to translate
|
||||
(todo) and others already translated (done). The purpose of this object
|
||||
is to simplify the handling of phrasing elements (like <b>) that must be
|
||||
translated together with their surrounding text.
|
||||
|
||||
For instance, the content of the "div" element below will be translated
|
||||
as a whole (without surrounding spaces):
|
||||
|
||||
<div>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
<b>sed</b> do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. <span class="more">Ut enim ad minim veniam,
|
||||
<em>quis nostrud exercitation</em> ullamco laboris nisi ut
|
||||
aliquip ex ea commodo consequat.</span>
|
||||
</div>
|
||||
|
||||
"""
|
||||
def __init__(self, callback):
|
||||
self.callback = callback # callback function to translate terms
|
||||
self._done = [] # translated strings
|
||||
self._todo = [] # todo strings that come after _done
|
||||
|
||||
def todo(self, text):
|
||||
self._todo.append(text)
|
||||
|
||||
def all_todo(self):
|
||||
return not self._done
|
||||
|
||||
def get_todo(self):
|
||||
return "".join(self._todo)
|
||||
|
||||
def flush(self):
|
||||
if self._todo:
|
||||
todo = "".join(self._todo)
|
||||
self._done.append(self.process_text(todo))
|
||||
del self._todo[:]
|
||||
|
||||
def done(self, text):
|
||||
self.flush()
|
||||
self._done.append(text)
|
||||
|
||||
def get_done(self):
|
||||
""" Complete the translations and return the result. """
|
||||
self.flush()
|
||||
return "".join(self._done)
|
||||
|
||||
def process_text(self, text):
|
||||
""" Translate text.strip(), but keep the surrounding spaces from text. """
|
||||
term = text.strip()
|
||||
trans = term and self.callback(term)
|
||||
return text.replace(term, trans) if trans else text
|
||||
|
||||
def process(self, node):
|
||||
""" Process the given xml `node`: collect `todo` and `done` items. """
|
||||
if isinstance(node, SKIPPED_ELEMENT_TYPES) or node.tag in SKIPPED_ELEMENTS:
|
||||
# serialize the node as done, but keep its tail as todo
|
||||
tail, node.tail = node.tail, None
|
||||
self.done(etree.tostring(node))
|
||||
self.todo(escape(tail or ""))
|
||||
return
|
||||
|
||||
# process children nodes locally in child_trans
|
||||
child_trans = XMLTranslator(self.callback)
|
||||
child_trans.todo(escape(node.text or ""))
|
||||
for child in node:
|
||||
child_trans.process(child)
|
||||
|
||||
if (child_trans.all_todo() and
|
||||
node.tag in TRANSLATED_ELEMENTS and
|
||||
not any(attr.startswith("t-") for attr in node.attrib)):
|
||||
# serialize the node element as todo
|
||||
self.todo(serialize(node.tag, node.attrib, child_trans.get_todo()))
|
||||
else:
|
||||
# complete translations and serialize result as done
|
||||
for attr in TRANSLATED_ATTRS:
|
||||
if node.get(attr):
|
||||
node.set(attr, self.process_text(node.get(attr)))
|
||||
self.done(serialize(node.tag, node.attrib, child_trans.get_done()))
|
||||
|
||||
# add node tail as todo
|
||||
self.todo(escape(node.tail or ""))
|
||||
|
||||
|
||||
def xml_translate(callback, value):
|
||||
""" Translate an XML value (string), using `callback` for translating text
|
||||
@@ -149,29 +250,10 @@ def xml_translate(callback, value):
|
||||
if not value:
|
||||
return value
|
||||
|
||||
def process_text(text):
|
||||
term = text.strip()
|
||||
trans = term and callback(term)
|
||||
return text.replace(term, trans) if trans else text
|
||||
|
||||
def process(node):
|
||||
if not isinstance(node, SKIPPED_ELEMENT_TYPES):
|
||||
if node.text:
|
||||
node.text = process_text(node.text)
|
||||
if node.tail:
|
||||
node.tail = process_text(node.tail)
|
||||
if node.tag == 'attribute' and node.get('name') in TRANSLATED_ATTRS:
|
||||
if node.text:
|
||||
node.text = process_text(node.text)
|
||||
for attr in TRANSLATED_ATTRS:
|
||||
if node.get(attr):
|
||||
node.set(attr, process_text(node.get(attr)))
|
||||
for child in node:
|
||||
process(child)
|
||||
|
||||
trans = XMLTranslator(callback)
|
||||
root = etree.fromstring(encode(value))
|
||||
process(root)
|
||||
return etree.tostring(root, encoding='utf8')
|
||||
trans.process(root)
|
||||
return trans.get_done()
|
||||
|
||||
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user