1#!/usr/local/bin/python3.8
2
3import os
4import json
5import subprocess
6import collections
7import argparse
8
9LOCALE_DIR = os.path.join(os.path.expanduser('~'), '.local/share/locale')
10
11USAGE_DESCRIPTION = """\
12Parses files in an applet, desklet or extension's directories, \
13extracts translatable strings, and then generates a .pot file for them.
14"""
15
16USAGE_EPILOG = """\
17For example:
18cinnamon-xlet-makepot example@mydomain.org/
19
20Will generate a file called example@mydomain.org.pot, in the directory \
21example@mydomain.org/po/ which contains all of the strings that need to be \
22translated. Translators can then use this pot file to generate .po \
23files which contain translations for the extracted strings.
24"""
25
26try:
27    import polib
28except Exception:
29    print("""\
30
31Module "polib" not available.
32
33You will need to install this module using your distribution's package manager
34(in debian-based syatems "apt-get install python3-polib")
35""")
36    quit()
37
38
39def scan_json(dir, pot_path):
40    append = os.path.exists(pot_path)
41    if append:
42        pot_file = polib.pofile(pot_path)
43    else:
44        pot_file = polib.POFile()
45
46    for root, dirs, files in os.walk(dir):
47        rel_root = os.path.relpath(root)
48        for file in files:
49            if rel_root == '.':
50                rel_path = file
51            else:
52                rel_path = os.path.join(rel_root, file)
53            if file == 'settings-schema.json':
54                fp = open(os.path.join(root, file))
55                data = json.load(fp, object_pairs_hook=collections.OrderedDict)
56                fp.close()
57                extract_settings_strings(data, rel_path.replace('/', '->'), pot_file)
58            elif file == 'metadata.json':
59                fp = open(os.path.join(root, file))
60                data = json.load(fp, object_pairs_hook=collections.OrderedDict)
61                fp.close()
62                extract_metadata_strings(data, rel_path.replace('/', '->'), pot_file)
63
64    if append:
65        pot_file.save(pot_path)
66    else:
67        pot_file.save(fpath=pot_path)
68
69
70def extract_settings_strings(data, dir, pot_file, parent=""):
71    for key in data.keys():
72        if key in ("description", "tooltip", "units", "title"):
73            comment = "%s->%s->%s" % (dir, parent, key)
74            save_entry(data[key], comment, pot_file)
75        elif key in "options":
76            opt_data = data[key]
77            for option in opt_data.keys():
78                if opt_data[option] == "custom":
79                    continue
80                comment = "%s->%s->%s" % (dir, parent, key)
81                save_entry(option, comment, pot_file)
82        elif key == "columns":
83            columns = data[key]
84            for i, col in enumerate(columns):
85                for col_key in col:
86                    if col_key in ("title", "units"):
87                        comment = "%s->%s->columns->%s" % (dir, parent, col_key)
88                        save_entry(col[col_key], comment, pot_file)
89        try:
90            extract_settings_strings(data[key], dir, pot_file, key)
91        except AttributeError:
92            pass
93
94
95def extract_metadata_strings(data, dir, pot_file):
96    for key in data:
97        if key in ("name", "description", "comments"):
98            comment = "%s->%s" % (dir, key)
99            save_entry(data[key], comment, pot_file)
100        elif key == "contributors":
101            comment = "%s->%s" % (dir, key)
102
103            values = data[key]
104            if isinstance(values, str):
105                values = values.split(",")
106
107            for value in values:
108                save_entry(value.strip(), comment, pot_file)
109
110
111def save_entry(msgid, comment, pot_file):
112    if not msgid.strip():
113        return
114
115    entry = pot_file.find(msgid)
116    if entry:
117        if comment not in entry.comment:
118            if entry.comment:
119                entry.comment += "\n"
120            entry.comment += comment
121    else:
122        entry = polib.POEntry(msgid=msgid, comment=comment)
123        pot_file.append(entry)
124
125
126def remove_empty_folders(path):
127    if not os.path.isdir(path):
128        return
129
130    # remove empty subfolders
131    files = os.listdir(path)
132    if len(files):
133        for f in files:
134            fullpath = os.path.join(path, f)
135            if os.path.isdir(fullpath):
136                remove_empty_folders(fullpath)
137
138    # if folder empty, delete it
139    files = os.listdir(path)
140    if len(files) == 0:
141        print('Removing empty folder:', path)
142        os.rmdir(path)
143
144
145def do_install(uuid, dir):
146    podir = os.path.join(dir, "po")
147    files_installed = 0
148    for root, subFolders, files in os.walk(podir):
149        for file in files:
150            locale_name, ext = os.path.splitext(file)
151            if ext == '.po':
152                lang_locale_dir = os.path.join(LOCALE_DIR, locale_name, 'LC_MESSAGES')
153                os.makedirs(lang_locale_dir, mode=0o755, exist_ok=True)
154                subprocess.call(["msgfmt", "-c", os.path.join(root, file), "-o", os.path.join(lang_locale_dir, '%s.mo' % uuid)])
155                files_installed += 1
156    if files_installed == 0:
157        print('Nothing to install')
158    else:
159        print('installed %i files' % files_installed)
160
161
162def do_remove(uuid):
163    files_removed = 0
164    if (os.path.exists(LOCALE_DIR)):
165        locale_names = os.listdir(LOCALE_DIR)
166        for locale_name in locale_names:
167            lang_locale_dir = os.path.join(LOCALE_DIR, locale_name)
168            mo_file = os.path.join(lang_locale_dir, 'LC_MESSAGES', "%s.mo" % uuid)
169            if os.path.isfile(mo_file):
170                os.remove(mo_file)
171                files_removed += 1
172            remove_empty_folders(lang_locale_dir)
173    if files_removed == 0:
174        print("Nothing to remove")
175    else:
176        print('removed %i files' % files_removed)
177
178
179def scan_xlet():
180    parser = argparse.ArgumentParser(description=USAGE_DESCRIPTION, epilog=USAGE_EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
181    parser.add_argument('-j', '--skip-js', action='store_false', dest='js', default=True,
182                        help='If this option is not included, javascript files will be scanned for translatable strings.')
183    parser.add_argument('-p', '--skip-python', action='store_false', dest='python', default=True,
184                        help='If this option is not included, python files will be scanned for translatable strings.')
185    parser.add_argument('-i', '--install', action='store_true', dest='install', default=False,
186                        help=('Compiles and installs any .po files contained in a po folder to the system locale store. '
187                              'Use this option to test your translations locally before uploading to Spices. '
188                              'It will use the applet, desklet, or extension UUID as the translation domain.'))
189    parser.add_argument('-r', '--remove', action='store_true', dest='remove', default=False,
190                        help=('The opposite of install, removes translations from the store. Again, it uses the UUID '
191                              'to find the correct files to remove.'))
192    parser.add_argument('-k', '--keyword', dest='keyword', default='_',
193                        help='Change the variable name gettext is assigned to in your files. The default is _.')
194    parser.add_argument('-o', '--potfile', dest='out_file', default=None,
195                        help=('Use this option to specify the location for the generated .pot file. By default '
196                              '<uuid>/po/<uuid>.pot is used. This is where translators are expecting to find the pot '
197                              'file, so you should only use this option if you want the pot file elsewhere for your own use.'))
198    parser.add_argument('dir', help='the path to the applet/desklet/extension directory')
199
200    args = parser.parse_args()
201
202    dir = os.path.abspath(args.dir)
203    if not os.path.exists(dir):
204        print('%s does not exist' % dir)
205        quit()
206
207    uuid = os.path.basename(dir)
208
209    if os.path.exists(os.path.join(dir, 'files', uuid)):
210        dir = os.path.join(dir, 'files', uuid)
211
212    if args.install:
213        do_install(uuid, dir)
214        quit()
215
216    if args.remove:
217        do_remove(uuid)
218        quit()
219
220    if args.out_file is not None:
221        pot_path = os.path.abspath(args.out_file)
222    else:
223        pot_path = os.path.join(dir, 'po', uuid + '.pot')
224
225    pwd = os.getcwd()
226    os.chdir(dir)
227    if args.js or args.python:
228        try:
229            subprocess.check_output(["xgettext", "--version"])
230        except OSError:
231            print("xgettext not found, you may need to install the gettext package")
232            quit()
233
234    os.makedirs(os.path.dirname(pot_path), mode=0o755, exist_ok=True)
235
236    pot_exists = False
237    if args.js:
238        print("Scanning JavaScript files...")
239
240        js_files = []
241        for root, dirs, files in os.walk(dir):
242            rel_root = os.path.relpath(root)
243            js_files += [os.path.join(rel_root, file) for file in files if file.endswith('.js')]
244        if len(js_files) == 0:
245            print('none found')
246        else:
247            print('found %i file(s)\n' % len(js_files))
248            command_args = [
249                'xgettext',
250                '--language=JavaScript',
251                '--from-code=UTF-8',
252                '--keyword=%s' % args.keyword,
253                '--output=%s' % pot_path
254            ]
255            command_args += js_files
256            subprocess.run(command_args)
257            pot_exists = True
258
259    if args.python:
260        print("Scanning for python files...")
261
262        py_files = []
263        for root, dirs, files in os.walk(dir):
264            rel_root = os.path.relpath(root)
265            py_files += [os.path.join(rel_root, file) for file in files if file.endswith('.py')]
266        if len(py_files) == 0:
267            print('none found')
268        else:
269            print('found %i file(s)\n' % len(py_files))
270            command_args = [
271                'xgettext',
272                '--language=Python',
273                '--from-code=UTF-8',
274                '--keyword=%s' % args.keyword,
275                '--output=%s' % pot_path
276            ]
277            if pot_exists:
278                command_args.append('-j')
279            command_args += py_files
280            subprocess.run(command_args)
281
282    print("Scanning metadata.json and settings-schema.json...")
283    scan_json(dir, pot_path)
284
285    os.chdir(pwd)
286
287    print("Extraction complete")
288    quit()
289
290
291if __name__ == "__main__":
292    scan_xlet()
293