i18n: fixed mistage in message generator which lead to some messages disappeared.
[blender.git] / po / update_msg.py
1 # $Id$
2 # ***** BEGIN GPL LICENSE BLOCK *****
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 #
18 # ***** END GPL LICENSE BLOCK *****
19
20 # <pep8 compliant>
21
22 # Write out messages.txt from blender
23
24 # Execite:
25 #   blender --background --python po/update_msg.py
26
27 import os
28
29 CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
30 SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.join(CURRENT_DIR, "..")))
31
32 FILE_NAME_MESSAGES = os.path.join(CURRENT_DIR, "messages.txt")
33 COMMENT_PREFIX = "#~ "
34
35
36 def dump_messages_rna(messages):
37     import bpy
38
39     # -------------------------------------------------------------------------
40     # Function definitions
41
42     def walkProperties(bl_rna):
43         import bpy
44
45         # get our parents properties not to export them multiple times
46         bl_rna_base = bl_rna.base
47         if bl_rna_base:
48             bl_rna_base_props = bl_rna_base.properties.values()
49         else:
50             bl_rna_base_props = ()
51
52         for prop in bl_rna.properties:
53             # only write this property is our parent hasn't got it.
54             if prop in bl_rna_base_props:
55                 continue
56             if prop.identifier == "rna_type":
57                 continue
58
59             msgsrc = "bpy.types.%s.%s" % (bl_rna.identifier, prop.identifier)
60             messages.setdefault(prop.name, []).append(msgsrc)
61             messages.setdefault(prop.description, []).append(msgsrc)
62
63             if isinstance(prop, bpy.types.EnumProperty):
64                 for item in prop.enum_items:
65                     msgsrc = "bpy.types.%s.%s, '%s'" % (bl_rna.identifier,
66                                                         prop.identifier,
67                                                         item.identifier,
68                                                         )
69                     messages.setdefault(item.name, []).append(msgsrc)
70                     messages.setdefault(item.description, []).append(msgsrc)
71
72     def walkRNA(bl_rna):
73         msgsrc = "bpy.types.%s" % bl_rna.identifier
74
75         if bl_rna.name and bl_rna.name != bl_rna.identifier:
76             messages.setdefault(bl_rna.name, []).append(msgsrc)
77
78         if bl_rna.description:
79             messages.setdefault(bl_rna.description, []).append(msgsrc)
80
81         if hasattr(bl_rna, 'bl_label') and  bl_rna.bl_label:
82             messages.setdefault(bl_rna.bl_label, []).append(msgsrc)
83
84         walkProperties(bl_rna)
85
86     def walkClass(cls):
87         walkRNA(cls.bl_rna)
88
89     def walk_keymap_hierarchy(hier, msgsrc_prev):
90         for lvl in hier:
91             msgsrc = "%s.%s" % (msgsrc_prev, lvl[1])
92             messages.setdefault(lvl[0], []).append(msgsrc)
93
94             if lvl[3]:
95                 walk_keymap_hierarchy(lvl[3], msgsrc)
96
97     # -------------------------------------------------------------------------
98     # Dump Messages
99
100     def full_class_id(cls):
101         """ gives us 'ID.Lamp.AreaLamp' which is best for sorting.
102         """
103         cls_id = ""
104         bl_rna = cls.bl_rna
105         while bl_rna:
106             cls_id = "%s.%s" % (bl_rna.identifier, cls_id)
107             bl_rna = bl_rna.base
108         return cls_id
109
110     cls_list = type(bpy.context).__base__.__subclasses__()
111     cls_list.sort(key=full_class_id)
112     for cls in cls_list:
113         walkClass(cls)
114
115     cls_list = bpy.types.Space.__subclasses__()
116     cls_list.sort(key=full_class_id)
117     for cls in cls_list:
118         walkClass(cls)
119
120     cls_list = bpy.types.Operator.__subclasses__()
121     cls_list.sort(key=full_class_id)
122     for cls in cls_list:
123         walkClass(cls)
124
125     cls_list = bpy.types.OperatorProperties.__subclasses__()
126     cls_list.sort(key=full_class_id)
127     for cls in cls_list:
128         walkClass(cls)
129
130     cls_list = bpy.types.Menu.__subclasses__()
131     cls_list.sort(key=full_class_id)
132     for cls in cls_list:
133         walkClass(cls)
134
135     from bpy_extras.keyconfig_utils import KM_HIERARCHY
136
137     walk_keymap_hierarchy(KM_HIERARCHY, "KM_HIERARCHY")
138
139
140 def dump_messages_pytext(messages):
141     """ dumps text inlined in the python user interface: eg.
142
143         layout.prop("someprop", text="My Name")
144     """
145     import ast
146
147     # -------------------------------------------------------------------------
148     # Gather function names
149
150     import bpy
151     # key: func_id
152     # val: [(arg_kw, arg_pos), (arg_kw, arg_pos), ...]
153     func_translate_args = {}
154
155     # so far only 'text' keywords, but we may want others translated later
156     translate_kw = ("text", )
157
158     for func_id, func in bpy.types.UILayout.bl_rna.functions.items():
159         # check it has a 'text' argument
160         for (arg_pos, (arg_kw, arg)) in enumerate(func.parameters.items()):
161             if ((arg_kw in translate_kw) and
162                 (arg.is_output == False) and
163                 (arg.type == 'STRING')):
164
165                 func_translate_args.setdefault(func_id, []).append((arg_kw,
166                                                                     arg_pos))
167     # print(func_translate_args)
168
169     # -------------------------------------------------------------------------
170     # Function definitions
171
172     def extract_strings(fp_rel, node_container):
173         """ Recursively get strings, needed incase we have "Blah" + "Blah",
174             passed as an argument in that case it wont evaluate to a string.
175         """
176
177         for node in ast.walk(node_container):
178             if type(node) == ast.Str:
179                 eval_str = ast.literal_eval(node)
180                 if eval_str:
181                     # print("%s:%d: %s" % (fp, node.lineno, eval_str))
182                     msgsrc = "%s:%s" % (fp_rel, node.lineno)
183                     messages.setdefault(eval_str, []).append(msgsrc)
184
185     def extract_strings_from_file(fp):
186         filedata = open(fp, 'r', encoding="utf8")
187         root_node = ast.parse(filedata.read(), fp, 'exec')
188         filedata.close()
189
190         fp_rel = os.path.relpath(fp, SOURCE_DIR)
191
192         for node in ast.walk(root_node):
193             if type(node) == ast.Call:
194                 # print("found function at")
195                 # print("%s:%d" % (fp, node.lineno))
196
197                 # lambda's
198                 if type(node.func) == ast.Name:
199                     continue
200
201                 # getattr(self, con.type)(context, box, con)
202                 if not hasattr(node.func, "attr"):
203                     continue
204
205                 translate_args = func_translate_args.get(node.func.attr, ())
206
207                 # do nothing if not found
208                 for arg_kw, arg_pos in translate_args:
209                     if arg_pos < len(node.args):
210                         extract_strings(fp_rel, node.args[arg_pos])
211                     else:
212                         for kw in node.keywords:
213                             if kw.arg == arg_kw:
214                                 extract_strings(fp_rel, kw.value)
215
216     # -------------------------------------------------------------------------
217     # Dump Messages
218
219     mod_dir = os.path.join(SOURCE_DIR,
220                            "release",
221                            "scripts",
222                            "startup",
223                            "bl_ui")
224
225     files = [os.path.join(mod_dir, fn)
226              for fn in sorted(os.listdir(mod_dir))
227              if not fn.startswith("_")
228              if fn.endswith("py")
229              ]
230
231     for fp in files:
232         extract_strings_from_file(fp)
233
234
235 def dump_messages():
236
237     def filter_message(msg):
238
239         # check for strings like ": %d"
240         msg_test = msg
241         for ignore in ("%d", "%s", "%r",  # string formatting
242                        "*", ".", "(", ")", "-", "/", "\\", "+", ":", "#", "%"
243                        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
244                        "x",  # used on its own eg: 100x200
245                        "X", "Y", "Z",  # used alone. no need to include
246                        ):
247             msg_test = msg_test.replace(ignore, "")
248         msg_test = msg_test.strip()
249         if not msg_test:
250             # print("Skipping: '%s'" % msg)
251             return True
252
253         # we could filter out different strings here
254
255         return False
256
257     if 1:
258         import collections
259         messages = collections.OrderedDict()
260     else:
261         messages = {}
262
263     messages[""] = []
264
265     # get strings from RNA
266     dump_messages_rna(messages)
267
268     # get strings from UI layout definitions text="..." args
269     dump_messages_pytext(messages)
270
271     del messages[""]
272
273     message_file = open(FILE_NAME_MESSAGES, 'w', encoding="utf8")
274     # message_file.writelines("\n".join(sorted(messages)))
275
276     for key, value in messages.items():
277
278         # filter out junk values
279         if filter_message(key):
280             continue
281
282         for msgsrc in value:
283             message_file.write("%s%s\n" % (COMMENT_PREFIX, msgsrc))
284         message_file.write("%s\n" % key)
285
286     message_file.close()
287
288     print("Written %d messages to: %r" % (len(messages), FILE_NAME_MESSAGES))
289
290
291 def main():
292
293     try:
294         import bpy
295     except ImportError:
296         print("This script must run from inside blender")
297         return
298
299     dump_messages()
300
301
302 if __name__ == "__main__":
303     print("\n\n *** Running %r *** \n" % __file__)
304     main()