diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py
index fac2ec78284..1cd833c9df6 100644
--- a/odoo/addons/base/tests/test_translate.py
+++ b/odoo/addons/base/tests/test_translate.py
@@ -52,6 +52,14 @@ class TranslationToolsTestCase(unittest.TestCase):
self.assertEquals(result, source)
self.assertItemsEqual(terms, [source])
+ def test_translate_xml_unicode(self):
+ """ Test xml_translate() on plain text with unicode characters. """
+ terms = []
+ source = u"Un heureux évènement"
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms, [source])
+
def test_translate_xml_text_entity(self):
""" Test xml_translate() on plain text with HTML escaped entities. """
terms = []
@@ -162,6 +170,40 @@ class TranslationToolsTestCase(unittest.TestCase):
self.assertItemsEqual(terms,
['', 'More '])
+ def test_translate_xml_with_namespace(self):
+ """ Test xml_translate() on elements with namespaces. """
+ terms = []
+ # do not slit the long line below, otherwise the result will not match
+ source = """
+
+
+ Oasis
+
+ """
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms, ['Oasis'])
+ result = xml_translate(lambda term: term, source)
+ self.assertEquals(result, source)
+
+ def test_translate_xml_invalid_translations(self):
+ """ Test xml_translate() with invalid translations. """
+ source = """
"""
+ translations = {
+ "Put some more text here": "Mettre plus de texte ici",
+ }
+ expect = """"""
+ result = xml_translate(translations.get, source)
+ self.assertEquals(result, expect)
+
def test_translate_html(self):
""" Test xml_translate() and html_translate() with elements. """
source = """"""
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index 1429480e8c0..c947ae5e75d 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -15,7 +15,6 @@ import threading
from collections import defaultdict
from datetime import datetime
from os.path import join
-from xml.sax.saxutils import escape
from babel.messages import extract
from lxml import etree
@@ -150,133 +149,130 @@ TRANSLATED_ATTRS = {
'string', 'help', 'sum', 'avg', 'confirm', 'placeholder', 'alt', 'title',
}
-avoid_pattern = re.compile(r"[\s\n]*]*>(.*)[^<]*>", re.DOTALL | re.MULTILINE | re.UNICODE)
-class XMLTranslator(object):
- """ A sequence of serialized XML/HTML 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 ) 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):
+def translate_xml_node(node, callback, method, parser=None):
+ """ Return the translation of the given XML/HTML node. """
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit,
- sed do eiusmod tempor incididunt ut labore et dolore
- magna aliqua. Ut enim ad minim veniam,
- quis nostrud exercitation ullamco laboris nisi ut
- aliquip ex ea commodo consequat.
-
+ def nonspace(text):
+ return bool(text) and not text.isspace()
- """
- def __init__(self, callback, method, parser=None):
- self.callback = callback # callback function to translate terms
- self.method = method # serialization method ('xml' or 'html')
- self.parser = parser # parser for validating translations
- self._done = [] # translated strings
- self._todo = [] # todo strings that come after _done
- self.needs_trans = False # whether todo needs translation
+ def concat(text1, text2):
+ return text2 if text1 is None else text1 + (text2 or "")
- def todo(self, text, needs_trans=True):
- self._todo.append(text)
- if needs_trans and text.strip():
- self.needs_trans = True
+ def append_content(node, source):
+ """ Append the content of ``source`` node to ``node``. """
+ if len(node):
+ node[-1].tail = concat(node[-1].tail, source.text)
+ else:
+ node.text = concat(node.text, source.text)
+ for child in source:
+ node.append(child)
- 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)
- done = self.process_text(todo) if self.needs_trans else todo
- self._done.append(done)
- del self._todo[:]
- self.needs_trans = False
-
- 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. """
+ def translate_text(text):
+ """ Return the translation of ``text`` (the term to translate is without
+ surrounding spaces), or a falsy value if no translation applies.
+ """
term = text.strip()
- trans = term and self.callback(term)
+ trans = term and callback(term)
+ return trans and text.replace(term, trans)
+
+ def translate_content(node):
+ """ Return ``node`` with its content translated inline. """
+ # serialize the node that contains the stuff to translate
+ text = etree.tostring(node, method=method, encoding='utf8').decode('utf8')
+ # retrieve the node's content and translate it
+ match = node_pattern.match(text)
+ trans = translate_text(match.group(1))
if trans:
+ # replace the content, and convert it back to an XML node
+ text = text[:match.start(1)] + trans + text[match.end(1):]
try:
- # parse the translation to validate it
- etree.fromstring("%s
" % encode(trans), parser=self.parser)
+ node = etree.fromstring(encode(text), parser=parser)
except etree.ParseError:
- # fallback: escape the translation
- trans = escape(trans)
- text = text.replace(term, trans)
- return text
+ # fallback: escape the translation as text
+ node = etree.Element(node.tag, node.attrib, node.nsmap)
+ node.text = trans
+ return node
- def process_attr(self, attr):
- """ Translate the given node attribute value. """
- term = attr.strip()
- trans = term and self.callback(term)
- return attr.replace(term, trans) if trans else attr
-
- def process(self, node):
- """ Process the given xml `node`: collect `todo` and `done` items. """
+ def process(node):
+ """ If ``node`` can be translated inline, return ``(has_text, node)``,
+ where ``has_text`` is a boolean that tells whether ``node`` contains
+ some actual text to translate. Otherwise return ``(None, result)``,
+ where ``result`` is the translation of ``node`` except for its tail.
+ """
if (
isinstance(node, SKIPPED_ELEMENT_TYPES) or
node.tag in SKIPPED_ELEMENTS or
- node.get("t-translation", "").strip() == "off" or
- node.tag == "attribute" and node.get("name") not in TRANSLATED_ATTRS or
- node.getparent() is None and node.text and '")
- return "%s>%s%s>" % (elem[:-2], content, tag)
- else:
- return etree.tostring(etree.Element(tag, attrib), method=self.method)
+ # translate the required attributes
+ for name, value in result.items():
+ if name in TRANSLATED_ATTRS:
+ result.set(name, translate_text(value) or value)
+
+ # add the untranslated tail to result
+ result.tail = node.tail
+
+ return (None, result)
+
+ has_text, node = process(node)
+ if has_text is True:
+ # translate the node as a whole
+ wrapped = etree.Element('div')
+ wrapped.append(node)
+ return translate_content(wrapped)[0]
+
+ return node
def xml_translate(callback, value):
@@ -286,17 +282,18 @@ def xml_translate(callback, value):
if not value:
return value
- trans = XMLTranslator(callback, 'xml')
try:
root = etree.fromstring(encode(value))
- trans.process(root)
- return trans.get_done()
+ result = translate_xml_node(root, callback, 'xml')
+ return etree.tostring(result, method='xml', encoding='utf8').decode('utf8')
except etree.ParseError:
# fallback for translated terms: use an HTML parser and wrap the term
wrapped = "%s
" % encode(value)
root = etree.fromstring(wrapped, etree.HTMLParser(encoding='utf-8'))
- trans.process(root[0][0]) # html > body > div
- return trans.get_done()[5:-6] # remove tags and
+ # root is html > body > div; translate the div only
+ result = translate_xml_node(root[0][0], callback, 'xml')
+ # remove tags and
from result
+ return etree.tostring(result, method='xml', encoding='utf8').decode('utf8')[5:-6]
def html_translate(callback, value):
""" Translate an HTML value (string), using `callback` for translating text
@@ -307,13 +304,16 @@ def html_translate(callback, value):
try:
parser = etree.HTMLParser(encoding='utf-8')
- trans = XMLTranslator(callback, 'html', parser)
+ # value may be some HTML fragment, wrap it into a div
wrapped = "%s
" % encode(value)
root = etree.fromstring(wrapped, parser)
- trans.process(root[0][0]) # html > body > div
- value = trans.get_done()[5:-6] # remove tags and
+ # root is html > body > div; translate the div only
+ result = translate_xml_node(root[0][0], callback, 'html', parser)
+ # remove tags and
from result
+ value = etree.tostring(result, method='html', encoding='utf8').decode('utf8')[5:-6]
except ValueError:
_logger.exception("Cannot translate malformed HTML, using source value instead")
+
return value