translate: implement a counter to export the code line number
[odoo/odoo.git] / bin / tools / translate.py
index 0178985..8cf09f9 100644 (file)
@@ -1,40 +1,45 @@
-# -*- encoding: utf-8 -*-
+# -*- coding: utf-8 -*-
 ##############################################################################
 #
-#    OpenERP, Open Source Management Solution  
-#    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
-#    $Id$
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
 #
 #    This program is free software: you can redistribute it and/or modify
-#    it under the terms of the GNU General Public License as published by
-#    the Free Software Foundation, either version 3 of the License, or
-#    (at your option) any later version.
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
 #
 #    This program is distributed in the hope that it will be useful,
 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#    GNU General Public License for more details.
+#    GNU Affero General Public License for more details.
 #
-#    You should have received a copy of the GNU General Public License
+#    You should have received a copy of the GNU Affero General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
 
+import codecs
+import csv
+import fnmatch
+import inspect
+import itertools
+import locale
 import os
+import pooler
+import re
+import logging
+import tarfile
+import tempfile
+import threading
 from os.path import join
-import fnmatch
-import csv, xml.dom, re
-import tools, pooler
-from osv.orm import BrowseRecordError
-import ir
+
+from datetime import datetime
+from lxml import etree
+
+import tools
 import netsvc
 from tools.misc import UpdateableStr
-import inspect
-import mx.DateTime as mxdt
-import tempfile
-import tarfile
-import codecs
-import locale
 
 _LOCALE2WIN32 = {
     'af_ZA': 'Afrikaans_South Africa',
@@ -93,7 +98,27 @@ _LOCALE2WIN32 = {
     'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
     'sk_SK': 'Slovak_Slovakia',
     'sl_SI': 'Slovenian_Slovenia',
+    #should find more specific locales for spanish countries,
+    #but better than nothing
+    'es_AR': 'Spanish_Spain',
+    'es_BO': 'Spanish_Spain',
+    'es_CL': 'Spanish_Spain',
+    'es_CO': 'Spanish_Spain',
+    'es_CR': 'Spanish_Spain',
+    'es_DO': 'Spanish_Spain',
+    'es_EC': 'Spanish_Spain',
     'es_ES': 'Spanish_Spain',
+    'es_GT': 'Spanish_Spain',
+    'es_HN': 'Spanish_Spain',
+    'es_MX': 'Spanish_Spain',
+    'es_NI': 'Spanish_Spain',
+    'es_PA': 'Spanish_Spain',
+    'es_PE': 'Spanish_Spain',
+    'es_PR': 'Spanish_Spain',
+    'es_PY': 'Spanish_Spain',
+    'es_SV': 'Spanish_Spain',
+    'es_UY': 'Spanish_Spain',
+    'es_VE': 'Spanish_Spain',
     'sv_SE': 'Swedish_Sweden',
     'ta_IN': 'English_Australia',
     'th_TH': 'Thai_Thailand',
@@ -101,6 +126,8 @@ _LOCALE2WIN32 = {
     'tr_TR': 'Turkish_Turkey',
     'uk_UA': 'Ukrainian_Ukraine',
     'vi_VN': 'Vietnamese_Viet Nam',
+    'tlh_TLH': 'Klingon',
+
 }
 
 
@@ -123,36 +150,113 @@ def translate(cr, name, source_type, lang, source=None):
     res = res_trans and res_trans[0] or False
     return res
 
+logger = logging.getLogger('translate')
+
 class GettextAlias(object):
+
+    def _get_db(self):
+        # find current DB based on thread/worker db name (see netsvc)
+        db_name = getattr(threading.currentThread(), 'dbname', None)
+        if db_name:
+            return pooler.get_db_only(db_name)
+
+    def _get_cr(self, frame):
+        is_new_cr = False
+        cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
+        if not cr:
+            s = frame.f_locals.get('self', {})
+            cr = getattr(s, 'cr', None)
+        if not cr:
+            db = self._get_db()
+            if db:
+                cr = db.cursor()
+                is_new_cr = True
+        return cr, is_new_cr
+
+    def _get_lang(self, frame):
+        lang = None
+        ctx = frame.f_locals.get('context')
+        if not ctx:
+            kwargs = frame.f_locals.get('kwargs')
+            if kwargs is None:
+                args = frame.f_locals.get('args')
+                if args and isinstance(args, (list, tuple)) \
+                        and isinstance(args[-1], dict):
+                    ctx = args[-1]
+            elif isinstance(kwargs, dict):
+                ctx = kwargs.get('context')
+        if ctx:
+            lang = ctx.get('lang')
+        if not lang:
+            s = frame.f_locals.get('self', {})
+            c = getattr(s, 'localcontext', None)
+            if c:
+                lang = c.get('lang')
+        return lang
+
     def __call__(self, source):
+        res = source
+        cr = None
+        is_new_cr = False
         try:
-            frame = inspect.stack()[1][0]
-        except:
-            return source
-
-        cr = frame.f_locals.get('cr')
-       try:
-               lang = (frame.f_locals.get('context') or {}).get('lang', False)
-               if not (lang and cr):
-                       return source
-       except:
-               return source
-
-        cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, 'code', source))
-        res_trans = cr.fetchone()
-        return res_trans and res_trans[0] or source
+            frame = inspect.currentframe()
+            if frame is None:
+                return source
+            frame = frame.f_back
+            if not frame:
+                return source
+            lang = self._get_lang(frame)
+            if lang:
+                cr, is_new_cr = self._get_cr(frame)
+                if cr:
+                    # Try to use ir.translation to benefit from global cache if possible
+                    pool = pooler.get_pool(cr.dbname)
+                    res = pool.get('ir.translation')._get_source(cr, 1, None, ('code','sql_constraint'), lang, source)
+                else:
+                    logger.debug('no context cursor detected, skipping translation for "%r"', source)
+            else:
+                logger.debug('no translation language detected, skipping translation for "%r" ', source)
+        except Exception:
+            logger.debug('translation went wrong for "%r", skipped', source)
+                # if so, double-check the root/base translations filenames
+        finally:
+            if cr and is_new_cr:
+                cr.close()
+        return res
+
 _ = GettextAlias()
 
 
