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 @@
+
+
+
+
+
+
+
+
+
+
+ You are about to enter the translation mode.
+
+
+ In this mode you can not change the structure of the document, you can only translate text.
+
+
+ Here are the visuals used in order to help you in your translation flow:
+
+ -
+ Normal translatable content
+
+ -
+ ERP translatable object data
+
+ -
+ Translation TODO
+
+
+
+
+
+
+
+
+
+
+
+
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
@@ -281,7 +284,7 @@
We'll do our best to get back to you as soon as possible.
diff --git a/addons/website/website.py b/addons/website/website.py
index bca9ed2ae26..70c0bea8fb0 100644
--- a/addons/website/website.py
+++ b/addons/website/website.py
@@ -7,22 +7,35 @@ from openerp.osv import osv, fields
from openerp.addons.web import http
from openerp.addons.web.http import request
import urllib
+from urlparse import urljoin
import math
import traceback
from openerp.tools.safe_eval import safe_eval
from openerp.exceptions import AccessError, AccessDenied
+import werkzeug
import logging
logger = logging.getLogger(__name__)
-def route(*route_args, **route_kwargs):
+def route(routes, *route_args, **route_kwargs):
def decorator(f):
- @http.route(*route_args, **route_kwargs)
+ new_routes = routes if isinstance(routes, list) else [routes]
+ f.multilang = route_kwargs.get('multilang', False)
+ if f.multilang:
+ route_kwargs.pop('multilang')
+ for r in list(new_routes):
+ new_routes.append('/' + r)
+ @http.route(new_routes, *route_args, **route_kwargs)
@functools.wraps(f, assigned=functools.WRAPPER_ASSIGNMENTS + ('func_name',))
def wrap(*args, **kwargs):
- request.route_lang = None # WIP: decorator will support lang argument
+ request.route_lang = kwargs.get('lang_code', None)
if not hasattr(request, 'website'):
+ request.multilang = f.multilang
request.website = request.registry['website'].get_current()
+ if request.route_lang:
+ lang_ok = [lg.code for lg in request.website.language_ids if lg.code == request.route_lang]
+ if not lang_ok:
+ return request.not_found()
request.website.preprocess_request(*args, **kwargs)
return f(*args, **kwargs)
return wrap
@@ -36,6 +49,19 @@ def auth_method_public():
request.uid = request.session.uid
http.auth_methods['public'] = auth_method_public
+def url_for(path, lang=None):
+ if request:
+ path = urljoin(request.httprequest.path, path)
+ langs = request.context.get('langs')
+ if path[0] == '/' and len(langs) > 1:
+ ps = path.split('/')
+ lang = lang or request.context.get('lang')
+ if ps[1] in langs:
+ ps[1] = lang
+ else:
+ ps.insert(1, lang)
+ path = '/'.join(ps)
+ return path
def urlplus(url, params):
if not params:
@@ -77,12 +103,22 @@ class website(osv.osv):
return lang
def preprocess_request(self, cr, uid, ids, *args, **kwargs):
+ def redirect(url):
+ return werkzeug.utils.redirect(url_for(url))
+ request.redirect = redirect
+
is_public_user = request.uid == self.get_public_user().id
+ lang = self.get_lang()
+ is_master_lang = lang == request.website.default_lang_id.code
request.context.update({
+ 'lang': lang,
+ 'langs': [lg.code for lg in request.website.language_ids],
+ 'multilang': request.multilang,
'is_public_user': is_public_user,
- 'editable': not is_public_user, # TODO: check perms
+ 'is_master_lang': is_master_lang,
+ 'editable': not is_public_user,
+ 'translatable': not is_public_user and not is_master_lang and request.multilang,
})
- request.context['lang'] = self.get_lang()
def get_current(self):
# WIP, currently hard coded
@@ -103,13 +139,15 @@ class website(osv.osv):
registry=request.registry,
json=simplejson,
website=request.website,
+ url_for=url_for,
res_company=request.website.company_id,
user_id=user.browse(cr, openerp.SUPERUSER_ID, uid),
)
- context = {
- 'inherit_branding': qweb_context.setdefault('editable', False),
- }
+ context = request.context.copy()
+ context.update(
+ inherit_branding=qweb_context.setdefault('editable', False),
+ )
# check if xmlid of the template exists
try:
diff --git a/addons/website/website_view.xml b/addons/website/website_view.xml
index aa31c0a85cb..99e91c0d0bc 100644
--- a/addons/website/website_view.xml
+++ b/addons/website/website_view.xml
@@ -45,7 +45,7 @@
-
+
diff --git a/addons/website_contract/views/website_contract.xml b/addons/website_contract/views/website_contract.xml
index 4863ddbafa2..ff30881a036 100644
--- a/addons/website_contract/views/website_contract.xml
+++ b/addons/website_contract/views/website_contract.xml
@@ -39,7 +39,7 @@