[MERGE] Unify PO extraction system between server/addons and web + minor cleanup
bzr revid: odo@openerp.com-20121003165507-hmrhqgf2gxvx1xx1
This commit is contained in:
@@ -227,6 +227,8 @@ def module_installed_bypass_session(dbname):
|
||||
return sorted_modules
|
||||
|
||||
def module_boot(req):
|
||||
return [m for m in req.config.server_wide_modules if m in openerpweb.addons_manifest]
|
||||
# TODO the following will be enabled once we separate the module code and translation loading
|
||||
serverside = []
|
||||
dbside = []
|
||||
for i in req.config.server_wide_modules:
|
||||
@@ -531,6 +533,20 @@ def parse_context(context, session):
|
||||
except ValueError:
|
||||
return common.nonliterals.Context(session, context)
|
||||
|
||||
|
||||
def _local_web_translations(trans_file):
|
||||
messages = []
|
||||
try:
|
||||
with open(trans_file) as t_file:
|
||||
po = babel.messages.pofile.read_po(t_file)
|
||||
except Exception:
|
||||
return
|
||||
for x in po:
|
||||
if x.id and x.string and "openerp-web" in x.auto_comments:
|
||||
messages.append({'id': x.id, 'string': x.string})
|
||||
return messages
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
# OpenERP Web web Controllers
|
||||
#----------------------------------------------------------
|
||||
@@ -576,6 +592,7 @@ class Home(openerpweb.Controller):
|
||||
def login(self, req, db, login, key):
|
||||
return login_and_redirect(req, db, login, key)
|
||||
|
||||
|
||||
class WebClient(openerpweb.Controller):
|
||||
_cp_path = "/web/webclient"
|
||||
|
||||
@@ -658,41 +675,53 @@ class WebClient(openerpweb.Controller):
|
||||
last_modified, checksum)
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def translations(self, req, mods, lang):
|
||||
lang_model = req.session.model('res.lang')
|
||||
ids = lang_model.search([("code", "=", lang)])
|
||||
if ids:
|
||||
lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
|
||||
"grouping", "decimal_point", "thousands_sep"])
|
||||
else:
|
||||
lang_obj = None
|
||||
def bootstrap_translations(self, req, mods):
|
||||
""" Load local translations from *.po files, as a temporary solution
|
||||
until we have established a valid session. This is meant only
|
||||
for translating the login page and db management chrome, using
|
||||
the browser's language. """
|
||||
lang = req.httprequest.accept_languages.best or 'en'
|
||||
# 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.
|
||||
if '-' in lang: # RFC2616 uses '-' separators for sublanguages
|
||||
lang = [lang.split('-',1)[0], lang]
|
||||
|
||||
if "_" in lang:
|
||||
separator = "_"
|
||||
else:
|
||||
separator = "@"
|
||||
langs = lang.split(separator)
|
||||
langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
|
||||
|
||||
transs = {}
|
||||
translations_per_module = {}
|
||||
for addon_name in mods:
|
||||
transl = {"messages":[]}
|
||||
transs[addon_name] = transl
|
||||
addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
|
||||
for l in langs:
|
||||
f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
|
||||
if not os.path.exists(f_name):
|
||||
continue
|
||||
try:
|
||||
with open(f_name) as t_file:
|
||||
po = babel.messages.pofile.read_po(t_file)
|
||||
except Exception:
|
||||
continue
|
||||
for x in po:
|
||||
if x.id and x.string and "openerp-web" in x.auto_comments:
|
||||
transl["messages"].append({'id': x.id, 'string': x.string})
|
||||
return {"modules": transs,
|
||||
"lang_parameters": lang_obj}
|
||||
f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
|
||||
if not os.path.exists(f_name):
|
||||
continue
|
||||
translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
|
||||
|
||||
return {"modules": translations_per_module,
|
||||
"lang_parameters": None}
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def translations(self, req, mods, lang):
|
||||
res_lang = req.session.model('res.lang')
|
||||
ids = res_lang.search([("code", "=", lang)])
|
||||
lang_params = None
|
||||
if ids:
|
||||
lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
|
||||
"grouping", "decimal_point", "thousands_sep"])
|
||||
|
||||
# Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
|
||||
# done server-side when the language is loaded, so we only need to load the user's lang.
|
||||
ir_translation = req.session.model('ir.translation')
|
||||
translations_per_module = {}
|
||||
messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
|
||||
('comments','like','openerp-web'),('value','!=',False),
|
||||
('value','!=','')],
|
||||
['module','src','value','lang'], order='module')
|
||||
for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
|
||||
translations_per_module.setdefault(mod,{'messages':[]})
|
||||
translations_per_module[mod]['messages'].extend({'id': m['src'],
|
||||
'string': m['value']} \
|
||||
for m in msg_group)
|
||||
return {"modules": translations_per_module,
|
||||
"lang_parameters": lang_params}
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def version_info(self, req):
|
||||
@@ -769,7 +798,7 @@ class Database(openerpweb.Controller):
|
||||
{'fileToken': int(token)}
|
||||
)
|
||||
except xmlrpclib.Fault, e:
|
||||
return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
|
||||
return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
|
||||
|
||||
@openerpweb.httprequest
|
||||
def restore(self, req, db_file, restore_pwd, new_db):
|
||||
|
||||
@@ -57,7 +57,12 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
|
||||
if(self.session_is_valid()) {
|
||||
return deferred.pipe(function() { return self.load_modules(); });
|
||||
}
|
||||
return deferred;
|
||||
return $.when(
|
||||
deferred,
|
||||
self.rpc('/web/webclient/bootstrap_translations', {mods: instance._modules}).pipe(function(trans) {
|
||||
instance.web._t.database.set_bundle(trans);
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -537,13 +542,9 @@ instance.web.qweb.preprocess_node = function() {
|
||||
if (translation && translation.value === 'off') {
|
||||
return;
|
||||
}
|
||||
var ts = _.str.trim(this.node.data);
|
||||
if (ts.length === 0) {
|
||||
return;
|
||||
}
|
||||
var tr = instance.web._t(ts);
|
||||
if (tr !== ts) {
|
||||
this.node.data = tr;
|
||||
var match = /^(\s*)(.+?)(\s*)$/.exec(this.node.data);
|
||||
if (match) {
|
||||
this.node.data = match[1] + instance.web._t(match[2]) + match[3];
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
@@ -600,8 +601,7 @@ var messages_by_seconds = function() {
|
||||
[120, _t("Don't leave yet,<br />it's still loading...")],
|
||||
[300, _t("You may not believe it,<br />but the application is actually loading...")],
|
||||
[420, _t("Take a minute to get a coffee,<br />because it's loading...")],
|
||||
[600, _t("It's loading...<br />By the way, did you tried the kitten mode?")],
|
||||
[3600, _t("Maybe you should consider pressing F5...")],
|
||||
[3600, _t("Maybe you should consider reloading the application by pressing F5...")],
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -1494,7 +1494,7 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
|
||||
return $.when(facet_from(this, value));
|
||||
}
|
||||
assert(value.length <= 1,
|
||||
_("M2O search fields do not currently handle multiple default values"));
|
||||
_t("M2O search fields do not currently handle multiple default values"));
|
||||
// there are many cases of {search_default_$m2ofield: [id]}, need
|
||||
// to handle this as if it were a single value.
|
||||
value = value[0];
|
||||
@@ -1578,7 +1578,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
|
||||
|
||||
$filter.unbind('click').click(function () {
|
||||
self.view.query.reset([{
|
||||
category: _("Custom Filter"),
|
||||
category: _t("Custom Filter"),
|
||||
icon: 'M',
|
||||
field: {
|
||||
get_context: function () { return filter.context; },
|
||||
|
||||
@@ -413,7 +413,7 @@
|
||||
<a class="oe_logo" href="#"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
|
||||
<div class="oe_secondary_menus_container"/>
|
||||
<div class="oe_footer">
|
||||
Powered by <a href="http://www.openerp.com" target="_blank"><span>Open</span>ERP</a>
|
||||
Powered by <a href="http://www.openerp.com" target="_blank"><span>OpenERP</span></a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="oe_application">
|
||||
@@ -1128,7 +1128,7 @@
|
||||
<td>
|
||||
<t t-call="HiddenInputFile">
|
||||
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
|
||||
<t t-set="fileupload_style">width: 83px;</t>
|
||||
<t t-set="fileupload_style" t-translation="off">width: 83px;</t>
|
||||
<button class="oe_button oe_field_button" type="button">
|
||||
<img t-att-src='_s + "/web/static/src/img/icons/STOCK_DIRECTORY.png"'/>
|
||||
<span>Select</span>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[extractors]
|
||||
qweb = npybabel:extract_qweb
|
||||
javascript = npybabel:extract_javascript
|
||||
[javascript: static/src/js/**.js]
|
||||
[qweb: static/src/xml/**.xml]
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
usage: $0 -a [DIR]
|
||||
usage: $0 <ADDON_DIR> <OUTPUT_FILE>
|
||||
|
||||
OPTIONS:
|
||||
-a [DIR] export the .pot files for all web addons found
|
||||
at target path (default: ./addons)
|
||||
-h print this message
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
do_all=
|
||||
|
||||
while getopts "a" opt
|
||||
do
|
||||
case "$opt" in
|
||||
a)
|
||||
do_all=true;;
|
||||
h)
|
||||
usage;;
|
||||
\?)
|
||||
usage;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $((OPTIND-1))
|
||||
|
||||
if [ -n "$do_all" ]
|
||||
then
|
||||
if [ "x$(which msgcat)" = "x" ]
|
||||
then
|
||||
echo "The msgcat command from the gettext tools is required in the PATH."
|
||||
echo "On a Debian/Ubuntu system you may install gettext via 'sudo apt-get install gettext'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Extracting all web addons translations"
|
||||
executable=$0
|
||||
target_dir=${1:-./addons}
|
||||
echo "Using target dir: ${target_dir}"
|
||||
for mod in $(find ${target_dir} -type d -name 'static' -exec sh -c 'basename $(dirname {})' \;); do
|
||||
echo ${mod}
|
||||
mod_pot=${target_dir}/${mod}/i18n/${mod}.pot
|
||||
web_pot=${mod_pot}.web
|
||||
mkdir -p $(dirname ${web_pot})
|
||||
$executable ${target_dir}/${mod} ${web_pot}
|
||||
if [ -f "${mod_pot}" ]; then
|
||||
echo "Merging with existing PO file: ${mod_pot}"
|
||||
msgcat --force-po -o "${mod_pot}.tmp" ${mod_pot} ${web_pot}
|
||||
mv ${mod_pot}.tmp ${mod_pot}
|
||||
rm ${web_pot}
|
||||
else
|
||||
echo "Renaming to final PO file: ${mod_pot}"
|
||||
mv ${web_pot} ${mod_pot}
|
||||
fi
|
||||
done
|
||||
elif [ -n "$2" ]
|
||||
then
|
||||
./npybabel.py extract -F babel.cfg -o $2 -k _t -k _lt --no-default-keywords $1
|
||||
else
|
||||
usage
|
||||
fi
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# EASY-INSTALL-ENTRY-SCRIPT: 'Babel==0.9.6','console_scripts','pybabel'
|
||||
__requires__ = 'Babel==0.9.6'
|
||||
import sys
|
||||
from pkg_resources import load_entry_point
|
||||
import re
|
||||
import json
|
||||
from lxml import etree as elt
|
||||
from babel.messages import extract
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(
|
||||
load_entry_point('Babel==0.9.6', 'console_scripts', 'pybabel')()
|
||||
)
|
||||
|
||||
XMLJS_EXPR = re.compile(r"""(?:\_t *\( *((?:"(?:[^"\\]|\\.)*")|(?:'(?:[^'\\]|\\.)*')) *\))""")
|
||||
|
||||
TRANSLATION_FLAG_COMMENT = "openerp-web"
|
||||
|
||||
# List of etree._Element subclasses that we choose to ignore when parsing XML.
|
||||
# We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
|
||||
SKIPPED_ELEMENT_TYPES = (elt._Comment, elt._ProcessingInstruction, elt.CommentBase, elt.PIBase)
|
||||
|
||||
def extract_xmljs(fileobj, keywords, comment_tags, options):
|
||||
"""Extract messages from Javascript code embedded into XML documents.
|
||||
This complements the ``extract_javascript`` extractor which works
|
||||
only on pure .js files, and the``extract_qweb`` extractor, which only
|
||||
extracts XML text.
|
||||
|
||||
:param fileobj: the file-like object the messages should be extracted
|
||||
from
|
||||
:param keywords: a list of keywords (i.e. function names) that should
|
||||
be recognized as translation functions
|
||||
:param comment_tags: a list of translator tags to search for and
|
||||
include in the results
|
||||
:param options: a dictionary of additional options (optional)
|
||||
:return: an iterator over ``(lineno, funcname, message, comments)``
|
||||
tuples
|
||||
:rtype: ``iterator``
|
||||
"""
|
||||
assert False, """ the XMLJS extractor does not work and was removed:
|
||||
|
||||
* Babel apparently does not accept two extractors for the same set of files
|
||||
so it would not run the xmljs extractor at all, extraction of JS stuff
|
||||
needs to be done from the XML extractor
|
||||
* The regex above fails up if there are back-slashed quotes within the
|
||||
translatable string (the string marked with _t), it just won't match the
|
||||
string
|
||||
* While extraction succeeds on XML entities (e.g. "), translation
|
||||
matching will fail if those entities are kept in the PO msgid as the
|
||||
XML parser will get an un-escaped string, without those entities (so a
|
||||
text extractor will extract ``Found match "%s"``, but the msgid
|
||||
of the PO file must be ``Found match "%s"`` or the translation will fail
|
||||
* single-quoted strings are not valid JSON string, so single-quoted strings
|
||||
matched by the regex (likely since XML attributes are double-quoted,
|
||||
single quotes within them don't have to be escaped) will blow up when
|
||||
json-parsed for their content
|
||||
|
||||
I think that's about it.
|
||||
|
||||
If this extractor is reimplemented, it should be integrated into
|
||||
extract_qweb, either in the current pass (probably not a good idea) or as
|
||||
a separate pass using iterparse, matching either elements with t-js or
|
||||
some other kinds of t-* directives (@t-esc, @t-raw, @t-att, others?),
|
||||
shove the attribute content into a StringIO and pass *that* to Babel's
|
||||
own extract_javascript; then add a line offset in order to yield the
|
||||
correct line number.
|
||||
"""
|
||||
content = fileobj.read()
|
||||
found = XMLJS_EXPR.finditer(content)
|
||||
index = 0
|
||||
line_nbr = 0
|
||||
for f in found:
|
||||
msg = f.group(1)
|
||||
msg = json.loads(msg)
|
||||
while index < f.start():
|
||||
if content[index] == "\n":
|
||||
line_nbr += 1
|
||||
index += 1
|
||||
yield (line_nbr, None, msg, [TRANSLATION_FLAG_COMMENT])
|
||||
|
||||
def extract_qweb(fileobj, keywords, comment_tags, options):
|
||||
"""Extract messages from qweb template files.
|
||||
:param fileobj: the file-like object the messages should be extracted
|
||||
from
|
||||
:param keywords: a list of keywords (i.e. function names) that should
|
||||
be recognized as translation functions
|
||||
:param comment_tags: a list of translator tags to search for and
|
||||
include in the results
|
||||
:param options: a dictionary of additional options (optional)
|
||||
:return: an iterator over ``(lineno, funcname, message, comments)``
|
||||
tuples
|
||||
:rtype: ``iterator``
|
||||
"""
|
||||
result = []
|
||||
def handle_text(text, lineno):
|
||||
text = (text or "").strip()
|
||||
if len(text) > 1: # Avoid mono-char tokens like ':' ',' etc.
|
||||
result.append((lineno, None, text, [TRANSLATION_FLAG_COMMENT]))
|
||||
|
||||
# not using elementTree.iterparse because we need to skip sub-trees in case
|
||||
# the ancestor element had a reason to be skipped
|
||||
def iter_elements(current_element):
|
||||
for el in current_element:
|
||||
if isinstance(el, SKIPPED_ELEMENT_TYPES): continue
|
||||
if "t-js" not in el.attrib and \
|
||||
not ("t-jquery" in el.attrib and "t-operation" not in el.attrib) and \
|
||||
not ("t-translation" in el.attrib and el.attrib["t-translation"].strip() == "off"):
|
||||
handle_text(el.text, el.sourceline)
|
||||
for att in ('title', 'alt', 'label', 'placeholder'):
|
||||
if att in el.attrib:
|
||||
handle_text(el.attrib[att], el.sourceline)
|
||||
iter_elements(el)
|
||||
handle_text(el.tail, el.sourceline)
|
||||
|
||||
tree = elt.parse(fileobj)
|
||||
iter_elements(tree.getroot())
|
||||
|
||||
return result
|
||||
|
||||
def extract_javascript(fileobj, keywords, comment_tags, options):
|
||||
"""Extract messages from Javascript source files. This extractor delegates
|
||||
to babel's buit-in javascript extractor, but adds a special comment
|
||||
used as a flag to identify web translations.
|
||||
|
||||
:param fileobj: the file-like object the messages should be extracted
|
||||
from
|
||||
:param keywords: a list of keywords (i.e. function names) that should
|
||||
be recognized as translation functions
|
||||
:param comment_tags: a list of translator tags to search for and
|
||||
include in the results
|
||||
:param options: a dictionary of additional options (optional)
|
||||
:return: an iterator over ``(lineno, funcname, message, comments)``
|
||||
tuples
|
||||
:rtype: ``iterator``
|
||||
"""
|
||||
for (message_lineno, funcname, messages, comments) in \
|
||||
extract.extract_javascript(fileobj, keywords, comment_tags, options):
|
||||
comments.append(TRANSLATION_FLAG_COMMENT)
|
||||
yield (message_lineno, funcname, messages, comments)
|
||||
Reference in New Issue
Block a user