+def quote(s):
+    """Returns quoted PO term string, with special PO characters escaped"""
+    assert r"\n" not in s, "Translation terms may not include escaped newlines ('\\n'), please use only literal newlines! (in '%s')" % s
+    return '"%s"' % s.replace('\\','\\\\') \
+                     .replace('"','\\"') \
+                     .replace('\n', '\\n"\n"')
+
+re_escaped_char = re.compile(r"(\\.)")
+re_escaped_replacements = {'n': '\n', }
+
+def _sub_replacement(match_obj):
+    return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])
+
+def unquote(str):
+    """Returns unquoted PO term string, with special PO characters unescaped"""
+    return re_escaped_char.sub(_sub_replacement, str[1:-1])
+
 # class to handle po files
 class TinyPoFile(object):
     def __init__(self, buffer):
+        self.logger = logging.getLogger('i18n')
         self.buffer = buffer
 
+    def warn(self, msg, *args):
+        self.logger.warning(msg, *args)
+
     def __iter__(self):
         self.buffer.seek(0)
         self.lines = self._get_lines()
-       self.lines_count = len(self.lines);
+        self.lines_count = len(self.lines);
 
         self.first = True
         self.tnrs= []
@@ -168,48 +272,45 @@ class TinyPoFile(object):
         return lines
 
     def cur_line(self):
-       return (self.lines_count - len(self.lines))
+        return (self.lines_count - len(self.lines))
 
     def next(self):
-        def unquote(str):
-            return str[1:-1].replace("\\n", "\n")   \
-                            .replace('\\"', '"')
-
         type = name = res_id = source = trad = None
 
         if self.tnrs:
             type, name, res_id, source, trad = self.tnrs.pop(0)
+            if not res_id:
+                res_id = '0'
         else:
             tmp_tnrs = []
             line = None
-           fuzzy = False
+            fuzzy = False
             while (not line):
                 if 0 == len(self.lines):
                     raise StopIteration()
                 line = self.lines.pop(0).strip()
-
             while line.startswith('#'):
-               if line.startswith('#~ '):
-                       break
+                if line.startswith('#~ '):
+                    break
                 if line.startswith('#:'):
-                   if ' ' in line[2:].strip():
-                       for lpart in line[2:].strip().split(' '):
-                               tmp_tnrs.append(lpart.strip().split(':',2))
-                   else:
+                    if ' ' in line[2:].strip():
+                        for lpart in line[2:].strip().split(' '):
+                            tmp_tnrs.append(lpart.strip().split(':',2))
+                    else:
                         tmp_tnrs.append( line[2:].strip().split(':',2) )
