diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index f2f97b7d116..8b8f82a3678 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -35,7 +35,7 @@ PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif' # Completely arbitrary limits MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768) class Website(openerp.addons.web.controllers.main.Home): - @website.route('/', type='http', auth="public") + @website.route('/', type='http', auth="public", multilang=True) def index(self, **kw): return self.page("website.homepage") @@ -85,7 +85,7 @@ class Website(openerp.addons.web.controllers.main.Home): return werkzeug.wrappers.Response(url, mimetype='text/plain') return werkzeug.utils.redirect(url) - @website.route('/website/theme_change', type='http', auth="admin") # FIXME: auth + @website.route('/website/theme_change', type='http', auth="admin") def theme_change(self, theme_id=False, **kwargs): imd = request.registry['ir.model.data'] view = request.registry['ir.ui.view'] @@ -107,7 +107,7 @@ class Website(openerp.addons.web.controllers.main.Home): return request.website.render('website.themes', {'theme_changed': True}) - @website.route('/page/', type='http', auth="public") + @website.route('/page/', type='http', auth="public", multilang=True) def page(self, path, **kwargs): values = { 'path': path, @@ -160,6 +160,51 @@ class Website(openerp.addons.web.controllers.main.Home): }) return result + @website.route('/website/get_view_translations', type='json', auth='admin') + def get_view_translations(self, xml_id, lang=None): + lang = lang or request.context.get('lang') + views = self.customize_template_get(xml_id, optional=False) + views_ids = [view.get('id') for view in views if view.get('active')] + domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)] + irt = request.registry.get('ir.translation') + return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context) + + @website.route('/website/set_translations', type='json', auth='admin') + def set_translations(self, data, lang): + irt = request.registry.get('ir.translation') + for view_id, trans in data.items(): + view_id = int(view_id) + for t in trans: + initial_content = t['initial_content'].strip() + new_content = t['new_content'].strip() + tid = t['translation_id'] + if not tid: + old_trans = irt.search_read( + request.cr, request.uid, + [ + ('type', '=', 'view'), + ('res_id', '=', view_id), + ('lang', '=', lang), + ('src', '=', initial_content), + ]) + if old_trans: + tid = old_trans[0]['id'] + if tid: + vals = {'value': new_content} + irt.write(request.cr, request.uid, [tid], vals) + else: + new_trans = { + 'name': 'website', + 'res_id': view_id, + 'lang': lang, + 'type': 'view', + 'source': initial_content, + 'value': new_content, + } + irt.create(request.cr, request.uid, new_trans) + irt._get_source.clear_cache(irt) # FIXME: find why ir.translation does not invalidate + return True + # # FIXME: auth, anybody can upload an attachment if URL known/found @website.route('/website/attach', type='http', auth='admin') def attach(self, func, upload): diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css index 64f58c8db11..1fde0c25f3d 100644 --- a/addons/website/static/src/css/editor.css +++ b/addons/website/static/src/css/editor.css @@ -107,6 +107,31 @@ table.editorbar-panel td.selected { background-color: #b1c9d9; } +.oe_translate_examples li { + margin: 10px; + padding: 4px; +} + +.oe_translatable_text { + outline: 1px solid black; +} + +.oe_translatable_field { + outline: 1px dashed black; +} + +.oe_translatable_text.oe_dirty, .oe_translatable_field.oe_dirty { + outline-color: red; +} + +.oe_translatable_text.oe_dirty:empty { + padding: 0 10px; +} + +.oe_translatable_todo { + background: #ffffb6; +} + /* ---- RTE ---- */ .oe_editable .btn { -webkit-user-select: auto; diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass index 90b02d3b83a..891630b63a4 100644 --- a/addons/website/static/src/css/editor.sass +++ b/addons/website/static/src/css/editor.sass @@ -93,6 +93,22 @@ table.editorbar-panel td.selected background-color: #b1c9d9 +// ---- TRANSLATIONS ---- {{{ +.oe_translate_examples li + margin: 10px + padding: 4px +.oe_translatable_text + outline: 1px solid black +.oe_translatable_field + outline: 1px dashed black +.oe_translatable_text.oe_dirty, .oe_translatable_field.oe_dirty + outline-color: red +.oe_translatable_text.oe_dirty:empty + padding: 0 10px +.oe_translatable_todo + background: rgb(255, 255, 182) +// }}} + /* ---- RTE ---- */ // bootstrap makes .btn elements unselectable -> RTE double-click can't know @@ -485,3 +501,5 @@ $navbar_height: 51px &.oe_ace_closed width: 0 opacity: 0 + +// vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker: diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index 41280199e45..8b15422b90d 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -282,7 +282,7 @@ }); }).get(); return $.when.apply(null, defs).then(function () { - window.location.href = window.location.href.replace(/unable_editor(=[^&]*)?|#.*/g, ''); + website.reload(); }); }, /** diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index f28a94c1800..bc0db10ce88 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -37,7 +37,9 @@ }); return $.when.apply(null, dones); }; - + website.reload = function () { + window.location.href = window.location.href.replace(/unable_editor(=[^&]*)?|#.*/g, ''); + }; var all_ready = null; var dom_ready = website.dom_ready = $.Deferred(); @@ -78,7 +80,7 @@ $pagination.last().before(col); }); - var page_start = page - parseInt(Math.floor((scope-1)/2)); + var page_start = page - parseInt(Math.floor((scope-1)/2), 10); if (page_start < 1 ) page_start = 1; var page_end = page_start + (scope-1); if (page_end > page_count ) page_end = page_count; @@ -106,10 +108,10 @@ */ website.ready = function() { if (!all_ready) { - all_ready = dom_ready.then(function () { - // TODO: load translations - return website.load_templates(templates); - }); + var tpl = website.load_templates(templates); + // var session; + // var trads = openerp._t.database.load_translations(session, ['website'], website.get_context().lang); + all_ready = $.when(dom_ready, tpl); } return all_ready; }; diff --git a/addons/website/static/src/js/website.translator.js b/addons/website/static/src/js/website.translator.js new file mode 100644 index 00000000000..14821af3072 --- /dev/null +++ b/addons/website/static/src/js/website.translator.js @@ -0,0 +1,192 @@ +(function () { + 'use strict'; + + var website = openerp.website; + website.templates.push('/website/static/src/xml/website.translator.xml'); + var nodialog = 'website_translator_nodialog'; + + website.EditorBar.include({ + start: function () { + var self = this; + this.initial_content = {}; + return this._super.apply(this, arguments).then(function () { + self.$('button[data-action=edit]').text("Translate"); + self.$('[data-action=snippet]').hide(); + self.$('#customize-menu-button').hide(); + }); + }, + edit: function () { + var self = this; + var mysuper = this._super; + if (!localStorage[nodialog]) { + var dialog = new website.TranslatorDialog(); + dialog.appendTo($(document.body)); + dialog.on('activate', this, function () { + localStorage[nodialog] = dialog.$('input[name=do_not_show]').prop('checked') || ''; + dialog.$el.modal('hide'); + this.translate(); + }); + } else { + this.translate().then(function () { + // Disable non translatable t-fields + $('[data-oe-type][data-oe-translate=0]').removeAttr('data-oe-type'); + mysuper.call(self); + }); + } + }, + translate: function () { + var self = this; + this.translations = null; + return openerp.jsonRpc('/website/get_view_translations', 'call', { + 'xml_id': $(document.documentElement).data('view-xmlid'), + 'lang': website.get_context().lang, + }).then(function (translations) { + self.translations = translations; + self.processTranslatableNodes(); + }); + }, + processTranslatableNodes: function () { + var self = this; + var $editables = $('[data-oe-model="ir.ui.view"]') + .not('link, script') + .not('.oe_snippets,.oe_snippet, .oe_snippet *') + .not('[data-oe-type]'); + + $editables.each(function () { + var $node = $(this); + var view_id = $node.attr('data-oe-source-id') || $node.attr('data-oe-id'); + self.transNode(this, view_id|0); + }); + $('.oe_translatable_text').on('paste', function () { + var node = $(this); + setTimeout(function () { + self.sanitizeNode(node); + }, 0); + }); + $(document).on('blur keyup paste', '.oe_translatable_text[contenteditable]', function(ev) { + var $node = $(this); + setTimeout(function () { + // Doing stuff next tick because paste and keyup events are + // fired before the content is changed + if (ev.type == 'paste') { + self.sanitizeNode($node[0]); + } + if (self.getInitialContent($node[0]) !== $node.text()) { + $node.addClass('oe_dirty').removeClass('oe_translatable_todo'); + } + }, 0); + }); + }, + getInitialContent: function (node) { + return this.initial_content[node.attributes['data-oe-nodeid'].value]; + }, + sanitizeNode: function (node) { + node.text(node.text()); + }, + isTextNode: function (node) { + return node.nodeType === 3 || node.nodeType === 4; + }, + isTranslatable: function (text) { + return text && _.str.trim(text) !== ''; + }, + markTranslatableNode: function (node, view_id) { + // TODO: link nodes with same content + node.className += ' oe_translatable_text'; + node.setAttribute('data-oe-translation-view-id', view_id); + var content = node.childNodes[0].data.trim(); + var trans = this.translations.filter(function (t) { + return t.res_id === view_id && t.value === content; + }); + if (trans.length) { + node.setAttribute('data-oe-translation-id', trans[0].id); + } else { + node.className += ' oe_translatable_todo'; + } + node.contentEditable = true; + var nid = _.uniqueId(); + $(node).attr('data-oe-nodeid', nid); + this.initial_content[nid] = content; + }, + save: function () { + var self = this; + var mysuper = this._super; + var trans = {}; + // this._super.apply(this, arguments); + $('.oe_translatable_text.oe_dirty').each(function () { + var $node = $(this); + var data = $node.data(); + if (!trans[data.oeTranslationViewId]) { + trans[data.oeTranslationViewId] = []; + } + trans[data.oeTranslationViewId].push({ + initial_content: self.getInitialContent(this), + new_content: $node.text(), + translation_id: data.oeTranslationId || null + }); + }); + openerp.jsonRpc('/website/set_translations', 'call', { + 'data': trans, + 'lang': website.get_context()['lang'], + }).then(function () { + mysuper.call(self); + }).fail(function () { + // TODO: bootstrap alert with error message + alert("Could not save translation"); + }); + }, + transNode: function (node, view_id) { + // Mostly handling text and cdata nodes here + // so avoid jquery usage in this function + if (node.attributes['data-oe-type']) { + if (node.attributes['data-oe-translate'].value == '1') { + node.className += ' oe_translatable_field'; + } + return; + } else if (node.childNodes.length === 1 + && this.isTextNode(node.childNodes[0]) + && !node.getAttribute('data-oe-model')) { + this.markTranslatableNode(node, view_id); + } else { + for (var i = 0, l = node.childNodes.length; i < l; i ++) { + var n = node.childNodes[i]; + if (this.isTextNode(n)) { + if (this.isTranslatable(n.data)) { + var container = document.createElement('span'); + node.insertBefore(container, n); + container.appendChild(n); + this.markTranslatableNode(container, view_id); + } + } else { + this.transNode(n, view_id); + } + } + } + }, + }); + + website.RTE.include({ + start: function () { + this._super.apply(this, arguments); + this.$el.hide(); + }, + fetch_editables: function (root) { + $(root).click(function (ev) { + ev.preventDefault(); + }); + return $('[data-oe-translate=1]'); + } + }); + + website.TranslatorDialog = openerp.Widget.extend({ + events: _.extend({}, website.EditorBar.prototype.events, { + 'hidden.bs.modal': 'destroy', + 'click button[data-action=activate]': function (ev) { + this.trigger('activate'); + }, + }), + template: 'website.TranslatorDialog', + start: function () { + this.$el.modal(); + }, + }); +})(); diff --git a/addons/website/static/src/xml/website.translator.xml b/addons/website/static/src/xml/website.translator.xml new file mode 100644 index 00000000000..f35cf583da9 --- /dev/null +++ b/addons/website/static/src/xml/website.translator.xml @@ -0,0 +1,53 @@ + + + + + + diff --git a/addons/website/static/src/xml/website.xml b/addons/website/static/src/xml/website.xml index 04c2403eda2..c2f7abe6ed2 100644 --- a/addons/website/static/src/xml/website.xml +++ b/addons/website/static/src/xml/website.xml @@ -22,7 +22,6 @@
  • Promote
  • -
  • Translate