diff --git a/openerp/addons/base/tests/test_translate.py b/openerp/addons/base/tests/test_translate.py index 4607cd64b9e..f9ce5189e20 100644 --- a/openerp/addons/base/tests/test_translate.py +++ b/openerp/addons/base/tests/test_translate.py @@ -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 = """
+

Blah blah blah

+ Put some more text here + + """ + 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 = """
+

Blah blah blah

+ Put some more text here + + """ + 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_inline2(self): + """ Test xml_translate() with formatting elements embedding other elements. """ + terms = [] + source = """
+

Blah blah blah

+ Put some more text here + + """ + 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_t(self): + """ Test xml_translate() with t-* attributes. """ + terms = [] + source = """ + stuff before + + stuff after + """ + result = xml_translate(terms.append, source) + self.assertEquals(result, source) + self.assertItemsEqual(terms, + ['stuff before', 'stuff after']) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 3361ed85de2..1d6dbedaf93 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -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" % (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 ) 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): + +
+ 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 __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() #