-               elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
-                       fuzzy = True
+                elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
+                    fuzzy = True
                 line = self.lines.pop(0).strip()
             while not line:
                 # allow empty lines between comments and msgid
                 line = self.lines.pop(0).strip()
-           if line.startswith('#~ '):
-               while line.startswith('#~ ') or not line.strip():
-                   if 0 == len(self.lines):
+            if line.startswith('#~ '):
+                while line.startswith('#~ ') or not line.strip():
+                    if 0 == len(self.lines):
                         raise StopIteration()
-                   line = self.lines.pop(0)
-               # This has been a deprecated entry, don't return anything
-               return self.next()
+                    line = self.lines.pop(0)
+                # This has been a deprecated entry, don't return anything
+                return self.next()
 
             if not line.startswith('msgid'):
                 raise Exception("malformed file: bad line: %s" % line)
@@ -242,9 +343,12 @@ class TinyPoFile(object):
                     self.tnrs.append((t, n, r, source, trad))
 
         self.first = False
-       
-       if name == None:
-               return self.next()
+
+        if name is None:
+            if not fuzzy:
+                self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s', 
+                        self.cur_line(), source[:30])
+            return self.next()
         return type, name, res_id, source, trad
 
     def write_infos(self, modules):
@@ -271,15 +375,11 @@ class TinyPoFile(object):
                               'version': release.version,
                               'modules': reduce(lambda s, m: s + "#\t* %s\n" % m, modules, ""),
                               'bugmail': release.support_email,
-                              'now': mxdt.ISO.strUTC(mxdt.ISO.DateTime.utc()),
+                              'now': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')+"+0000",
                             }
                           )
 
     def write(self, modules, tnrs, source, trad):
-        def quote(s):
-            return '"%s"' % s.replace('"','\\"') \
-                             .replace('\n', '\\n"\n"') \
-                            .replace(' \\ ',' \\\\ ')
 
         plurial = len(modules) > 1 and 's' or ''
         self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
@@ -337,7 +437,9 @@ def trans_export(lang, modules, buffer, format, dbname=None):
             rows_by_module = {}
             for row in rows:
                 module = row[0]
-                rows_by_module.setdefault(module, []).append(row)
+                # first row is the "header", as in csv, it will be popped
+                rows_by_module.setdefault(module, [['module', 'type', 'name', 'res_id', 'src', ''],])
+                rows_by_module[module].append(row)
 
             tmpdir = tempfile.mkdtemp()
             for mod, modrows in rows_by_module.items():
@@ -359,6 +461,9 @@ def trans_export(lang, modules, buffer, format, dbname=None):
     if newlang:
         lang = 'en_US'
     trans = trans_generate(lang, modules, dbname)
+    if newlang and format!='csv':
+        for trx in trans:
+            trx[-1] = ''
     modules = set([t[0] for t in trans[1:]])
     _process(format, modules, trans, buffer, lang, newlang)
     del trans
@@ -366,10 +471,10 @@ def trans_export(lang, modules, buffer, format, dbname=None):
 
 def trans_parse_xsl(de):
     res = []
-    for n in [i for i in de.childNodes if (i.nodeType == i.ELEMENT_NODE)]:
-        if n.hasAttribute("t"):
-            for m in [j for j in n.childNodes if (j.nodeType == j.TEXT_NODE)]:
-                l = m.data.strip().replace('\n',' ')
+    for n in de:
+        if n.get("t"):
+            for m in [j for j in n if j.text]:
+                l = m.text.strip().replace('\n',' ')
                 if len(l):
                     res.append(l.encode("utf8"))
         res.extend(trans_parse_xsl(n))
@@ -377,9 +482,9 @@ def trans_parse_xsl(de):
 
 def trans_parse_rml(de):
     res = []
-    for n in [i for i in de.childNodes if (i.nodeType == i.ELEMENT_NODE)]:
-        for m in [j for j in n.childNodes if (j.nodeType == j.TEXT_NODE)]:
-            string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.data)]
+    for n in de:
+        for m in [j for j in n if j.text]:
+            string_list = [s.replace('\n', ' ').strip() for s in re.split('\[\[.+?\]\]', m.text)]
             for s in string_list:
                 if s:
                     res.append(s.encode("utf8"))
@@ -388,15 +493,16 @@ def trans_parse_rml(de):
 
 def trans_parse_view(de):
     res = []
