[IMP] tools.translate: various improvements to tranlations export/import, courtesy...
authorOlivier Dony <odo@openerp.com>
Tue, 16 Nov 2010 12:38:44 +0000 (13:38 +0100)
committerOlivier Dony <odo@openerp.com>
Tue, 16 Nov 2010 12:38:44 +0000 (13:38 +0100)
- better support for multiline labels
- better support for multiple addons paths

bzr revid: odo@openerp.com-20101116123844-mo9mljrzduzs90gg

bin/tools/translate.py

index c22b2ca..c75153c 100644 (file)
@@ -200,6 +200,23 @@ class GettextAlias(object):
 _ = 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"
+    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):
@@ -231,11 +248,6 @@ class TinyPoFile(object):
         return (self.lines_count - len(self.lines))
 
     def next(self):
-        def unquote(str):
-            return str[1:-1].replace("\\n", "\n")   \
-                            .replace("\\\\ ", "\\ ") \
-                            .replace('\\"', '"')
-
         type = name = res_id = source = trad = None
 
         if self.tnrs:
@@ -337,11 +349,6 @@ class TinyPoFile(object):
                           )
 
     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)))
@@ -654,29 +661,21 @@ def trans_generate(lang, modules, dbname=None):
 
     # 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):
-#                path = path[len(mp)+1:]
-#                return path.split(os.path.sep)[0]
-        path_dir = os.path.dirname(path[1:])
-        if path_dir:
-            if os.path.exists(os.path.join(tools.config['addons_path'],path[1:])):
-                return path.split(os.path.sep)[1]
-            else:
-                root_addons = os.path.join(tools.config['root_path'], 'addons')
-                if os.path.exists(os.path.join(root_addons,path[1:])):
-                    return path.split(os.path.sep)[1]
+        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
 
     modobj = pool.get('ir.module.module')
@@ -685,25 +684,55 @@ def trans_generate(lang, modules, dbname=None):
 
     root_path = os.path.join(tools.config['root_path'], 'addons')
 
-    if root_path in tools.config['addons_path'] :
-        path_list = [root_path]
+    apaths = map(os.path.abspath, map(str.strip, tools.config['addons_path'].split(',')))
+    if root_path in apaths:
+        path_list = apaths
     else :
-        path_list = [root_path,tools.config['addons_path']]
+        path_list = [root_path,] + apaths
+
+    logger.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning modules at paths: %s" % (' '.join(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(frelativepath)
+        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.notifyChannel("i18n", netsvc.LOG_DEBUG, "Scanning code of %s at module: %s" % (frelativepath, module))
             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, terms_type, frelativepath, 0, encode(i.group(1)))
+            ite = re_dquotes.finditer(code_string)
+            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)
+                # 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, 0, encode(src))
+            ite = re_quotes.finditer(code_string)
+            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)
+                src = src.decode('string_escape')
+                push_translation(module, terms_type, frelativepath, 0, encode(src))
 
     for path in path_list:
+        logger.notifyChannel("i18n", netsvc.LOG_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')