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