-    if de.hasAttribute("string"):
-        s = de.getAttribute('string')
-        if s:
-            res.append(s.encode("utf8"))
-    if de.hasAttribute("sum"):
-        s = de.getAttribute('sum')
-        if s:
-            res.append(s.encode("utf8"))
-    for n in [i for i in de.childNodes if (i.nodeType == i.ELEMENT_NODE)]:
+    if de.tag == 'attribute' and de.get("name") == 'string':
+        if de.text:
+            res.append(de.text.encode("utf8"))
+    if de.get("string"):
+        res.append(de.get('string').encode("utf8"))
+    if de.get("sum"):
+        res.append(de.get('sum').encode("utf8"))
+    if de.get("confirm"):
+        res.append(de.get('confirm').encode("utf8"))
+    for n in de:
         res.extend(trans_parse_view(n))
     return res
 
@@ -415,7 +521,7 @@ def in_modules(object_name, modules):
     return module in modules
 
 def trans_generate(lang, modules, dbname=None):
-    logger = netsvc.Logger()
+    logger = logging.getLogger('i18n')
     if not dbname:
         dbname=tools.config['db_name']
         if not modules:
@@ -431,13 +537,21 @@ def trans_generate(lang, modules, dbname=None):
 
     query = 'SELECT name, model, res_id, module'    \
             '  FROM ir_model_data'
-    query_param = None
+            
+    query_models = """SELECT m.id, m.model, imd.module 
+            FROM ir_model AS m, ir_model_data AS imd 
+            WHERE m.id = imd.res_id AND imd.model = 'ir.model' """
+
     if 'all_installed' in modules:
         query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
-    elif not 'all' in modules:
-        query += ' WHERE module IN (%s)' % ','.join(['%s']*len(modules))
-        query_param = modules
+        query_models += " AND imd.module in ( SELECT name FROM ir_module_module WHERE state = 'installed') "
+    query_param = None
+    if 'all' not in modules:
+        query += ' WHERE module IN %s'
+        query_models += ' AND imd.module in %s'
+        query_param = (tuple(modules),)
     query += ' ORDER BY module, model, name'
+    query_models += ' ORDER BY module, model'
 
     cr.execute(query, query_param)
 
@@ -446,7 +560,7 @@ def trans_generate(lang, modules, dbname=None):
         tuple = (module, source, name, id, type)
         if source and tuple not in _to_translate:
             _to_translate.append(tuple)
-    
+
     def encode(s):
         if isinstance(s, unicode):
             return s.encode('utf8')
@@ -458,30 +572,30 @@ def trans_generate(lang, modules, dbname=None):
         xml_name = "%s.%s" % (module, encode(xml_name))
 
         if not pool.get(model):
-            logger.notifyChannel("db", netsvc.LOG_ERROR, "unable to find object %r" % (model,))
+            logger.error("Unable to find object %r", model)
             continue
-        
-        try:
-            obj = pool.get(model).browse(cr, uid, res_id)
-        except BrowseRecordError:
-            logger.notifyChannel("db", netsvc.LOG_ERROR, "unable to find object %r with id %d" % (model, res_id))
+
+        exists = pool.get(model).exists(cr, uid, res_id)
+        if not exists:
+            logger.warning("Unable to find object %r with id %d", model, res_id)
             continue
+        obj = pool.get(model).browse(cr, uid, res_id)
 
         if model=='ir.ui.view':
-            d = xml.dom.minidom.parseString(encode(obj.arch))
-            for t in trans_parse_view(d.documentElement):
+            d = etree.XML(encode(obj.arch))
+            for t in trans_parse_view(d):
                 push_translation(module, 'view', encode(obj.model), 0, t)
         elif model=='ir.actions.wizard':
             service_name = 'wizard.'+encode(obj.wiz_name)
-            if netsvc.SERVICES.get(service_name):
-                obj2 = netsvc.SERVICES[service_name]
+            if netsvc.Service._services.get(service_name):
+                obj2 = netsvc.Service._services[service_name]
                 for state_name, state_def in obj2.states.iteritems():
                     if 'result' in state_def:
                         result = state_def['result']
                         if result['type'] != 'form':
                             continue
                         name = "%s,%s" % (encode(obj.wiz_name), state_name)
-                        
+
                         def_params = {
                             'string': ('wizard_field', lambda s: [encode(s)]),
                             'selection': ('selection', lambda s: [encode(e[1]) for e in ((not callable(s)) and s or [])]),
@@ -489,9 +603,12 @@ def trans_generate(lang, modules, dbname=None):
                         }
 
                         # export fields
