Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBastien Montagne <montagne29@wanadoo.fr>2013-02-24 12:50:55 +0400
committerBastien Montagne <montagne29@wanadoo.fr>2013-02-24 12:50:55 +0400
commit2c348d003e007fc168ae7a8d60b9d59792270ade (patch)
tree31f74ff2060a14c83b4a3952c69e2de166cc0ddd /release/scripts/modules/bl_i18n_utils/bl_extract_messages.py
parentc9d1f6fc5bb8e25e96e6f06683fd26bfaac161ec (diff)
Big i18n tools update, I/II.
Notes: * Everything is still a bit raw and sometimes hackish. * Not every feature implemented yet. * A bunch of cleanup is still needed. * Doc needs to be updated too!
Diffstat (limited to 'release/scripts/modules/bl_i18n_utils/bl_extract_messages.py')
-rw-r--r--release/scripts/modules/bl_i18n_utils/bl_extract_messages.py891
1 files changed, 891 insertions, 0 deletions
diff --git a/release/scripts/modules/bl_i18n_utils/bl_extract_messages.py b/release/scripts/modules/bl_i18n_utils/bl_extract_messages.py
new file mode 100644
index 00000000000..5287bcdbeb5
--- /dev/null
+++ b/release/scripts/modules/bl_i18n_utils/bl_extract_messages.py
@@ -0,0 +1,891 @@
+# ***** BEGIN GPL LICENSE BLOCK *****
+#
+# 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 2
+# 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ***** END GPL LICENSE BLOCK *****
+
+# <pep8 compliant>
+
+# Populate a template file (POT format currently) from Blender RNA/py/C data.
+# XXX: This script is meant to be used from inside Blender!
+# You should not directly use this script, rather use update_msg.py!
+
+import collections
+import copy
+import datetime
+import os
+import re
+import sys
+
+# XXX Relative import does not work here when used from Blender...
+from bl_i18n_utils import settings as i18n_settings, utils
+
+import bpy
+
+##### Utils #####
+
+# check for strings like "+%f°"
+ignore_reg = re.compile(r"^(?:[-*.()/\\+%°0-9]|%d|%f|%s|%r|\s)*$")
+filter_message = ignore_reg.match
+
+
+def init_spell_check(settings, lang="en_US"):
+ try:
+ from bl_i18n_utils import spell_check_utils
+ return spell_check_utils.SpellChecker(settings, lang)
+ except Exception as e:
+ print("Failed to import spell_check_utils ({})".format(str(e)))
+ return None
+
+
+def _gen_check_ctxt(settings):
+ return {
+ "multi_rnatip": set(),
+ "multi_lines": set(),
+ "py_in_rna": set(),
+ "not_capitalized": set(),
+ "end_point": set(),
+ "undoc_ops": set(),
+ "spell_checker": init_spell_check(settings),
+ "spell_errors": {},
+ }
+
+
+def _gen_reports(check_ctxt):
+ return {
+ "check_ctxt": check_ctxt,
+ "rna_structs": [],
+ "rna_structs_skipped": [],
+ "rna_props": [],
+ "rna_props_skipped": [],
+ "py_messages": [],
+ "py_messages_skipped": [],
+ "src_messages": [],
+ "src_messages_skipped": [],
+ "messages_skipped": set(),
+ }
+
+
+def check(check_ctxt, msgs, key, msgsrc, settings):
+ """
+ Performs a set of checks over the given key (context, message)...
+ """
+ if check_ctxt is None:
+ return
+ multi_rnatip = check_ctxt.get("multi_rnatip")
+ multi_lines = check_ctxt.get("multi_lines")
+ py_in_rna = check_ctxt.get("py_in_rna")
+ not_capitalized = check_ctxt.get("not_capitalized")
+ end_point = check_ctxt.get("end_point")
+ undoc_ops = check_ctxt.get("undoc_ops")
+ spell_checker = check_ctxt.get("spell_checker")
+ spell_errors = check_ctxt.get("spell_errors")
+
+ if multi_rnatip is not None:
+ if key in msgs and key not in multi_rnatip:
+ multi_rnatip.add(key)
+ if multi_lines is not None:
+ if '\n' in key[1]:
+ multi_lines.add(key)
+ if py_in_rna is not None:
+ if key in py_in_rna[1]:
+ py_in_rna[0].add(key)
+ if not_capitalized is not None:
+ if(key[1] not in settings.WARN_MSGID_NOT_CAPITALIZED_ALLOWED and
+ key[1][0].isalpha() and not key[1][0].isupper()):
+ not_capitalized.add(key)
+ if end_point is not None:
+ if (key[1].strip().endswith('.') and not key[1].strip().endswith('...') and
+ key[1] not in settings.WARN_MSGID_END_POINT_ALLOWED):
+ end_point.add(key)
+ if undoc_ops is not None:
+ if key[1] == settings.UNDOC_OPS_STR:
+ undoc_ops.add(key)
+ if spell_checker is not None and spell_errors is not None:
+ err = spell_checker.check(key[1])
+ if err:
+ spell_errors[key] = err
+
+
+def print_info(reports, pot):
+ def _print(*args, **kwargs):
+ kwargs["file"] = sys.stderr
+ print(*args, **kwargs)
+
+ pot.update_info()
+
+ _print("{} RNA structs were processed (among which {} were skipped), containing {} RNA properties "
+ "(among which {} were skipped).".format(len(reports["rna_structs"]), len(reports["rna_structs_skipped"]),
+ len(reports["rna_props"]), len(reports["rna_props_skipped"])))
+ _print("{} messages were extracted from Python UI code (among which {} were skipped), and {} from C source code "
+ "(among which {} were skipped).".format(len(reports["py_messages"]), len(reports["py_messages_skipped"]),
+ len(reports["src_messages"]), len(reports["src_messages_skipped"])))
+ _print("{} messages were rejected.".format(len(reports["messages_skipped"])))
+ _print("\n")
+ _print("Current POT stats:")
+ pot.print_stats(prefix="\t", output=_print)
+ _print("\n")
+
+ check_ctxt = reports["check_ctxt"]
+ if check_ctxt is None:
+ return
+ multi_rnatip = check_ctxt.get("multi_rnatip")
+ multi_lines = check_ctxt.get("multi_lines")
+ py_in_rna = check_ctxt.get("py_in_rna")
+ not_capitalized = check_ctxt.get("not_capitalized")
+ end_point = check_ctxt.get("end_point")
+ undoc_ops = check_ctxt.get("undoc_ops")
+ spell_errors = check_ctxt.get("spell_errors")
+
+ # XXX Temp, no multi_rnatip nor py_in_rna, see below.
+ keys = multi_lines | not_capitalized | end_point | undoc_ops | spell_errors.keys()
+ if keys:
+ _print("WARNINGS:")
+ for key in keys:
+ if undoc_ops and key in undoc_ops:
+ _print("\tThe following operators are undocumented!")
+ else:
+ _print("\t“{}”|“{}”:".format(*key))
+ if multi_lines and key in multi_lines:
+ _print("\t\t-> newline in this message!")
+ if not_capitalized and key in not_capitalized:
+ _print("\t\t-> message not capitalized!")
+ if end_point and key in end_point:
+ _print("\t\t-> message with endpoint!")
+ # XXX Hide this one for now, too much false positives.
+# if multi_rnatip and key in multi_rnatip:
+# _print("\t\t-> tip used in several RNA items")
+# if py_in_rna and key in py_in_rna:
+# _print("\t\t-> RNA message also used in py UI code!")
+ if spell_errors and spell_errors.get(key):
+ lines = ["\t\t-> {}: misspelled, suggestions are ({})".format(w, "'" + "', '".join(errs) + "'")
+ for w, errs in spell_errors[key]]
+ _print("\n".join(lines))
+ _print("\t\t{}".format("\n\t\t".join(pot.msgs[key].sources)))
+
+
+def enable_addons(addons={}, support={}, disable=False):
+ """
+ Enable (or disable) addons based either on a set of names, or a set of 'support' types.
+ Returns the list of all affected addons (as fake modules)!
+ """
+ import addon_utils
+
+ userpref = bpy.context.user_preferences
+ used_ext = {ext.module for ext in userpref.addons}
+
+ ret = [mod for mod in addon_utils.modules(addon_utils.addons_fake_modules)
+ if ((addons and mod.__name__ in addons) or
+ (not addons and addon_utils.module_bl_info(mod)["support"] in support))]
+
+ for mod in ret:
+ module_name = mod.__name__
+ if disable:
+ if module_name not in used_ext:
+ continue
+ print(" Disabling module ", module_name)
+ bpy.ops.wm.addon_disable(module=module_name)
+ else:
+ if module_name in used_ext:
+ continue
+ print(" Enabling module ", module_name)
+ bpy.ops.wm.addon_enable(module=module_name)
+
+ # XXX There are currently some problems with bpy/rna...
+ # *Very* tricky to solve!
+ # So this is a hack to make all newly added operator visible by
+ # bpy.types.OperatorProperties.__subclasses__()
+ for cat in dir(bpy.ops):
+ cat = getattr(bpy.ops, cat)
+ for op in dir(cat):
+ getattr(cat, op).get_rna()
+
+ return ret
+
+
+def process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt, settings):
+ if filter_message(msgid):
+ reports["messages_skipped"].add((msgid, msgsrc))
+ return
+ if not msgctxt:
+ # We do *not* want any "" context!
+ msgctxt = settings.DEFAULT_CONTEXT
+ # Always unescape keys!
+ msgctxt = utils.I18nMessage.do_unescape(msgctxt)
+ msgid = utils.I18nMessage.do_unescape(msgid)
+ key = (msgctxt, msgid)
+ check(check_ctxt, msgs, key, msgsrc, settings)
+ msgsrc = settings.PO_COMMENT_PREFIX_SOURCE_CUSTOM + msgsrc
+ if key not in msgs:
+ msgs[key] = utils.I18nMessage([msgctxt], [msgid], [], [msgsrc], settings=settings)
+ else:
+ msgs[key].comment_lines.append(msgsrc)
+
+
+##### RNA #####
+def dump_messages_rna(msgs, reports, settings):
+ """
+ Dump into messages dict all RNA-defined UI messages (labels en tooltips).
+ """
+ def class_blacklist():
+ blacklist_rna_class = [
+ # core classes
+ "Context", "Event", "Function", "UILayout", "BlendData", "UnknownType",
+ # registerable classes
+ "Panel", "Menu", "Header", "RenderEngine", "Operator", "OperatorMacro", "Macro", "KeyingSetInfo",
+ # window classes
+ "Window",
+ ]
+
+ # Collect internal operators
+ # extend with all internal operators
+ # note that this uses internal api introspection functions
+ # all possible operator names
+ op_ids = set(cls.bl_rna.identifier for cls in bpy.types.OperatorProperties.__subclasses__()) | \
+ set(cls.bl_rna.identifier for cls in bpy.types.Operator.__subclasses__()) | \
+ set(cls.bl_rna.identifier for cls in bpy.types.OperatorMacro.__subclasses__())
+
+ get_instance = __import__("_bpy").ops.get_instance
+# path_resolve = type(bpy.context).__base__.path_resolve
+ for idname in op_ids:
+ op = get_instance(idname)
+ # XXX Do not skip INTERNAL's anymore, some of those ops show up in UI now!
+# if 'INTERNAL' in path_resolve(op, "bl_options"):
+# blacklist_rna_class.append(idname)
+
+ # Collect builtin classes we don't need to doc
+ blacklist_rna_class.append("Property")
+ blacklist_rna_class.extend([cls.__name__ for cls in bpy.types.Property.__subclasses__()])
+
+ # Collect classes which are attached to collections, these are api access only.
+ collection_props = set()
+ for cls_id in dir(bpy.types):
+ cls = getattr(bpy.types, cls_id)
+ for prop in cls.bl_rna.properties:
+ if prop.type == 'COLLECTION':
+ prop_cls = prop.srna
+ if prop_cls is not None:
+ collection_props.add(prop_cls.identifier)
+ blacklist_rna_class.extend(sorted(collection_props))
+
+ return blacklist_rna_class
+
+ check_ctxt_rna = check_ctxt_rna_tip = None
+ check_ctxt = reports["check_ctxt"]
+ if check_ctxt:
+ check_ctxt_rna = {
+ "multi_lines": check_ctxt.get("multi_lines"),
+ "not_capitalized": check_ctxt.get("not_capitalized"),
+ "end_point": check_ctxt.get("end_point"),
+ "undoc_ops": check_ctxt.get("undoc_ops"),
+ "spell_checker": check_ctxt.get("spell_checker"),
+ "spell_errors": check_ctxt.get("spell_errors"),
+ }
+ check_ctxt_rna_tip = check_ctxt_rna
+ check_ctxt_rna_tip["multi_rnatip"] = check_ctxt.get("multi_rnatip")
+
+ default_context = settings.DEFAULT_CONTEXT
+
+ # Function definitions
+ def walk_properties(cls):
+ bl_rna = cls.bl_rna
+ # Get our parents' properties, to not export them multiple times.
+ bl_rna_base = bl_rna.base
+ if bl_rna_base:
+ bl_rna_base_props = set(bl_rna_base.properties.values())
+ else:
+ bl_rna_base_props = set()
+
+ for prop in bl_rna.properties:
+ # Only write this property if our parent hasn't got it.
+ if prop in bl_rna_base_props:
+ continue
+ if prop.identifier == "rna_type":
+ continue
+ reports["rna_props"].append((cls, prop))
+
+ msgsrc = "bpy.types.{}.{}".format(bl_rna.identifier, prop.identifier)
+ msgctxt = prop.translation_context or default_context
+
+ if prop.name and (prop.name != prop.identifier or msgctxt != default_context):
+ process_msg(msgs, msgctxt, prop.name, msgsrc, reports, check_ctxt_rna, settings)
+ if prop.description:
+ process_msg(msgs, default_context, prop.description, msgsrc, reports, check_ctxt_rna_tip, settings)
+
+ if isinstance(prop, bpy.types.EnumProperty):
+ for item in prop.enum_items:
+ msgsrc = "bpy.types.{}.{}:'{}'".format(bl_rna.identifier, prop.identifier, item.identifier)
+ if item.name and item.name != item.identifier:
+ process_msg(msgs, msgctxt, item.name, msgsrc, reports, check_ctxt_rna, settings)
+ if item.description:
+ process_msg(msgs, default_context, item.description, msgsrc, reports, check_ctxt_rna_tip,
+ settings)
+
+ blacklist_rna_class = class_blacklist()
+
+ def walk_class(cls):
+ bl_rna = cls.bl_rna
+ reports["rna_structs"].append(cls)
+ if bl_rna.identifier in blacklist_rna_class:
+ reports["rna_structs_skipped"].append(cls)
+ return
+
+ # XXX translation_context of Operator sub-classes are not "good"!
+ # So ignore those Operator sub-classes (anyway, will get the same from OperatorProperties sub-classes!)...
+ if issubclass(cls, bpy.types.Operator):
+ reports["rna_structs_skipped"].append(cls)
+ return
+
+ msgsrc = "bpy.types." + bl_rna.identifier
+ msgctxt = bl_rna.translation_context or default_context
+
+ if bl_rna.name and (bl_rna.name != bl_rna.identifier or msgctxt != default_context):
+ process_msg(msgs, msgctxt, bl_rna.name, msgsrc, reports, check_ctxt_rna, settings)
+
+ if bl_rna.description:
+ process_msg(msgs, default_context, bl_rna.description, msgsrc, reports, check_ctxt_rna_tip, settings)
+
+ if hasattr(bl_rna, 'bl_label') and bl_rna.bl_label:
+ process_msg(msgs, msgctxt, bl_rna.bl_label, msgsrc, reports, check_ctxt_rna, settings)
+
+ walk_properties(cls)
+
+ def walk_keymap_hierarchy(hier, msgsrc_prev):
+ for lvl in hier:
+ msgsrc = msgsrc_prev + "." + lvl[1]
+ process_msg(msgs, default_context, lvl[0], msgsrc, reports, None, settings)
+ if lvl[3]:
+ walk_keymap_hierarchy(lvl[3], msgsrc)
+
+ # Dump Messages
+ def process_cls_list(cls_list):
+ if not cls_list:
+ return
+
+ def full_class_id(cls):
+ """ gives us 'ID.Lamp.AreaLamp' which is best for sorting."""
+ cls_id = ""
+ bl_rna = cls.bl_rna
+ while bl_rna:
+ cls_id = bl_rna.identifier + "." + cls_id
+ bl_rna = bl_rna.base
+ return cls_id
+
+ cls_list.sort(key=full_class_id)
+ for cls in cls_list:
+ walk_class(cls)
+ # Recursively process subclasses.
+ process_cls_list(cls.__subclasses__())
+
+ # Parse everything (recursively parsing from bpy_struct "class"...).
+ process_cls_list(bpy.types.ID.__base__.__subclasses__())
+
+ # And parse keymaps!
+ from bpy_extras.keyconfig_utils import KM_HIERARCHY
+
+ walk_keymap_hierarchy(KM_HIERARCHY, "KM_HIERARCHY")
+
+
+##### Python source code #####
+def dump_py_messages_from_files(msgs, reports, files, settings):
+ """
+ Dump text inlined in the python files given, e.g. 'My Name' in:
+ layout.prop("someprop", text="My Name")
+ """
+ import ast
+
+ bpy_struct = bpy.types.ID.__base__
+
+ # Helper function
+ def extract_strings_ex(node, is_split=False):
+ """
+ Recursively get strings, needed in case we have "Blah" + "Blah", passed as an argument in that case it won't
+ evaluate to a string. However, break on some kind of stopper nodes, like e.g. Subscript.
+ """
+ if type(node) == ast.Str:
+ eval_str = ast.literal_eval(node)
+ if eval_str:
+ yield (is_split, eval_str, (node,))
+ else:
+ is_split = (type(node) in separate_nodes)
+ for nd in ast.iter_child_nodes(node):
+ if type(nd) not in stopper_nodes:
+ yield from extract_strings_ex(nd, is_split=is_split)
+
+ def _extract_string_merge(estr_ls, nds_ls):
+ return "".join(s for s in estr_ls if s is not None), tuple(n for n in nds_ls if n is not None)
+
+ def extract_strings(node):
+ estr_ls = []
+ nds_ls = []
+ for is_split, estr, nds in extract_strings_ex(node):
+ estr_ls.append(estr)
+ nds_ls.extend(nds)
+ ret = _extract_string_merge(estr_ls, nds_ls)
+ return ret
+
+ def extract_strings_split(node):
+ """
+ Returns a list args as returned by 'extract_strings()', But split into groups based on separate_nodes, this way
+ expressions like ("A" if test else "B") wont be merged but "A" + "B" will.
+ """
+ estr_ls = []
+ nds_ls = []
+ bag = []
+ for is_split, estr, nds in extract_strings_ex(node):
+ if is_split:
+ bag.append((estr_ls, nds_ls))
+ estr_ls = []
+ nds_ls = []
+
+ estr_ls.append(estr)
+ nds_ls.extend(nds)
+
+ bag.append((estr_ls, nds_ls))
+
+ return [_extract_string_merge(estr_ls, nds_ls) for estr_ls, nds_ls in bag]
+
+
+ def _ctxt_to_ctxt(node):
+ return extract_strings(node)[0]
+
+ def _op_to_ctxt(node):
+ opname, _ = extract_strings(node)
+ if not opname:
+ return settings.DEFAULT_CONTEXT
+ op = bpy.ops
+ for n in opname.split('.'):
+ op = getattr(op, n)
+ try:
+ return op.get_rna().bl_rna.translation_context
+ except Exception as e:
+ default_op_context = bpy.app.translations.contexts.operator_default
+ print("ERROR: ", str(e))
+ print(" Assuming default operator context '{}'".format(default_op_context))
+ return default_op_context
+
+ # Gather function names.
+ # In addition of UI func, also parse pgettext ones...
+ # Tuples of (module name, (short names, ...)).
+ pgettext_variants = (
+ ("pgettext", ("_",)),
+ ("pgettext_iface", ("iface_",)),
+ ("pgettext_tip", ("tip_",))
+ )
+ pgettext_variants_args = {"msgid": (0, {"msgctxt": 1})}
+
+ # key: msgid keywords.
+ # val: tuples of ((keywords,), context_getter_func) to get a context for that msgid.
+ # Note: order is important, first one wins!
+ translate_kw = {
+ "text": ((("text_ctxt",), _ctxt_to_ctxt),
+ (("operator",), _op_to_ctxt),
+ ),
+ "msgid": ((("msgctxt",), _ctxt_to_ctxt),
+ ),
+ }
+
+ context_kw_set = {}
+ for k, ctxts in translate_kw.items():
+ s = set()
+ for c, _ in ctxts:
+ s |= set(c)
+ context_kw_set[k] = s
+
+ # {func_id: {msgid: (arg_pos,
+ # {msgctxt: arg_pos,
+ # ...
+ # }
+ # ),
+ # ...
+ # },
+ # ...
+ # }
+ func_translate_args = {}
+
+ # First, functions from UILayout
+ # First loop is for msgid args, second one is for msgctxt args.
+ for func_id, func in bpy.types.UILayout.bl_rna.functions.items():
+ # check it has one or more arguments as defined in translate_kw
+ for arg_pos, (arg_kw, arg) in enumerate(func.parameters.items()):
+ if ((arg_kw in translate_kw) and (not arg.is_output) and (arg.type == 'STRING')):
+ func_translate_args.setdefault(func_id, {})[arg_kw] = (arg_pos, {})
+ for func_id, func in bpy.types.UILayout.bl_rna.functions.items():
+ if func_id not in func_translate_args:
+ continue
+ for arg_pos, (arg_kw, arg) in enumerate(func.parameters.items()):
+ if (not arg.is_output) and (arg.type == 'STRING'):
+ for msgid, msgctxts in context_kw_set.items():
+ if arg_kw in msgctxts:
+ func_translate_args[func_id][msgid][1][arg_kw] = arg_pos
+ # We manually add funcs from bpy.app.translations
+ for func_id, func_ids in pgettext_variants:
+ func_translate_args[func_id] = pgettext_variants_args
+ for func_id in func_ids:
+ func_translate_args[func_id] = pgettext_variants_args
+ #print(func_translate_args)
+
+ # Break recursive nodes look up on some kind of nodes.
+ # E.g. we don’t want to get strings inside subscripts (blah["foo"])!
+ stopper_nodes = {ast.Subscript}
+ # Consider strings separate: ("a" if test else "b")
+ separate_nodes = {ast.IfExp}
+
+ check_ctxt_py = None
+ if reports["check_ctxt"]:
+ check_ctxt = reports["check_ctxt"]
+ check_ctxt_py = {
+ "py_in_rna": (check_ctxt.get("py_in_rna"), set(msgs.keys())),
+ "multi_lines": check_ctxt.get("multi_lines"),
+ "not_capitalized": check_ctxt.get("not_capitalized"),
+ "end_point": check_ctxt.get("end_point"),
+ "spell_checker": check_ctxt.get("spell_checker"),
+ "spell_errors": check_ctxt.get("spell_errors"),
+ }
+
+ for fp in files:
+ with open(fp, 'r', encoding="utf8") as filedata:
+ root_node = ast.parse(filedata.read(), fp, 'exec')
+
+ fp_rel = os.path.relpath(fp, settings.SOURCE_DIR)
+
+ for node in ast.walk(root_node):
+ if type(node) == ast.Call:
+ # print("found function at")
+ # print("%s:%d" % (fp, node.lineno))
+
+ # We can't skip such situations! from blah import foo\nfoo("bar") would also be an ast.Name func!
+ if type(node.func) == ast.Name:
+ func_id = node.func.id
+ elif hasattr(node.func, "attr"):
+ func_id = node.func.attr
+ # Ugly things like getattr(self, con.type)(context, box, con)
+ else:
+ continue
+
+ func_args = func_translate_args.get(func_id, {})
+
+ # First try to get i18n contexts, for every possible msgid id.
+ msgctxts = dict.fromkeys(func_args.keys(), "")
+ for msgid, (_, context_args) in func_args.items():
+ context_elements = {}
+ for arg_kw, arg_pos in context_args.items():
+ if arg_pos < len(node.args):
+ context_elements[arg_kw] = node.args[arg_pos]
+ else:
+ for kw in node.keywords:
+ if kw.arg == arg_kw:
+ context_elements[arg_kw] = kw.value
+ break
+ #print(context_elements)
+ for kws, proc in translate_kw[msgid]:
+ if set(kws) <= context_elements.keys():
+ args = tuple(context_elements[k] for k in kws)
+ #print("running ", proc, " with ", args)
+ ctxt = proc(*args)
+ if ctxt:
+ msgctxts[msgid] = ctxt
+ break
+
+ #print(translate_args)
+ # do nothing if not found
+ for arg_kw, (arg_pos, _) in func_args.items():
+ msgctxt = msgctxts[arg_kw]
+ estr_lst = [(None, ())]
+ if arg_pos < len(node.args):
+ estr_lst = extract_strings_split(node.args[arg_pos])
+ #print(estr, nds)
+ else:
+ for kw in node.keywords:
+ if kw.arg == arg_kw:
+ estr_lst = extract_strings_split(kw.value)
+ break
+ #print(estr, nds)
+ for estr, nds in estr_lst:
+ if estr:
+ if nds:
+ msgsrc = "{}:{}".format(fp_rel, sorted({nd.lineno for nd in nds})[0])
+ else:
+ msgsrc = "{}:???".format(fp_rel)
+ process_msg(msgs, msgctxt, estr, msgsrc, reports, check_ctxt_py, settings)
+ reports["py_messages"].append((msgctxt, estr, msgsrc))
+
+
+def dump_py_messages(msgs, reports, addons, settings):
+ def _get_files(path):
+ if os.path.isdir(path):
+ # XXX use walk instead of listdir?
+ return [os.path.join(path, fn) for fn in sorted(os.listdir(path))
+ if not fn.startswith("_") and fn.endswith(".py")]
+ return [path]
+
+ files = []
+ for path in settings.CUSTOM_PY_UI_FILES:
+ files += _get_files(path)
+
+ # Add all addons we support in main translation file!
+ for mod in addons:
+ fn = mod.__file__
+ if os.path.basename(fn) == "__init__.py":
+ files += _get_files(os.path.dirname(fn))
+ else:
+ files.append(fn)
+
+ dump_py_messages_from_files(msgs, reports, files, settings)
+
+
+##### C source code #####
+def dump_src_messages(msgs, reports, settings):
+ def get_contexts():
+ """Return a mapping {C_CTXT_NAME: ctxt_value}."""
+ return {k: getattr(bpy.app.translations.contexts, n) for k, n in bpy.app.translations.contexts_C_to_py.items()}
+
+ contexts = get_contexts()
+
+ # Build regexes to extract messages (with optional contexts) from C source.
+ pygettexts = tuple(re.compile(r).search for r in settings.PYGETTEXT_KEYWORDS)
+
+ _clean_str = re.compile(settings.str_clean_re).finditer
+ clean_str = lambda s: "".join(m.group("clean") for m in _clean_str(s))
+
+ def dump_src_file(path, rel_path, msgs, reports, settings):
+ def process_entry(_msgctxt, _msgid):
+ # Context.
+ msgctxt = settings.DEFAULT_CONTEXT
+ if _msgctxt:
+ if _msgctxt in contexts:
+ msgctxt = contexts[_msgctxt]
+ elif '"' in _msgctxt or "'" in _msgctxt:
+ msgctxt = clean_str(_msgctxt)
+ else:
+ print("WARNING: raw context “{}” couldn’t be resolved!".format(_msgctxt))
+ # Message.
+ msgid = ""
+ if _msgid:
+ if '"' in _msgid or "'" in _msgid:
+ msgid = clean_str(_msgid)
+ else:
+ print("WARNING: raw message “{}” couldn’t be resolved!".format(_msgid))
+ return msgctxt, msgid
+
+ check_ctxt_src = None
+ if reports["check_ctxt"]:
+ check_ctxt = reports["check_ctxt"]
+ check_ctxt_src = {
+ "multi_lines": check_ctxt.get("multi_lines"),
+ "not_capitalized": check_ctxt.get("not_capitalized"),
+ "end_point": check_ctxt.get("end_point"),
+ "spell_checker": check_ctxt.get("spell_checker"),
+ "spell_errors": check_ctxt.get("spell_errors"),
+ }
+
+ data = ""
+ with open(path) as f:
+ data = f.read()
+ for srch in pygettexts:
+ m = srch(data)
+ line = pos = 0
+ while m:
+ d = m.groupdict()
+ # Line.
+ line += data[pos:m.start()].count('\n')
+ msgsrc = rel_path + ":" + str(line)
+ _msgid = d.get("msg_raw")
+ # First, try the "multi-contexts" stuff!
+ _msgctxts = tuple(d.get("ctxt_raw{}".format(i)) for i in range(settings.PYGETTEXT_MAX_MULTI_CTXT))
+ if _msgctxts[0]:
+ for _msgctxt in _msgctxts:
+ if not _msgctxt:
+ break
+ msgctxt, msgid = process_entry(_msgctxt, _msgid)
+ process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt_src, settings)
+ reports["src_messages"].append((msgctxt, msgid, msgsrc))
+ else:
+ _msgctxt = d.get("ctxt_raw")
+ msgctxt, msgid = process_entry(_msgctxt, _msgid)
+ process_msg(msgs, msgctxt, msgid, msgsrc, reports, check_ctxt_src, settings)
+ reports["src_messages"].append((msgctxt, msgid, msgsrc))
+
+ pos = m.end()
+ line += data[m.start():pos].count('\n')
+ m = srch(data, pos)
+
+ forbidden = set()
+ forced = set()
+ if os.path.isfile(settings.SRC_POTFILES):
+ with open(settings.SRC_POTFILES) as src:
+ for l in src:
+ if l[0] == '-':
+ forbidden.add(l[1:].rstrip('\n'))
+ elif l[0] != '#':
+ forced.add(l.rstrip('\n'))
+ for root, dirs, files in os.walk(settings.POTFILES_SOURCE_DIR):
+ if "/.svn" in root:
+ continue
+ for fname in files:
+ if os.path.splitext(fname)[1] not in settings.PYGETTEXT_ALLOWED_EXTS:
+ continue
+ path = os.path.join(root, fname)
+ rel_path = os.path.relpath(path, settings.SOURCE_DIR)
+ if rel_path in forbidden:
+ continue
+ elif rel_path not in forced:
+ forced.add(rel_path)
+ for rel_path in sorted(forced):
+ path = os.path.join(settings.SOURCE_DIR, rel_path)
+ if os.path.exists(path):
+ dump_src_file(path, rel_path, msgs, reports, settings)
+
+
+##### Main functions! #####
+def dump_messages(do_messages, do_checks, settings):
+ bl_ver = "Blender " + bpy.app.version_string
+ bl_rev = bpy.app.build_revision
+ bl_date = datetime.datetime.strptime(bpy.app.build_date.decode() + "T" + bpy.app.build_time.decode(),
+ "%Y-%m-%dT%H:%M:%S")
+ pot = utils.I18nMessages.gen_empty_messages(settings.PARSER_TEMPLATE_ID, bl_ver, bl_rev, bl_date, bl_date.year,
+ settings=settings)
+ msgs = pot.msgs
+
+ # Enable all wanted addons.
+ # For now, enable all official addons, before extracting msgids.
+ addons = enable_addons(support={"OFFICIAL"})
+ # Note this is not needed if we have been started with factory settings, but just in case...
+ enable_addons(support={"COMMUNITY", "TESTING"}, disable=True)
+
+ reports = _gen_reports(_gen_check_ctxt(settings) if do_checks else None)
+
+ # Get strings from RNA.
+ dump_messages_rna(msgs, reports, settings)
+
+ # Get strings from UI layout definitions text="..." args.
+ dump_py_messages(msgs, reports, addons, settings)
+
+ # Get strings from C source code.
+ dump_src_messages(msgs, reports, settings)
+
+ # Get strings specific to translations' menu.
+ for lng in settings.LANGUAGES:
+ process_msg(msgs, settings.DEFAULT_CONTEXT, lng[1], "Languages’ labels from bl_i18n_utils/settings.py",
+ reports, None, settings)
+ for cat in settings.LANGUAGES_CATEGORIES:
+ process_msg(msgs, settings.DEFAULT_CONTEXT, cat[1],
+ "Language categories’ labels from bl_i18n_utils/settings.py", reports, None, settings)
+
+ #pot.check()
+ pot.unescape() # Strings gathered in py/C source code may contain escaped chars...
+ print_info(reports, pot)
+ #pot.check()
+
+ if do_messages:
+ print("Writing messages…")
+ pot.write('PO', settings.FILE_NAME_POT)
+
+ print("Finished extracting UI messages!")
+
+
+def dump_addon_messages(module_name, messages_formats, do_checks, settings):
+ # Enable our addon and get strings from RNA.
+ addon = enable_addons(addons={module_name})[0]
+
+ addon_info = addon_utils.module_bl_info(addon)
+ ver = addon_info.name + " " + ".".join(addon_info.version)
+ rev = "???"
+ date = datetime.datetime()
+ pot = utils.I18nMessages.gen_empty_messages(settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year,
+ settings=settings)
+ msgs = pot.msgs
+
+ minus_msgs = copy.deepcopy(msgs)
+
+ check_ctxt = _gen_check_ctxt(settings) if do_checks else None
+ minus_check_ctxt = _gen_check_ctxt(settings) if do_checks else None
+
+ # Get current addon state (loaded or not):
+ was_loaded = addon_utils.check(module_name)[1]
+
+ # Enable our addon and get strings from RNA.
+ addons = enable_addons(addons={module_name})
+ reports = _gen_reports(check_ctxt)
+ dump_messages_rna(msgs, reports, settings)
+
+ # Now disable our addon, and rescan RNA.
+ enable_addons(addons={module_name}, disable=True)
+ reports["check_ctxt"] = minus_check_ctxt
+ dump_messages_rna(minus_msgs, reports, settings)
+
+ # Restore previous state if needed!
+ if was_loaded:
+ enable_addons(addons={module_name})
+
+ # and make the diff!
+ for key in minus_msgs:
+ if key == settings.PO_HEADER_KEY:
+ continue
+ del msgs[key]
+
+ if check_ctxt:
+ for key in check_ctxt:
+ for warning in minus_check_ctxt[key]:
+ check_ctxt[key].remove(warning)
+
+ # and we are done with those!
+ del minus_msgs
+ del minus_check_ctxt
+
+ # get strings from UI layout definitions text="..." args
+ reports["check_ctxt"] = check_ctxt
+ dump_messages_pytext(msgs, reports, addons, settings)
+
+ print_info(reports, pot)
+
+ return pot
+
+
+def main():
+ try:
+ import bpy
+ except ImportError:
+ print("This script must run from inside blender")
+ return
+
+ import sys
+ back_argv = sys.argv
+ # Get rid of Blender args!
+ sys.argv = sys.argv[sys.argv.index("--") + 1:]
+
+ import argparse
+ parser = argparse.ArgumentParser(description="Process UI messages from inside Blender.")
+ parser.add_argument('-c', '--no_checks', default=True, action="store_false", help="No checks over UI messages.")
+ parser.add_argument('-m', '--no_messages', default=True, action="store_false", help="No export of UI messages.")
+ parser.add_argument('-o', '--output', default=None, help="Output POT file path.")
+ parser.add_argument('-s', '--settings', default=None,
+ help="Override (some) default settings. Either a JSon file name, or a JSon string.")
+ args = parser.parse_args()
+
+ settings = i18n_settings.I18nSettings()
+ settings.from_json(args.settings)
+
+ if args.output:
+ settings.FILE_NAME_POT = args.output
+
+ dump_messages(do_messages=args.no_messages, do_checks=args.no_checks, settings=settings)
+
+ sys.argv = back_argv
+
+
+if __name__ == "__main__":
+ print("\n\n *** Running {} *** \n".format(__file__))
+ main()