diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 0618cd367f3..6f21118e369 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -123,9 +123,9 @@ class AuthSignupHome(Home): raise UserError(_("The form was not properly filled in.")) if values.get('password') != qcontext.get('confirm_password'): raise UserError(_("Passwords do not match; please retype them.")) - supported_langs = [code for code, _ in request.env['res.lang'].get_installed()] - if request.lang in supported_langs: - values['lang'] = request.lang + supported_lang_codes = [code for code, _ in request.env['res.lang'].get_installed()] + if request.lang.code in supported_lang_codes: + values['lang'] = request.lang.code self._signup_with_values(qcontext.get('token'), values) request.env.cr.commit() diff --git a/addons/http_routing/__manifest__.py b/addons/http_routing/__manifest__.py index 6305f25310a..f4636332f45 100644 --- a/addons/http_routing/__manifest__.py +++ b/addons/http_routing/__manifest__.py @@ -11,6 +11,7 @@ Proposes advanced routing options not available in web or base to keep base modules simple. """, 'data': [ + 'views/res_lang_views.xml', ], 'depends': ['web'], } diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py index d6a4524eb31..c9d08f3c754 100644 --- a/addons/http_routing/models/ir_http.py +++ b/addons/http_routing/models/ir_http.py @@ -128,41 +128,58 @@ def unslug_url(s): # Language tools # ------------------------------------------------------------ -def url_for(path_or_uri, lang=None): - current_path = request.httprequest.path # should already be text +def url_for(path_or_uri, lang_code=None): + ''' Given a relative URL, make it absolute and add the required lang or + remove useless lang. + Nothing will be done for absolute URL. + If there is only one language installed, the lang will not be handled + unless forced with `lang` parameter. + + :param lang_code: Must be the lang `code`. It could also be something + else, such as `'[lang]'` (used for url_return). + ''' + Lang = request.env['res.lang'] location = pycompat.to_text(path_or_uri).strip() - force_lang = lang is not None + force_lang = lang_code is not None url = werkzeug.urls.url_parse(location) - + # relative URL with either a path or a force_lang if not url.netloc and not url.scheme and (url.path or force_lang): - location = werkzeug.urls.url_join(current_path, location) + location = werkzeug.urls.url_join(request.httprequest.path, location) + lang_url_codes = [url_code for _, url_code, _ in Lang.get_available()] + lang_code = pycompat.to_text(lang_code or request.context['lang']) + lang_url_code = Lang._lang_get(lang_code).url_code + lang_url_code = lang_url_code if lang_url_code in lang_url_codes else lang_code - lang = pycompat.to_text(lang or request.context.get('lang') or 'en_US') - langs = [lg[0] for lg in request.env['ir.http']._get_language_codes()] - - if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs): + if (len(lang_url_codes) > 1 or force_lang) and is_multilang_url(location, lang_url_codes): ps = location.split(u'/') - if ps[1] in langs: + default_lg = request.env['ir.http']._get_default_lang() + if ps[1] in lang_url_codes: # Replace the language only if we explicitly provide a language to url_for if force_lang: - ps[1] = lang + ps[1] = lang_url_code # Remove the default language unless it's explicitly provided - elif ps[1] == request.env['ir.http']._get_default_lang().code: + elif ps[1] == default_lg.url_code: ps.pop(1) # Insert the context language or the provided language - elif lang != request.env['ir.http']._get_default_lang().code or force_lang: - ps.insert(1, lang) + elif lang_url_code != default_lg.url_code or force_lang: + ps.insert(1, lang_url_code) location = u'/'.join(ps) return location -def is_multilang_url(local_url, langs=None): - if not langs: - langs = [lg[0] for lg in request.env['ir.http']._get_language_codes()] +def is_multilang_url(local_url, lang_url_codes=None): + ''' Check if the given URL content is supposed to be translated. + To be considered as translatable, the URL should either: + 1. Match a POST (non-GET actually) controller that is `website=True` and + either `multilang` specified to True or if not specified, with `type='http'`. + 2. If not matching 1., everything not under /static/ will be translatable + ''' + if not lang_url_codes: + lang_url_codes = [url_code for _, url_code, _ in request.env['res.lang'].get_available()] spath = local_url.split('/') # if a language is already in the path, remove it - if spath[1] in langs: + if spath[1] in lang_url_codes: spath.pop(1) local_url = '/'.join(spath) try: @@ -222,10 +239,6 @@ class IrHttp(models.AbstractModel): model=ModelConverter, ) - @classmethod - def _get_language_codes(cls): - return request.env['res.lang'].get_installed() - @classmethod def _get_default_lang(cls): lang_code = request.env['ir.default'].sudo().get('res.partner', 'lang') @@ -285,13 +298,17 @@ class IrHttp(models.AbstractModel): return any(bot in user_agent.encode('ascii', 'ignore') for bot in cls.bots) @classmethod - def get_nearest_lang(cls, lang): - # Try to find a similar lang. Eg: fr_BE and fr_FR - short = lang.partition('_')[0] + def get_nearest_lang(cls, lang_code): + """ Try to find a similar lang. Eg: fr_BE and fr_FR + :param lang_code: the lang `code` (en_US) + """ short_match = False - for code, dummy in cls._get_language_codes(): - if code == lang: - return lang + if not lang_code: + return short_match + short = lang_code.partition('_')[0] + for code in [code for code, _, _ in request.env['res.lang'].get_available()]: + if code == lang_code: + return code if not short_match and code.startswith(short): short_match = code return short_match @@ -317,19 +334,28 @@ class IrHttp(models.AbstractModel): @classmethod def _add_dispatch_parameters(cls, func): + Lang = request.env['res.lang'] # only called for is_frontend request if request.routing_iteration == 1: context = dict(request.context) path = request.httprequest.path.split('/') - langs = [code for code, _ in request.env['res.lang'].get_installed()] is_a_bot = cls.is_a_bot() - cook_lang = request.httprequest.cookies.get('frontend_lang') - nearest_lang = not func and cls.get_nearest_lang(path[1]) - preferred_lang = ((cook_lang if cook_lang in langs else False) - or (not is_a_bot and cls.get_nearest_lang(request.lang)) - or cls._get_default_lang().code) - request.lang = context['lang'] = nearest_lang or preferred_lang + available_langs = Lang.get_available() + lang_codes = [code for code, _, _ in available_langs] + nearest_lang = not func and cls.get_nearest_lang(Lang._lang_get_code(path[1])) + cook_lang = request.httprequest.cookies.get('frontend_lang') + cook_lang = cook_lang in lang_codes and cook_lang + + if nearest_lang: + lang = Lang._lang_get(nearest_lang) + else: + nearest_ctx_lg = not is_a_bot and cls.get_nearest_lang(request.env.context['lang']) + preferred_lang = Lang._lang_get(cook_lang or nearest_ctx_lg) or cls._get_default_lang() + lang = preferred_lang + + request.lang = lang + context['lang'] = lang.code # bind modified context request.context = context @@ -380,34 +406,34 @@ class IrHttp(models.AbstractModel): return cls._handle_exception(e) # For website routes (only), add website params on `request` - cook_lang = request.httprequest.cookies.get('frontend_lang') if request.is_frontend: request.redirect = lambda url, code=302: werkzeug.utils.redirect(url_for(url), code) cls._add_dispatch_parameters(func) path = request.httprequest.path.split('/') + default_lg_id = cls._get_default_lang() if request.routing_iteration == 1: is_a_bot = cls.is_a_bot() - nearest_lang = not func and cls.get_nearest_lang(path[1]) + nearest_lang = not func and cls.get_nearest_lang(request.env['res.lang']._lang_get_code(path[1])) url_lang = nearest_lang and path[1] # if lang in url but not the displayed or default language --> change or remove # or no lang in url, and lang to dispay not the default language --> add lang # and not a POST request # and not a bot or bot but default lang in url - if ((url_lang and (url_lang != request.lang or url_lang == cls._get_default_lang().code)) - or (not url_lang and request.is_frontend_multilang and request.lang != cls._get_default_lang().code) + if ((url_lang and (url_lang != request.lang.url_code or url_lang == default_lg_id.url_code)) + or (not url_lang and request.is_frontend_multilang and request.lang != default_lg_id) and request.httprequest.method != 'POST') \ - and (not is_a_bot or (url_lang and url_lang == cls._get_default_lang().code)): + and (not is_a_bot or (url_lang and url_lang == default_lg_id.url_code)): if url_lang: path.pop(1) - if request.lang != cls._get_default_lang().code: - path.insert(1, request.lang) + if request.lang != default_lg_id: + path.insert(1, request.lang.url_code) path = '/'.join(path) or '/' routing_error = None redirect = request.redirect(path + '?' + request.httprequest.query_string.decode('utf-8')) - redirect.set_cookie('frontend_lang', request.lang) + redirect.set_cookie('frontend_lang', request.lang.code) return redirect elif url_lang: request.uid = None @@ -415,7 +441,7 @@ class IrHttp(models.AbstractModel): routing_error = None return cls.reroute('/'.join(path) or '/') - if request.lang == cls._get_default_lang().code: + if request.lang == default_lg_id: context = dict(request.context) context['edit_translations'] = False request.context = context @@ -426,8 +452,9 @@ class IrHttp(models.AbstractModel): # removed cache for auth public result = super(IrHttp, cls)._dispatch() - if request.is_frontend and cook_lang != request.lang and hasattr(result, 'set_cookie'): - result.set_cookie('frontend_lang', request.lang) + cook_lang = request.httprequest.cookies.get('frontend_lang') + if request.is_frontend and cook_lang != request.lang.code and hasattr(result, 'set_cookie'): + result.set_cookie('frontend_lang', request.lang.code) return result @@ -463,8 +490,8 @@ class IrHttp(models.AbstractModel): generated_path = werkzeug.url_unquote_plus(path) current_path = werkzeug.url_unquote_plus(request.httprequest.path) if generated_path != current_path: - if request.lang != cls._get_default_lang().code: - path = '/' + request.lang + path + if request.lang != cls._get_default_lang(): + path = '/' + request.lang.url_code + path if request.httprequest.query_string: path += '?' + request.httprequest.query_string.decode('utf-8') return werkzeug.utils.redirect(path, code=301) diff --git a/addons/http_routing/models/res_lang.py b/addons/http_routing/models/res_lang.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/addons/http_routing/views/res_lang_views.xml b/addons/http_routing/views/res_lang_views.xml new file mode 100644 index 00000000000..96d03217f69 --- /dev/null +++ b/addons/http_routing/views/res_lang_views.xml @@ -0,0 +1,24 @@ + + + + res.lang.form.http_routing.inherit + res.lang + + + + 0 + + + + + + res.lang.tree.model.inherit + res.lang + + + + 0 + + + + diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 591849a8bc0..dfe575b8f6f 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -762,7 +762,9 @@ class WebClient(http.Controller): # For performance reasons we only load a single translation, so for # sub-languages (that should only be partially translated) we load the # main language PO instead - that should be enough for the login screen. - lang = request.lang.split('_')[0] + context = dict(request.context) + request.session._fix_lang(context) + lang = context['lang'].split('_')[0] translations_per_module = {} for addon_name in mods: diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index a1cca57919f..f91870adfaf 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -107,10 +107,14 @@ class Website(Home): # Business # ------------------------------------------------------ + @http.route('/website/get_languages', type='json', auth="user", website=True) + def website_languages(self, **kwargs): + return [(lg.code, lg.url_code, lg.name) for lg in request.website.language_ids] + @http.route('/website/lang/', type='http', auth="public", website=True, multilang=False) def change_lang(self, lang, r='/', **kwargs): if lang == 'default': - lang = request.website.default_lang_code + lang = request.website.default_lang_id.url_code r = '/%s%s' % (lang, r or '/') redirect = werkzeug.utils.redirect(r or ('/%s' % lang), 303) redirect.set_cookie('frontend_lang', lang) diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 58c15a8128c..ecacdc62bac 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -15,7 +15,7 @@ import odoo from odoo import api, models, registry from odoo import SUPERUSER_ID from odoo.http import request -from odoo.tools import config +from odoo.tools import config, ormcache from odoo.tools.safe_eval import safe_eval from odoo.osv.expression import FALSE_DOMAIN, OR @@ -132,18 +132,6 @@ class Http(models.AbstractModel): if request.routing_iteration == 1: request.website = request.website.with_context(request.context) - @classmethod - def _get_languages(cls): - if getattr(request, 'website', False): - return request.website.language_ids - return super(Http, cls)._get_languages() - - @classmethod - def _get_language_codes(cls): - if getattr(request, 'website', False): - return request.website._get_languages() - return super(Http, cls)._get_language_codes() - @classmethod def _get_default_lang(cls): if getattr(request, 'website', False): @@ -205,7 +193,7 @@ class Http(models.AbstractModel): @classmethod def _handle_exception(cls, exception): code = 500 # default code - is_website_request = bool(getattr(request, 'is_frontend', False) and getattr(request, 'website', False)) + is_website_request = bool(getattr(request, 'is_frontend', False) and get_request_website()) if not is_website_request: # Don't touch non website requests exception handling return super(Http, cls)._handle_exception(exception) diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index 0e93876be45..d3483fcb1b4 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -4,9 +4,10 @@ import re from collections import OrderedDict from odoo import models -from odoo.addons.http_routing.models.ir_http import url_for from odoo.http import request from odoo.addons.base.models.assetsbundle import AssetsBundle +from odoo.addons.http_routing.models.ir_http import url_for +from odoo.addons.website.models import ir_http from odoo.tools import html_escape as escape re_background_image = re.compile(r"(background-image\s*:\s*url\(\s*['\"]?\s*)([^)'\"]+)") @@ -47,7 +48,7 @@ class QWeb(models.AbstractModel): options.get('edit_translations') or options.get('debug') or (request and request.session.debug): return atts - website = request and getattr(request, 'website', None) + website = ir_http.get_request_website() if not website and options.get('website_id'): website = self.env['website'].browse(options['website_id']) diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 6949f173a46..6b1efe5a717 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -302,7 +302,7 @@ class View(models.Model): if request and getattr(request, 'is_frontend', False): editable = request.website.is_publisher() - translatable = editable and self._context.get('lang') != request.website.default_lang_code + translatable = editable and self._context.get('lang') != request.website.default_lang_id.code editable = not translatable and editable # in edit mode ir.ui.view will tag nodes @@ -355,7 +355,7 @@ class View(models.Model): url_for=url_for, res_company=request.website.company_id.sudo(), default_lang_code=request.env['ir.http']._get_default_lang().code, - languages=request.env['ir.http']._get_language_codes(), + languages=request.env['res.lang'].get_available(), translatable=translatable, editable=editable, menu_data=self.env['ir.ui.menu'].load_menus_root() if request.website.is_user() else None, @@ -367,7 +367,7 @@ class View(models.Model): def get_default_lang_code(self): website_id = self.env.context.get('website_id') if website_id: - lang_code = self.env['website'].browse(website_id).default_lang_code + lang_code = self.env['website'].browse(website_id).default_lang_id.code return lang_code else: return super(View, self).get_default_lang_code() diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py index 6bcae68b61f..0fff2f6eb3b 100644 --- a/addons/website/models/res_config_settings.py +++ b/addons/website/models/res_config_settings.py @@ -19,15 +19,11 @@ class ResConfigSettings(models.TransientModel): website_country_group_ids = fields.Many2many(related='website_id.country_group_ids', readonly=False) website_company_id = fields.Many2one(related='website_id.company_id', string='Website Company', readonly=False) website_logo = fields.Binary(related='website_id.logo', readonly=False) - language_ids = fields.Many2many(related='website_id.language_ids', relation='res.lang', - readonly=False) + language_ids = fields.Many2many(related='website_id.language_ids', relation='res.lang', readonly=False) website_language_count = fields.Integer(string='Number of languages', compute='_compute_website_language_count', readonly=True) - website_default_lang_id = fields.Many2one( - string='Default language', related='website_id.default_lang_id', readonly=False, - relation='res.lang') - website_default_lang_code = fields.Char( - 'Default language code', related='website_id.default_lang_code', readonly=False, - ) + website_default_lang_id = fields.Many2one(string='Default language', related='website_id.default_lang_id', + readonly=False, relation='res.lang') + website_default_lang_code = fields.Char('Default language code', related='website_id.default_lang_id.code', readonly=False) specific_user_account = fields.Boolean(related='website_id.specific_user_account', readonly=False, help='Are newly created user accounts website specific') diff --git a/addons/website/models/res_lang.py b/addons/website/models/res_lang.py index c84a212ed5a..9fc3f69e998 100644 --- a/addons/website/models/res_lang.py +++ b/addons/website/models/res_lang.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models, _ +from odoo import api, models, tools, _ +from odoo.addons.website.models import ir_http from odoo.exceptions import UserError +from odoo.http import request class Lang(models.Model): @@ -13,3 +15,12 @@ class Lang(models.Model): if self.env['website'].search([('language_ids', 'in', self._ids)]): raise UserError(_("Cannot deactivate a language that is currently used on a website.")) return super(Lang, self).write(vals) + + @api.model + @tools.ormcache_context(keys=("website_id",)) + def get_available(self): + """ Return the available languages as a list of (code, name) sorted by name. """ + website = ir_http.get_request_website() + if website: + return sorted([(lang.code, lang.url_code, lang.name) for lang in request.website.language_ids]) + return super(Lang, self).get_available() diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 8ce3d46568a..29398c35872 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -56,7 +56,6 @@ class Website(models.Model): company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.company, required=True) language_ids = fields.Many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages', default=_active_languages) default_lang_id = fields.Many2one('res.lang', string="Default Language", default=_default_language, required=True) - default_lang_code = fields.Char("Default language code", related='default_lang_id.code', store=True, readonly=False) auto_redirect_lang = fields.Boolean('Autoredirect Language', default=True, help="Should users be redirected to their browser's language") def _default_social_facebook(self): @@ -156,7 +155,8 @@ class Website(models.Model): public_user_to_change_websites = self.env['website'] self._handle_favicon(values) - self._get_languages.clear_cache(self) + self.clear_caches() + if 'company_id' in values and 'user_id' not in values: public_user_to_change_websites = self.filtered(lambda w: w.sudo().user_id.company_id.id != values['company_id']) if public_user_to_change_websites: @@ -437,19 +437,11 @@ class Website(models.Model): # Languages # ---------------------------------------------------------- - def get_languages(self): - self.ensure_one() - return self._get_languages() - - @tools.cache('self.id') - def _get_languages(self): - return [(lg.code, lg.name) for lg in self.language_ids] - def get_alternate_languages(self, req=None): langs = [] if req is None: req = request.httprequest - default = self.get_current_website().default_lang_code + default = self.get_current_website().default_lang_id.url_code shorts = [] def get_url_localized(router, lang): @@ -460,11 +452,11 @@ class Website(models.Model): return router.build(request.endpoint, arguments) router = request.httprequest.app.get_db_router(request.db).bind('') - for code, dummy in self.get_languages(): - lg_path = ('/' + code) if code != default else '' - lg_codes = code.split('_') + for lg in self.language_ids: + lg_path = ('/' + lg.url_code) if lg.url_code != default else '' + lg_codes = lg.code.split('_') shorts.append(lg_codes[0]) - uri = get_url_localized(router, code) if request.endpoint else request.httprequest.path + uri = get_url_localized(router, lg.url_code) if request.endpoint else request.httprequest.path if req.query_string: uri += u'?' + req.query_string.decode('utf-8') lang = { diff --git a/addons/website/models/website_visitor.py b/addons/website/models/website_visitor.py index 9b262c26031..307ba256214 100644 --- a/addons/website/models/website_visitor.py +++ b/addons/website/models/website_visitor.py @@ -122,10 +122,9 @@ class WebsiteVisitor(models.Model): def _create_visitor(self, website_page_id=False): country_code = request.session.get('geoip', {}).get('country_code', False) country_id = request.env['res.country'].sudo().search([('code', '=', country_code)], limit=1).id if country_code else False - lang_id = request.env['res.lang'].sudo().search([('code', '=', request.lang)], limit=1).id vals = { 'last_connection_datetime': datetime.now(), - 'lang_id': lang_id, + 'lang_id': request.lang.id, 'country_id': country_id, 'website_id': request.website.id } diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js index 4b61a33d779..a8e208f1408 100644 --- a/addons/website/static/src/js/content/website_root.js +++ b/addons/website/static/src/js/content/website_root.js @@ -105,7 +105,7 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({ var $target = $(ev.target); // retrieve the hash before the redirect var redirect = { - lang: $target.data('lang'), + lang: $target.data('url_code'), url: encodeURIComponent($target.attr('href').replace(/[&?]edit_translations[^&?]+/, '')), hash: encodeURIComponent(window.location.hash) }; diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js index 3844a1d4731..2ac6d7fcded 100644 --- a/addons/website/static/src/js/menu/seo.js +++ b/addons/website/static/src/js/menu/seo.js @@ -497,9 +497,7 @@ var MetaKeywords = Widget.extend({ }, }); this._rpc({ - model: 'website', - method: 'get_languages', - args: [[context.website_id]], + route: '/website/get_languages', }).then(function (data) { self.$('#language-box').html(core.qweb.render('Configurator.language_promote', { 'language': data, diff --git a/addons/website/static/src/js/menu/translate.js b/addons/website/static/src/js/menu/translate.js index 043a900fff6..cab37bf3473 100644 --- a/addons/website/static/src/js/menu/translate.js +++ b/addons/website/static/src/js/menu/translate.js @@ -46,14 +46,11 @@ var TranslatePageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ * @returns {Promise} */ _goToMasterPage: function () { - var lang = '/' + utils.get_cookie('frontend_lang'); - var current = document.createElement('a'); current.href = window.location.toString(); current.search += (current.search ? '&' : '?') + 'enable_editor=1'; - if (current.pathname.indexOf(lang) === 0) { - current.pathname = current.pathname.replace(lang, ''); - } + // we are in translate mode, the pathname starts with '/ - + diff --git a/addons/website/static/tests/tours/rte.js b/addons/website/static/tests/tours/rte.js index a57b282bd09..a5cd53eaf61 100644 --- a/addons/website/static/tests/tours/rte.js +++ b/addons/website/static/tests/tours/rte.js @@ -32,7 +32,7 @@ tour.register('rte_translator', { timeout: 60000, }, { content: "go to english version", - trigger: '.js_language_selector a[data-lang="en_US"]', + trigger: '.js_language_selector a[data-url_code="en"]', extra_trigger: 'html[lang*="fr"]', }, { content: "Open new page menu", @@ -73,7 +73,7 @@ tour.register('rte_translator', { extra_trigger: 'body:not(.o_wait_reload):not(:has(.note-editor))', }, { content: "click on french version", - trigger: '.js_language_selector a[data-lang="fr_BE"]', + trigger: '.js_language_selector a[data-url_code="fr_BE"]', extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))', }, { content: "translate", @@ -137,7 +137,7 @@ tour.register('rte_translator', { extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))', }, { content: "return to english version", - trigger: '.js_language_selector a[data-lang="en_US"]', + trigger: '.js_language_selector a[data-url_code="en"]', }, { content: "edit english version", trigger: 'a[data-action=edit]', @@ -170,7 +170,7 @@ tour.register('rte_translator', { trigger : '.js_language_selector .dropdown-toggle', }, { content: "return in french", - trigger : 'html[lang="en-US"] .js_language_selector .js_change_lang[data-lang="fr_BE"]', + trigger : 'html[lang="en-US"] .js_language_selector .js_change_lang[data-url_code="fr_BE"]', }, { content: "check bis: content is translated", trigger: '#wrap p font:first:contains(translated french text)', diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py index dc24bd6e22a..9bb3efd60de 100644 --- a/addons/website/tests/__init__.py +++ b/addons/website/tests/__init__.py @@ -1,15 +1,16 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import test_converter from . import test_base_url -from . import test_get_current_website -from . import test_qweb +from . import test_converter from . import test_crawl -from . import test_ui -from . import test_views +from . import test_get_current_website +from . import test_lang_url from . import test_menu from . import test_page +from . import test_qweb +from . import test_ui +from . import test_views from . import test_website_favicon from . import test_website_reset_password from . import test_website_visitor diff --git a/addons/website/tests/test_lang_url.py b/addons/website/tests/test_lang_url.py new file mode 100644 index 00000000000..f4998168074 --- /dev/null +++ b/addons/website/tests/test_lang_url.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.http_routing.models.ir_http import url_for +from odoo.addons.website.tools import MockRequest +from odoo.tests import HttpCase, tagged + + +@tagged('-at_install', 'post_install') +class TestLangUrl(HttpCase): + def setUp(self): + super(TestLangUrl, self).setUp() + + # Simulate multi lang without loading translations + self.website = self.env['website'].browse(1) + lang_fr = self.env.ref('base.lang_fr') + lang_fr.write({'active': True, 'url_code': 'fr'}) + self.website.language_ids = self.env.ref('base.lang_en') + lang_fr + self.website.default_lang_id = self.env.ref('base.lang_en') + + def test_01_url_for(self): + with MockRequest(self.env, website=self.website): + self.assertEqual(url_for('', '[lang]'), '/[lang]/hello/', "`[lang]` is used to be replaced in the url_return after installing a language, it should not be replaced or removed.") + + def test_02_url_redirect(self): + url = '/fr_WHATEVER/contactus' + r = self.url_open(url) + self.assertEqual(r.status_code, 200) + self.assertTrue(r.url.endswith('/fr/contactus'), "fr_WHATEVER should be forwarded to 'fr_FR' lang as closest match") + + url = '/fr_FR/contactus' + r = self.url_open(url) + self.assertEqual(r.status_code, 200) + self.assertTrue(r.url.endswith('/fr/contactus'), "lang in url should use url_code ('fr' in this case)") diff --git a/addons/website/tests/test_qweb.py b/addons/website/tests/test_qweb.py index 9386c9f81e3..9cc199291a9 100644 --- a/addons/website/tests/test_qweb.py +++ b/addons/website/tests/test_qweb.py @@ -77,7 +77,7 @@ class TestQwebProcessAtt(TransactionCase): self.website.default_lang_id = self.env.ref('base.lang_en') self.website.cdn_activated = True self.website.cdn_url = "http://test.cdn" - self.website.cdn_filters = "\n".join(["^(/[a-z]{2}_[A-Z]{2})?/a$", "^/b$"]) + self.website.cdn_filters = "\n".join(["^(/[a-z]{2}_[A-Z]{2})?/a$", "^(/[a-z]{2})?/a$", "^/b$"]) def _test_att(self, url, expect, tag='a', attribute='href'): self.assertEqual( @@ -88,8 +88,8 @@ class TestQwebProcessAtt(TransactionCase): def test_process_att_no_request(self): # no request so no URL rewriting self._test_att('/', {'href': '/'}) - self._test_att('/en_US/', {'href': '/en_US/'}) - self._test_att('/fr_FR/', {'href': '/fr_FR/'}) + self._test_att('/en/', {'href': '/en/'}) + self._test_att('/fr/', {'href': '/fr/'}) # no URL rewritting for CDN self._test_att('/a', {'href': '/a'}) @@ -97,8 +97,8 @@ class TestQwebProcessAtt(TransactionCase): with MockRequest(self.env): # no website so URL rewriting self._test_att('/', {'href': '/'}) - self._test_att('/en_US/', {'href': '/en_US/'}) - self._test_att('/fr_FR/', {'href': '/fr_FR/'}) + self._test_att('/en/', {'href': '/en/'}) + self._test_att('/fr/', {'href': '/fr/'}) # no URL rewritting for CDN self._test_att('/a', {'href': '/a'}) @@ -106,37 +106,37 @@ class TestQwebProcessAtt(TransactionCase): with MockRequest(self.env, website=self.website, multilang=False): # lang not changed in URL but CDN enabled self._test_att('/a', {'href': 'http://test.cdn/a'}) - self._test_att('/en_US/a', {'href': 'http://test.cdn/en_US/a'}) + self._test_att('/en/a', {'href': 'http://test.cdn/en/a'}) self._test_att('/b', {'href': 'http://test.cdn/b'}) - self._test_att('/en_US/b', {'href': '/en_US/b'}) + self._test_att('/en/b', {'href': '/en/b'}) def test_process_att_no_request_lang(self): with MockRequest(self.env, website=self.website): self._test_att('/', {'href': '/'}) - self._test_att('/en_US/', {'href': '/'}) - self._test_att('/fr_FR/', {'href': '/fr_FR/'}) + self._test_att('/en/', {'href': '/'}) + self._test_att('/fr/', {'href': '/fr/'}) def test_process_att_with_request_lang(self): with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}): - self._test_att('/', {'href': '/fr_FR/'}) - self._test_att('/en_US/', {'href': '/'}) - self._test_att('/fr_FR/', {'href': '/fr_FR/'}) + self._test_att('/', {'href': '/fr/'}) + self._test_att('/en/', {'href': '/'}) + self._test_att('/fr/', {'href': '/fr/'}) def test_process_att_matching_cdn_and_lang(self): with MockRequest(self.env, website=self.website): # lang prefix is added before CDN self._test_att('/a', {'href': 'http://test.cdn/a'}) - self._test_att('/en_US/a', {'href': 'http://test.cdn/a'}) - self._test_att('/fr_FR/a', {'href': 'http://test.cdn/fr_FR/a'}) + self._test_att('/en/a', {'href': 'http://test.cdn/a'}) + self._test_att('/fr/a', {'href': 'http://test.cdn/fr/a'}) self._test_att('/b', {'href': 'http://test.cdn/b'}) - self._test_att('/en_US/b', {'href': 'http://test.cdn/b'}) - self._test_att('/fr_FR/b', {'href': '/fr_FR/b'}) + self._test_att('/en/b', {'href': 'http://test.cdn/b'}) + self._test_att('/fr/b', {'href': '/fr/b'}) def test_process_att_no_route(self): with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}, routing=False): # default on multilang=True if route is not /{module}/static/ self._test_att('/web/static/hi', {'href': '/web/static/hi'}) - self._test_att('/my-page', {'href': '/fr_FR/my-page'}) + self._test_att('/my-page', {'href': '/fr/my-page'}) def test_process_att_url_crap(self): with MockRequest(self.env, website=self.website) as request: diff --git a/addons/website/tools.py b/addons/website/tools.py index e3bb897ec52..e7058d65541 100644 --- a/addons/website/tools.py +++ b/addons/website/tools.py @@ -84,14 +84,24 @@ class MockRequest(object): app.get_db_router = app.bind = app.match = app if not kw.get('routing', True): app.match = werkzeugRaiseNotFound + + lang = kw.get('lang') + if not lang: + lang_code = kw.get('context', {}).get('lang', env.context.get('lang', 'en_US')) + lang = env['res.lang']._lang_get(lang_code) + + context = kw.get('context', {}) + context.setdefault('lang', lang_code) + self.request = DotDict({ - 'context': kw.get('context', {}), + 'context': context, 'db': None, 'env': env, 'httprequest': { 'path': '/hello/', 'app': app, }, + 'lang': lang, 'redirect': werkzeug.utils.redirect, 'session': { 'geoip': { diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 312d1ce701c..b89cb0721b4 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -227,7 +227,6 @@ - @@ -395,16 +394,15 @@
  • diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 97a9e11169e..9412ddce2be 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -649,7 +649,7 @@ class WebsiteSale(http.Controller): if mode[0] == 'new': new_values['company_id'] = request.website.company_id.id - lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None + lang = request.lang.code if request.lang.code in request.website.mapped('language_ids.code') else None if lang: new_values['lang'] = lang if mode == ('edit', 'billing') and order.partner_id.type == 'contact': diff --git a/addons/website_sale_stock/models/product_product.py b/addons/website_sale_stock/models/product_product.py index 07a873fa1a7..8c33b40d895 100644 --- a/addons/website_sale_stock/models/product_product.py +++ b/addons/website_sale_stock/models/product_product.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models -from odoo.http import request +from odoo.addons.website.models import ir_http class ProductProduct(models.Model): @@ -11,7 +11,7 @@ class ProductProduct(models.Model): cart_qty = fields.Integer(compute='_compute_cart_qty') def _compute_cart_qty(self): - website = request and getattr(request, 'website', None) + website = ir_http.get_request_website() if not website: self.cart_qty = 0 return diff --git a/odoo/addons/base/models/res_lang.py b/odoo/addons/base/models/res_lang.py index 22c6221b5c2..eccb6b09c93 100644 --- a/odoo/addons/base/models/res_lang.py +++ b/odoo/addons/base/models/res_lang.py @@ -28,6 +28,7 @@ class Lang(models.Model): name = fields.Char(required=True) code = fields.Char(string='Locale Code', required=True, help='This field is used to set/get locales for user') iso_code = fields.Char(string='ISO code', help='This ISO code is the name of po files to use for translations') + url_code = fields.Char('URL Code', required=True, help='The Lang Code displayed in the URL') active = fields.Boolean() direction = fields.Selection([('ltr', 'Left-to-Right'), ('rtl', 'Right-to-Left')], required=True, default='ltr') date_format = fields.Char(string='Date Format', required=True, default=DEFAULT_DATE_FORMAT) @@ -50,6 +51,7 @@ class Lang(models.Model): _sql_constraints = [ ('name_uniq', 'unique(name)', 'The name of the language must be unique !'), ('code_uniq', 'unique(code)', 'The code of the language must be unique !'), + ('url_code_uniq', 'unique(url_code)', 'The URL code of the language must be unique !'), ] @api.constrains('active') @@ -181,6 +183,10 @@ class Lang(models.Model): def _lang_get_id(self, code): return self.with_context(active_test=True).search([('code', '=', code)]).id + @tools.ormcache('url_code') + def _lang_get_code(self, url_code): + return self.with_context(active_test=True).search([('url_code', '=', url_code)]).code or url_code + def _lang_get(self, code): """ Return the language using this code if it is active """ return self.browse(self._lang_get_id(code)) @@ -198,7 +204,7 @@ class Lang(models.Model): def get_available(self): """ Return the available languages as a list of (code, name) sorted by name. """ langs = self.with_context(active_test=False).search([]) - return sorted([(lang.code, lang.name) for lang in langs], key=itemgetter(1)) + return sorted([(lang.code, lang.url_code, lang.name) for lang in langs], key=itemgetter(2)) @api.model @tools.ormcache() @@ -218,6 +224,9 @@ class Lang(models.Model): @api.model_create_multi def create(self, vals_list): self.clear_caches() + for vals in vals_list: + if not vals.get('url_code'): + vals['url_code'] = vals.get('iso_code') or vals['code'] return super(Lang, self).create(vals_list) def write(self, vals): diff --git a/odoo/addons/base/views/res_lang_views.xml b/odoo/addons/base/views/res_lang_views.xml index 2f7b4990e90..483f2c1879f 100644 --- a/odoo/addons/base/views/res_lang_views.xml +++ b/odoo/addons/base/views/res_lang_views.xml @@ -9,6 +9,7 @@ +