+                        if not result.has_key('fields'):
+                            logger.warning("res has no fields: %r", result)
+                            continue
                         for field_name, field_def in result['fields'].iteritems():
                             res_name = name + ',' + field_name
-                           
+
                             for fn in def_params:
                                 if fn in field_def:
                                     transtype, modifier = def_params[fn]
@@ -501,8 +618,8 @@ def trans_generate(lang, modules, dbname=None):
                         # export arch
                         arch = result['arch']
                         if arch and not isinstance(arch, UpdateableStr):
-                            d = xml.dom.minidom.parseString(arch)
-                            for t in trans_parse_view(d.documentElement):
+                            d = etree.XML(arch)
+                            for t in trans_parse_view(d):
                                 push_translation(module, 'wizard_view', name, 0, t)
 
                         # export button labels
@@ -513,11 +630,11 @@ def trans_generate(lang, modules, dbname=None):
                             push_translation(module, 'wizard_button', res_name, 0, button_label)
 
         elif model=='ir.model.fields':
-           try:
+            try:
                 field_name = encode(obj.name)
-           except AttributeError, exc:
-               logger.notifyChannel("db", netsvc.LOG_ERROR, "name error in %s: %s" % (xml_name,str(exc)))
-               continue
+            except AttributeError, exc:
+                logger.error("name error in %s: %s", xml_name, str(exc))
+                continue
             objmodel = pool.get(obj.model)
             if not objmodel or not field_name in objmodel._columns:
                 continue
@@ -544,7 +661,7 @@ def trans_generate(lang, modules, dbname=None):
                         push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
 
             if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
-                for key, val in field_def.selection:
+                for dummy, val in field_def.selection:
                     push_translation(module, 'selection', name, 0, encode(val))
 
         elif model=='ir.actions.report.xml':
@@ -553,49 +670,69 @@ def trans_generate(lang, modules, dbname=None):
             if obj.report_rml:
                 fname = obj.report_rml
                 parse_func = trans_parse_rml
-                report_type = "rml"
+                report_type = "report"
             elif obj.report_xsl:
                 fname = obj.report_xsl
                 parse_func = trans_parse_xsl
                 report_type = "xsl"
-            try:
-                xmlstr = tools.file_open(fname).read()
-                d = xml.dom.minidom.parseString(xmlstr)
-                for t in parse_func(d.documentElement):
-                    push_translation(module, report_type, name, 0, t)
-            except IOError, xml.dom.expatbuilder.expat.ExpatError:
-                if fname:
-                    logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't export translation for report %s %s %s" % (name, report_type, fname))
-
-        for constraint in pool.get(model)._constraints:
-            msg = constraint[1]
-            push_translation(module, 'constraint', model, 0, encode(msg))
-
-        for field_name,field_def in pool.get(model)._columns.items():
+            if fname and obj.report_type in ('pdf', 'xsl'):
+                try:
+                    d = etree.parse(tools.file_open(fname))
+                    for t in parse_func(d.iter()):
+                        push_translation(module, report_type, name, 0, t)
+                except (IOError, etree.XMLSyntaxError):
+                    logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
+
+        for field_name,field_def in obj._table._columns.items():
             if field_def.translate:
                 name = model + "," + field_name
-               try:
+                try:
                     trad = getattr(obj, field_name) or ''
-               except:
-                   trad = ''
+                except:
+                    trad = ''
                 push_translation(module, 'model', name, xml_name, encode(trad))
 
+        # End of data for ir.model.data query results
+
+    cr.execute(query_models, query_param)
+
+    def push_constraint_msg(module, term_type, model, msg):
+        # Check presence of __call__ directly instead of using
+        # callable() because it will be deprecated as of Python 3.0
+        if not hasattr(msg, '__call__'):
+            push_translation(module, term_type, model, 0, encode(msg))
+
+    for (model_id, model, module) in cr.fetchall():
+        module = encode(module)
+        model = encode(model)
+
+        model_obj = pool.get(model)
+
+        if not model_obj:
+            logging.getLogger("i18n").error("Unable to find object %r", model)
+            continue
+
+        for constraint in getattr(model_obj, '_constraints', []):
+            push_constraint_msg(module, 'constraint', model, constraint[1])
+
+        for constraint in getattr(model_obj, '_sql_constraints', []):
+            push_constraint_msg(module, 'sql_constraint', model, constraint[2])
+
     # parse source code for _() calls
