This article is the third part of my Reading Odoo Source Code tutorial series. In the Reading Odoo Source Code tutorial series, I usually write about tips and tricks on how to solve a problem when creating an odoo module, by reading the odoo source code directly. With the hope that it can make us become independent programmers, without depending on the odoo forum or stackoverflow.
This article is not related to the first part or the second one, but I highly recommend you to read those two articles, I hope it can give you some inspiration.
The study case that we will discuss in this article is how to solve the translation (multi-language) that is only partially loaded on the portal/frontend page on odoo 14. More precisely, on the single portal/frontend page application which is separate from other odoo pages.
Before starting the discussion, please download the tutorial_multi_language module that I have prepared here. Install the module. Then open a new tab and enter the http://localhost:8069/customer-portal url. The view will look like the image below.
On this page there are 2 menus, it is the Sale menu and the Purchase menu. If we click on one of the menus, for example on the Sale menu, the Sale page will be rendered without reloading the whole page.
If we click the Save button then a message will be displayed in a dialog. Like in the image below.
In this article, we will discuss how to make the above page become multilingual. So the user can open the page in the language he usually uses. In this tutorial, I will use Korean in addition to English. You need to remember that I don’t speak Korean, the Korean language that appears in this article is the result of Google Translate, and I don’t have the capacity to make sure that the translation is correct or not.
To make the page that we create can be multilingual, there are several things that we need to pay attention to. That is :
1. Multi language in XML file
If we want the multi-language feature active in the XML file, what we need to do is to provide a translation file/record for each word/sentence in the XML file that we have created. If a word/sentence in the XML file has a translation, odoo will automatically replace the word/sentence with the translated version. For example, if the XML file contains the word of Quantity and in the odoo database there is a translation for the Quantity word, when we render the XML file in a url that uses Bahasa Indonesia, the Quantity word will be automatically replaced by Jumlah by odoo. If the word/sentence does not have a translation in the odoo database, the original word/sentence will be displayed.
2. Multi language in Python file
Multi-language in Python file is different from multi-language in the XML file, besides we have to provide the translation file/record for each word/sentence, we also have to manually mark the string data types variables which need to be translated by odoo. If we don’t mark these variables, odoo won’t translate these words/sentences. To mark these words/sentences, we can use the translation module provided by odoo. This is how to import the odoo translation module in a Python file.
from odoo import _
The module used to enable the multi-language feature in python files is the _ module. Meanwhile, to mark the string that need to be translated or not is to wrap it with the _ module, like in the code below.
menus = [ {'menu_id': 'sale', 'label': _('Sale')}, {'menu_id': 'purchase', 'label': _('Purchase')} ]
By wrapping the string in the _ module odoo will automatically translate the string if needed.
3. Multi language in JavaScript file
Same with the multi-language in Python file, if we want to use the multi-language feature in javascript, we must also wrap the word/sentence with the translation module. This is how to import the translation module and how to use it in a javascript file.
var core = require('web.core'); var _t = core._t; alert(_t('Good Morning'));
What we need to pay attention to is, if our javascript code is executed before the list of translation words is loaded, the above code will not run. So we have to pay attention to where we put those codes.
After we have wrapped all the words/sentences with the translation module, then we can prepare a translation file. We don’t need to prepare this file manually, because odoo already has a feature to generate this file. To do this, make sure you’re in developer mode, then enter the Settings >> Translations >> Export Translation menu. A popup will appear, fill in the Language field in the popup with the language you want to use. Then in the File Format field, I suggest selecting the PO File. Because I think this type of file is easier to edit. Then in the Apps to Export field option, select the name of the module/addon that you have created.
If the language that you want to use is not available, you must enable it first. To do this, make sure you are in developer mode, then enter the Settings >> Translations >> Languages menu. Then click the Activate button if the language that you want to use is not active yet.
Return to the translation file generating process. If you have selected the language, file type and, module/addon name, click the Export button, a popup will appear.
Click the link with the download icon, then you will get a file with the .po extension whose contents are something like this.
# Translation of Odoo Server. # This file contains the translation of the following modules: # * tutorial_multi_language # msgid "" msgstr "" "Project-Id-Version: Odoo Server 14.0+e\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-19 03:01+0000\n" "PO-Revision-Date: 2021-10-19 03:01+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" #. module: tutorial_multi_language #. openerp-web #: code:addonstutorial_multi_language/static/src/xml/templates.xml:0 #: code:addonstutorial_multi_language/static/src/xml/templates.xml:0 #, python-format msgid "Customer Name" msgstr ""
All you need to do is, open the .po file that you just downloaded with a text editor, then translate each word/sentence marked with the msgid keyword then insert your translated word/sentence into the double-quote marked with the msgstr keyword. Like in the code below.
#. module: tutorial_multi_language #. openerp-web #: code:addonstutorial_multi_language/static/src/xml/templates.xml:0 #: code:addonstutorial_multi_language/static/src/xml/templates.xml:0 #, python-format msgid "Customer Name" msgstr "ęł ę° ě´ëŚ"
You need to remember, sometimes there are cases where not every word/sentence in your module is included in the .po file that is generated automatically, or, sometimes odoo inserts the words/sentences that you know it doesn’t exist in your module. In addition, if the words/sentences in your module is a word/sentence that has been translated by another module, for example, the Save word, odoo will automatically translate this word and include it in the .po file, but it could be not the correct translation that you want. So, you should double-check this auto-generated file.
After translating each word/sentence in the .po file, create a folder with the name of i18n then place the .po file that you have translated into this folder. You don’t need to import the i18n folder in the __manifest__.py file. All you need to do is restart the odoo service then upgrade your module.
After the module upgrade process is complete, make sure the words/sentences that you have translated have been saved in the odoo database. To do this, please enter the Settings >> Translations >> Translated Term menu, then filter based on your module name, as shown in the image below.
Ok, now let’s discuss the real issue. At the beginning of this article, I have created a single page application on the odoo frontend page, at the http://localhost:8069/customer-portal address. If I want to open the page in the Korean language, all I need to do is add the URL_Code of Korean language, i.e. ko_KP for North Korea or ko_KR for South Korea (please correct me if I’m wrong), after the ip/domain/port of the odoo server, followed by the route of the controller that we want to access. So if I want to open the page in the North Korean language, I can type http://localhost:8069/ko_KP/customer-portal in the web browser address bar.
From the picture above, it can be seen that the page is already displayed in Korean. Now let’s try to open a menu, for example the Sale menu.
Well, this is the problem that we will try to solve. Why not all words/sentences on one page be translated by odoo. Why are the words/sentences in the xml file which rendered by the controller(python) just fine, but the words/sentences in the xml file which is dynamically rendered by javascript not translated by odoo.
The question is, where do we start the process to solve this problem?
This is the tip from me, if we have problems on the frontend, try to inspect the elements that you suspect in Google Chrome Developer Tools. Try looking for any class, id, or any attribute in the element that could be a clue about the problem we are facing. Before writing this article I did some research by doing this method, but couldn’t find anything that I could use as a guide to solving this problem. But it does not mean that this method is not effective. This method will be effective only in certain cases, for example in this article.
If the first tip doesn’t solve the problem, it’s time to use the second tip. It is by looking for any clue on the Network Tab of Google Chrome Developer Tools. I’ve also written how to use this tip in one of the Read the Odoo Source Code tutorial series. I highly recommend you to read the article.
When we open the Network Tab on Google Chrome Developer Tools, Google Chrome will display every action related to the communication between the web browser and the web server. Try to analyze each of these actions. This process does take some patience, but if you do it often, you will get used to it and will begin to understand what actions that odoo usually executes, so you will find certain patterns.
When I research with this method, this is what I found.
From the picture above, it can be seen that there is something wrong with the action with the http://localhost:8069/ko_KP/website/translations/3e3e3662962d53a0d7dce4b78680b64cceb942a3?mods=&lang=en_US url. In the web browser address bar, it is clear that the language I chose is Korean (ko_KP), in cookies the value of the frontend_lang parameter is also Korean, but why the lang parameter in the url filled with en_US?
We have to find out who is responsible for filling in the lang parameter above, by finding where the route /website/translations/ above is written, both in the controller and in the xml file, or the javascript that triggers the http://localhost:8069/ko_KP/website/translations/3e3e3662962d53a0d7dce4b78680b64cceb942a3?mods=&lang=en_US url above.
There are many methods to find out where a code is written, but the method that I like the most is to use the Linux built-in grep command. I’ve written how to use this command, please read here.
From the picture above, when I execute the grep command with the website/translations keyword, the word is found in 2 files in the http_routing module, the first one is in the controller with code below.
@http.route('/website/translations/<string:unique>', type='http', auth="public", website=True) def get_website_translations(self, unique, lang, mods=None): IrHttp = request.env['ir.http'].sudo() modules = IrHttp.get_translation_frontend_modules() if mods: modules += mods return WebClient().translations(unique, mods=','.join(modules), lang=lang)
And the second one is in the model with the code below.
@api.model def get_frontend_session_info(self): session_info = super(IrHttp, self).get_frontend_session_info() IrHttpModel = request.env['ir.http'].sudo() modules = IrHttpModel.get_translation_frontend_modules() user_context = request.session.get_context() if request.session.uid else {} lang = user_context.get('lang') translation_hash = request.env['ir.translation'].get_web_translations_hash(modules, lang) session_info.update({ 'translationURL': '/website/translations', 'cache_hashes': { 'translations': translation_hash, }, }) return session_info
Unfortunately I can’t find any xml or javascript files that trigger the http://localhost:8069/ko_KP/website/translations/3e3e3662962d53a0d7dce4b78680b64cceb942a3?mods=&lang=en_US action above. Finding the xml or javascript file that triggers this action is very important, because I believe the lang parameter with value of en_US is set in xml or javascript file dynamically.
So, let’s make the controller with the /website/translations route in the http_routing module above trigger an error, with the hope that we can get the stack info of what actions which triggered this controller on the frontend by forcing the error division by zero, like in the code below.
@http.route('/website/translations/<string:unique>', type='http', auth="public", website=True) def get_website_translations(self, unique, lang, mods=None): a = 8 / 0 IrHttp = request.env['ir.http'].sudo() modules = IrHttp.get_translation_frontend_modules() if mods: modules += mods return WebClient().translations(unique, mods=','.join(modules), lang=lang)
After we restart the odoo service, then refresh the web browser, when odoo executes the action to http://localhost:8069/ko_KP/website/translations/3e3e3662962d53a0d7dce4b78680b64cceb942a3?mods=&lang=en_US, odoo will trigger the internal server error, with the stack on the frontend as shown below.
Next, we just need to analyze each line of the stack info in the image above. But I have a suspicion on the 2 load_translations methods above. We can click directly on the file name and line number in the info above, to see the contents of the file. But because the contents of the javascript file have been minimized, they are difficult to read. Therefore I prefer to write the grep command in the linux terminal with that keyword. The result looks like the image below.
From the results of the grep command above, it can be seen that the load_translations keyword is exist in several javascript files in several modules. When this happens, try to check the files on the module that we know we have installed first. We can ignore the files in the module that we know we don’t have installed because the files will not be executed automatically by odoo. Furthermore, if we want to check the javascript file, try to check the file in the module with a name that begins with the web word, for example the web, web_enterprise, website, website_sale etc.
The first file that we should analyze is the web/static/src/js/core/translation.js file, where the code that contain the load_translations word is looks like this.
load_translations: function(session, modules, lang, url) { var self = this; var cacheId = session.cache_hashes && session.cache_hashes.translations; url = url || '/web/webclient/translations'; url += '/' + (cacheId ? cacheId : Date.now()); return $.get(url, { mods: modules ? modules.join(',') : null, lang: lang || null, }).then(function (trans) { self.set_bundle(trans); }); }
To find out if this is the right file or not, we can test it by adding/removing some code, for example by adding the console.log command. However, since this method only accepts the lang parameter, we can ignore this method. What we want to know is the method that manages the lang parameter with the value of en_US, not the method that accepts this input. So even though this method is the right load_translations method on the stack, as shown above, it is not the method we are looking for.
Next we can analyze the web/static/src/js/core/session.js file. This file has several lines of code with the load_translations keyword. But this file only has one method with the load_translations name, whose contents look like the code below.
load_translations: function () { /* We need to get the website lang at this level. The only way is to get it is to take the HTML tag lang Without it, we will always send undefined if there is no lang in the user_context. */ var html = document.documentElement, htmlLang = (html.getAttribute('lang') || 'en_US').replace('-', '_'), lang = this.user_context.lang || htmlLang; return _t.database.load_translations(this, this.module_list, lang, this.translationURL); },
It looks like this file is the correct one, i.e. the file that manages the lang parameter which passed in the http://localhost:8069/ko_KP/website/translations/3e3e3662962d53a0d7dce4b78680b64cceb942a3?mods=&lang=en_US url. To test it, let’s add a console.log command like in the code below.
load_translations: function () { /* We need to get the website lang at this level. The only way is to get it is to take the HTML tag lang Without it, we will always send undefined if there is no lang in the user_context. */ var html = document.documentElement, htmlLang = (html.getAttribute('lang') || 'en_US').replace('-', '_'), lang = this.user_context.lang || htmlLang; console.log("The loaded lang is ", lang) return _t.database.load_translations(this, this.module_list, lang, this.translationURL); },
On the console tab of Google Chrome Developer Tools, the web browser displays a message as shown below.
From the picture above, it can be recognized that the load_translations method in the web/static/src/js/core/session.js file is the method that responsible for managing the multi-language features on the odoo portal page. From the code above, the value of the lang parameter is taken from the html element with the lang attribute or from the user_context property. So, we just need to find out how to add a html element with lang attribute or add user_context dynamically.
But before doing it all, there is one tip that I want to share with you. Please always compare the source code on your computer/server with the source code on the odoo github page. Maybe the source code on your computer/server is too old and has bugs, while the source code on the github page has been fixed. When I tried to compare the contents of the web/static/src/js/core/session.js file with the files on github, I found this commit message.
It seems that the lang parameter that always has a value of en_US even though we have chosen another language is a bug. The contents of the load_translations method have been changed and become like this.
load_translations: function () { var lang = this.user_context.lang /* We need to get the website lang at this level. The only way is to get it is to take the HTML tag lang Without it, we will always send undefined if there is no lang in the user_context. */ var html = document.documentElement, htmlLang = html.getAttribute('lang'); if (!this.user_context.lang && htmlLang) { lang = htmlLang.replace('-', '_'); } return _t.database.load_translations(this, this.module_list, lang, this.translationURL); },
Now let’s apply the changes above to the files on our computer/server. You can do this manually or just execute the Git pull command.
After we remove the code that triggers the division by zero error, in the http_routing module, restart the odoo service, then refresh the web browser, the result was like this.
Now the lang parameter in the http://localhost:8069/ko_KP/website/translations/3e3e3662962d53a0d7dce4b78680b64cceb942a3?mods=&lang= url has no value. Let’s open one of the menus.
Now the Save button on the Sale menu has been translated, but the other words have not. It still doesn’t solve the problem, but at least it’s better than the previous one.
Next, we need to find out why the Save word is translated, while the other words are not. We have to find out when the translated word is loaded. And what method that handles it.
You need to know, when you see the list of words/sentences that have been translated in the Settings >> Translations >> Translated Term menu, we can see that the translated words/sentences is stored in the ir.translation model (please take a look at the address bar in your web browser). So we can override the search_read method and force it to tiggers the error division by zero, so we can get the stack info about the method, the files, and the module that triggers the search_read method. Please look at the code below.
class IrTranslation(models.Model): _inherit = "ir.translation" @api.model def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): res = super(IrTranslation, self).search_read(domain=domain, fields=fields, offset=offset, limit=limit, order=order) for r in res: if r.get('src','') == 'Save': a = 8 / 0 return res
The meaning of the code above is, if odoo calls the search_read method to search and read the translated word/sentence in the ir.translation model where the src field (original word/sentence) has value of Save, we will force odoo to trigger division by zero error. Why do we choose the Save word? Remember, in the last image the Save word has been translated. So we can make sure that when odoo reads the translation record of the Save word, the reading process must realated with the customer portal.
After we restart the odoo service, then refresh the customer portal page (remember, don’t refresh the odoo erp page, because there is a button with Save text too) the error message looks like below.
http: 500 Internal Server Error: Traceback (most recent call last): File "/odoo14/odoo14-server/odoo/addons/base/models/ir_http.py", line 237, in _dispatch result = request.dispatch() File "/odoo14/odoo14-server/odoo/http.py", line 806, in dispatch r = self._call_function(**self.params) File "/odoo14/odoo14-server/odoo/http.py", line 359, in _call_function return checked_call(self.db, *args, **kwargs) File "/odoo14/odoo14-server/odoo/service/model.py", line 94, in wrapper return f(dbname, *args, **kwargs) File "/odoo14/odoo14-server/odoo/http.py", line 347, in checked_call result = self.endpoint(*a, **kw) File "/odoo14/odoo14-server/odoo/http.py", line 912, in __call__ return self.method(*args, **kw) File "/odoo14/odoo14-server/odoo/http.py", line 531, in response_wrap response = f(*args, **kw) File "/odoo14/odoo14-server/addons/http_routing/controllers/main.py", line 17, in get_website_translations return WebClient().translations(unique, mods=','.join(modules), lang=lang) File "/odoo14/odoo14-server/odoo/http.py", line 531, in response_wrap response = f(*args, **kw) File "/odoo14/odoo14-server/addons/web/controllers/main.py", line 1030, in translations translations_per_module, lang_params = request.env["ir.translation"].get_translations_for_webclient(mods, lang) File "/odoo14/odoo14-server/odoo/addons/base/models/ir_translation.py", line 896, in get_translations_for_webclient ['module', 'src', 'value', 'lang'], order='module') File "/odoo14/custom/tutorial/tutorial_multi_language/models/models.py", line 21, in search_read a = 8 / 0 Exception
Now we have the stack info about what methods/files that triggers the search_read method in the ir.translation model. Let’s analyze them one by one.
First let’s analyze the get_translations_for_webclient method at the 896 line in the /odoo14/odoo14-server/odoo/addons/base/models/ir_translation.py file. Please see the contents of the file on odoo’s github page . Please take a look at the domain that passed to the search_read method, which looks like this.
[('module', 'in', mods), ('lang', '=', lang), ('comments', 'like', 'openerp-web'), ('value', '!=', False), ('value', '!=', '')],
From the above domain, it can be seen that the words/sentences that we have translated can be loaded by odoo, there are several conditions that must be met. The first condition is, the name of the module that we write must be in the list. Therefore, let’s check whether the module that I have created with the name of tutorial_multi_language is in the list or not, by adding the print command, like in the code below.
@api.model def get_translations_for_webclient(self, mods, lang): print(mods)
Where the results is like the list below.
['web', 'web_editor', 'portal', 'website_studio', 'website_sms', 'website', 'website_enterprise', 'website_mail', 'website_form', 'payment']
It turns out that the module I have created (tutorial_multi_language) is not on that list. Then what should we do to include this module in the list? First, we have to find out what method and what file that manages this list.
Let’s analyze the second stack, which is the translations method at the 1030 line in the /odoo14/odoo14-server/addons/web/controllers/main.py file. Please see the contents of the file on odoo’s github page. This method only accepts the list of modules that passed in the mods parameter and then passes it again to the get_translations_for_webclient method, so this method is not what we are looking for.
Now let’s analyze the third stack, which is the get_website_translations method at 17 line in the /odoo14/odoo14-server/addons/http_routing/controllers/main.py file. Please see the contents of the file on odoo’s github page.
I am sure this is the method that we’re looking for, the method that manages the list of modules that their translation record needs to be loaded on the portal. Look at the code below.
modules = IrHttp.get_translation_frontend_modules()
It turns out that the list of modules is acquired by execute the get_translation_frontend_modules method. Let’s grep with the get_translation_frontend_modules keyword to find out the contents of the method, so we can override it. This is the result on my computer.
Now let’s see the contents of the ir_http.py file in the portal module.
@classmethod def _get_translation_frontend_modules_name(cls): mods = super(IrHttp, cls)._get_translation_frontend_modules_name() return mods + ['portal']
It turns out that the module name must be added manually by overriding the _get_translation_frontend_modules_name method like in the portal module above. Ok, now in the same way, let’s add the name of the module that we have created.
class IrHttp(models.AbstractModel): _inherit = 'ir.http' @classmethod def _get_translation_frontend_modules_name(cls): mods = super(IrHttp, cls)._get_translation_frontend_modules_name() return mods + ['tutorial_multi_language']
Delete or comment the code that forces the division by zero error in the ir.translation model, restart the odoo service, then refresh the web browser.
Finally, the words/sentences that were untranslated previously, now translated correctly. Ok, now let’s test it by clicking the Save button to see the error message that loaded via ajax.
The error message has also been translated. That’s means all pages have been translated properly by odoo. Congratulation, we have solved the issue.
2 Replies to “Read Odoo Source Code : How to Solve the Translation that is Only Partially Loaded”
Great tutorial Sir đ ! I am really appreciated that you shared your experience in developing Odoo đ
Hello Sir! I am getting into trouble, maybe you could help? I would like to edit/modify/inherit the _rpc() method in web/static/src/js/core/service_mixin.js. I tried as following:
odoo.define( "mail_activity_chatter_done.mail_activitiy_chatter", function (require) { "use strict"; var MailActivity = require("mail.Activity"); var Dialog = require("web.Dialog"); var core = require("web.core"); var _t = core._t; var rpc = require("web.rpc"); var BasicModel = require("web.BasicModel"); var ServicesMixin = require("web.ServicesMixin"); ServicesMixin.include({ _rpc: function (params, options) { var query = rpc.buildQuery(params); console.log("hello world") var def = this.call('ajax', 'rpc', query.route, query.params, options, this) || $.Deferred(); var promise = def.promise(); var abort = (def.abort ? def.abort : def.reject) || function () {}; promise.abort = abort.bind(def); return promise; }, }) core.action_registry.add('') return {ServiceMixinExtend: ServiceMixinExtend}; });
But this method does not seem to be overwritten. Could you maybe give me any hint on how to overwrite this method?
Thank you in Advance