-    def get_module_from_path(path,mod_paths=None):
-       if not mod_paths:
-               # First, construct a list of possible paths
-               def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons'))     # default addons path (base)
-               ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
-               mod_paths=[def_path]
-               for adp in ad_paths:
-                       mod_paths.append(adp)
-                       if not adp.startswith('/'):
-                               mod_paths.append(os.path.join(def_path,adp))
-                       elif adp.startswith(def_path):
-                               mod_paths.append(adp[len(def_path)+1:])
-       
-       for mp in mod_paths:
-           if path.startswith(mp) and (os.path.dirname(path) != mp):
+    def get_module_from_path(path, mod_paths=None):
+        if not mod_paths:
+            # First, construct a list of possible paths
+            def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons'))     # default addons path (base)
+            ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
+            mod_paths=[def_path]
+            for adp in ad_paths:
+                mod_paths.append(adp)
+                if not os.path.isabs(adp):
+                    mod_paths.append(adp)
+                elif adp.startswith(def_path):
+                    mod_paths.append(adp[len(def_path)+1:])
+        for mp in mod_paths:
+            if path.startswith(mp) and (os.path.dirname(path) != mp):
                 path = path[len(mp)+1:]
                 return path.split(os.path.sep)[0]
         return 'base'   # files that are not in a module are considered as being in 'base' module
@@ -603,28 +740,79 @@ def trans_generate(lang, modules, dbname=None):
     modobj = pool.get('ir.module.module')
     installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
     installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
-    
-    if tools.config['root_path'] in tools.config['addons_path'] :
-        path_list = [tools.config['root_path']]
+
+    root_path = os.path.join(tools.config['root_path'], 'addons')
+
+    apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
+    if root_path in apaths:
+        path_list = apaths
     else :
-        path_list = [tools.config['root_path'],tools.config['addons_path']]
-
-    for path in path_list: 
-        for root, dirs, files in tools.osutil.walksymlinks(path):
-            for fname in fnmatch.filter(files, '*.py'):
-                fabsolutepath = join(root, fname)
-                frelativepath = fabsolutepath[len(path):]
-                module = get_module_from_path(frelativepath)
-                is_mod_installed = module in installed_modules
-                if (('all' in modules) or (module in modules)) and is_mod_installed:
-                    code_string = tools.file_open(fabsolutepath, subdir='').read()
-                    iter = re.finditer('[^a-zA-Z0-9_]_\([\s]*["\'](.+?)["\'][\s]*\)',
-                        code_string, re.S)
-                    
-                    if module in installed_modules : 
-                        frelativepath =str("addons"+frelativepath)
-                    for i in iter:
-                        push_translation(module, 'code', frelativepath, 0, encode(i.group(1)))
+        path_list = [root_path,] + apaths
+    
+    # Also scan these non-addon paths
+    for bin_path in ['osv', 'report' ]:
+        path_list.append(os.path.join(tools.config['root_path'], bin_path))
+
+    logger.debug("Scanning modules at paths: ", path_list)
+
+    mod_paths = []
+    join_dquotes = re.compile(r'([^\\])"[\s\\]*"', re.DOTALL)
+    join_quotes = re.compile(r'([^\\])\'[\s\\]*\'', re.DOTALL)
+    re_dquotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*"(.+?)"[\s]*?\)', re.DOTALL)
+    re_quotes = re.compile(r'[^a-zA-Z0-9_]_\([\s]*\'(.+?)\'[\s]*?\)', re.DOTALL)
+
+    def export_code_terms_from_file(fname, path, root, terms_type):
+        fabsolutepath = join(root, fname)
+        frelativepath = fabsolutepath[len(path):]
+        module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
+        is_mod_installed = module in installed_modules
+        if (('all' in modules) or (module in modules)) and is_mod_installed:
+            logger.debug("Scanning code of %s at module: %s", frelativepath, module)
+            code_string = tools.file_open(fabsolutepath, subdir='').read()
+            if module in installed_modules:
+                frelativepath = str("addons" + frelativepath)
+            ite = re_dquotes.finditer(code_string)
+            code_offset = 0
+            code_line = 1
+            for i in ite:
+                src = i.group(1)
+                if src.startswith('""'):
+                    assert src.endswith('""'), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
+                    src = src[2:-2]
+                else:
+                    src = join_dquotes.sub(r'\1', src)
+                # try to count the lines from the last pos to our place:
+                code_line += code_string[code_offset:i.start(1)].count('\n')
+                # now, since we did a binary read of a python source file, we
+                # have to expand pythonic escapes like the interpreter does.
+                src = src.decode('string_escape')
+                push_translation(module, terms_type, frelativepath, code_line, encode(src))
+                code_line += i.group(1).count('\n')
+                code_offset = i.end() # we have counted newlines up to the match end
+
+            ite = re_quotes.finditer(code_string)
+            code_offset = 0 #reset counters
+            code_line = 1
+            for i in ite:
+                src = i.group(1)
+                if src.startswith("''"):
+                    assert src.endswith("''"), "Incorrect usage of _(..) function (should contain only literal strings!) in file %s near: %s" % (frelativepath, src[:30])
+                    src = src[2:-2]
+                else:
+                    src = join_quotes.sub(r'\1', src)
+                code_line += code_string[code_offset:i.start(1)].count('\n')
+                src = src.decode('string_escape')
+                push_translation(module, terms_type, frelativepath, code_line, encode(src))
+                code_line += i.group(1).count('\n')
+                code_offset = i.end() # we have counted newlines up to the match end
+
+    for path in path_list:
+        logger.debug("Scanning files of modules at %s", path)
+        for root, dummy, files in tools.osutil.walksymlinks(path):
+            for fname in itertools.chain(fnmatch.filter(files, '*.py')):
+                export_code_terms_from_file(fname, path, root, 'code')
+            for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
+                export_code_terms_from_file(fname, path, root, 'report')
 
 
     out = [["module","type","name","res_id","src","value"]] # header
@@ -633,36 +821,40 @@ def trans_generate(lang, modules, dbname=None):
     for module, source, name, id, type in _to_translate:
         trans = trans_obj._get_source(cr, uid, name, type, lang, source)
         out.append([module, type, name, id, source, encode(trans) or ''])
-    
+
     cr.close()
     return out
 
-def trans_load(db_name, filename, lang, strict=False, verbose=True):
-    logger = netsvc.Logger()
+def trans_load(db_name, filename, lang, verbose=True, context=None):
+    logger = logging.getLogger('i18n')
     try:
         fileobj = open(filename,'r')
+        logger.info("loading %s", filename)
         fileformat = os.path.splitext(filename)[-1][1:].lower()
-        r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose)
+        r = trans_load_data(db_name, fileobj, fileformat, lang, verbose=verbose, context=context)
         fileobj.close()
         return r
     except IOError:
         if verbose:
-            logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,)) 
+            logger.error("couldn't read translation file %s", filename)
         return None
 
-def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True):
-    logger = netsvc.Logger()
+def trans_load_data(db_name, fileobj, fileformat, lang, lang_name=None, verbose=True, context=None):
+    logger = logging.getLogger('i18n')
     if verbose:
-        logger.notifyChannel("i18n", netsvc.LOG_INFO, 'loading translation file for language %s' % (lang))
+        logger.info('loading translation file for language %s', lang)
+    if context is None:
+        context = {}
     pool = pooler.get_pool(db_name)
     lang_obj = pool.get('res.lang')
     trans_obj = pool.get('ir.translation')
     model_data_obj = pool.get('ir.model.data')
+    iso_lang = tools.get_iso_codes(lang)
     try:
         uid = 1
         cr = pooler.get_db(db_name).cursor()
         ids = lang_obj.search(cr, uid, [('code','=', lang)])
-        
+
         if not ids:
             # lets create the language with locale information
             fail = True
@@ -676,22 +868,28 @@ def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=
             if fail:
                 lc = locale.getdefaultlocale()[0]
                 msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
-                logger.notifyChannel('i18n', netsvc.LOG_WARNING, msg % (lang, lc)) 
-                
+                logger.warning(msg, lang, lc)
+
             if not lang_name:
                 lang_name = tools.get_languages().get(lang, lang)
-            
+
+            def fix_xa0(s):
+                if s == '\xa0':
+                    return '\xc2\xa0'
+                return s
+
             lang_info = {
                 'code': lang,
+                'iso_code': iso_lang,
                 'name': lang_name,
                 'translatable': 1,
                 'date_format' : str(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')),
                 'time_format' : str(locale.nl_langinfo(locale.T_FMT)),
-                'decimal_point' : str(locale.localeconv()['decimal_point']).replace('\xa0', '\xc2\xa0'),
-                'thousands_sep' : str(locale.localeconv()['thousands_sep']).replace('\xa0', '\xc2\xa0'),
+                'decimal_point' : fix_xa0(str(locale.localeconv()['decimal_point'])),
+                'thousands_sep' : fix_xa0(str(locale.localeconv()['thousands_sep'])),
             }
-            
-            try: 
+
+            try:
                 lang_obj.create(cr, uid, lang_info)
             finally:
                 resetlocale()
@@ -709,6 +907,7 @@ def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=
             reader = TinyPoFile(fileobj)
             f = ['type', 'name', 'res_id', 'src', 'value']
         else:
+            logger.error('Bad file format: %s', fileformat)
             raise Exception(_('Bad file format'))
 
         # read the rest of the file
@@ -729,7 +928,7 @@ def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=
                 dic[f[i]] = row[i]
 
             try:
-                dic['res_id'] = int(dic['res_id'])
+                dic['res_id'] = dic['res_id'] and int(dic['res_id']) or 0
             except:
                 model_data_ids = model_data_obj.search(cr, uid, [
                     ('model', '=', dic['name'].split(',')[0]),
@@ -742,60 +941,35 @@ def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=
                 else:
                     dic['res_id'] = False
 
-            if dic['type'] == 'model' and not strict:
-                (model, field) = dic['name'].split(',')
-
-                # get the ids of the resources of this model which share
-                # the same source
-                obj = pool.get(model)
-                if obj:
-                    ids = obj.search(cr, uid, [(field, '=', dic['src'])])
-
-                    # if the resource id (res_id) is in that list, use it,
-                    # otherwise use the whole list
-                   if not ids:
-                       ids = []
-                    ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
-                    for id in ids:
-                        dic['res_id'] = id
-                        ids = trans_obj.search(cr, uid, [
-                            ('lang', '=', lang),
-                            ('type', '=', dic['type']),
-                            ('name', '=', dic['name']),
-                            ('src', '=', dic['src']),
-                            ('res_id', '=', dic['res_id'])
-                        ])
-                        if ids:
-                            trans_obj.write(cr, uid, ids, {'value': dic['value']})
-                        else:
-                            trans_obj.create(cr, uid, dic)
-            else:
-                ids = trans_obj.search(cr, uid, [
-                    ('lang', '=', lang),
-                    ('type', '=', dic['type']),
-                    ('name', '=', dic['name']),
-                    ('src', '=', dic['src'])
-                ])
-                if ids:
+            args = [
+                ('lang', '=', lang),
+                ('type', '=', dic['type']),
+                ('name', '=', dic['name']),
+                ('src', '=', dic['src']),
+            ]
+            if dic['type'] == 'model':
+                args.append(('res_id', '=', dic['res_id']))
+            ids = trans_obj.search(cr, uid, args)
+            if ids:
+                if context.get('overwrite'):
                     trans_obj.write(cr, uid, ids, {'value': dic['value']})
-                else:
-                    trans_obj.create(cr, uid, dic)
+            else:
+                trans_obj.create(cr, uid, dic)
             cr.commit()
         cr.close()
         if verbose:
-            logger.notifyChannel("i18n", netsvc.LOG_INFO,
-                    "translation file loaded succesfully")
+            logger.info("translation file loaded succesfully")
     except IOError:
-        filename = '[lang: %s][format: %s]' % (lang or 'new', fileformat)
-        logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
+        filename = '[lang: %s][format: %s]' % (iso_lang or 'new', fileformat)
+        logger.exception("couldn't read translation file %s", filename)
 
 def get_locales(lang=None):
     if lang is None:
         lang = locale.getdefaultlocale()[0]
-    
+
     if os.name == 'nt':
         lang = _LOCALE2WIN32.get(lang, lang)
-    
+
     def process(enc):
         ln = locale._build_localename((lang, enc))
         yield ln
@@ -808,9 +982,9 @@ def get_locales(lang=None):
     prefenc = locale.getpreferredencoding()
     if prefenc:
         for x in process(prefenc): yield x
-        
+
         prefenc = {
-            'latin1': 'latin9', 
+            'latin1': 'latin9',
             'iso-8859-1': 'iso8859-15',
             'cp1252': '1252',
         }.get(prefenc.lower())
@@ -822,7 +996,7 @@ def get_locales(lang=None):
 
 
 def resetlocale():
-    # locale.resetlocale is bugged with some locales. 
+    # locale.resetlocale is bugged with some locales.
     for ln in get_locales():
         try:
             return locale.setlocale(locale.LC_ALL, ln)