1#!/usr/local/bin/python3.8 2# 3# languages.py - Read language names from several sources and save them for E:S 4# 5# Copyright (C) 2012 Rodrigo Silva (MestreLion) <linux@rodrigosilva.com> 6# 7# This program is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. See <http://www.gnu.org/licenses/gpl.html> 19 20import sys 21import os.path as osp 22import json 23import argparse 24 25class Locale(object): 26 """Wrapper for a Locale object in various sources, like babel and pyicu""" 27 28 @staticmethod 29 def getAvailableSources(): 30 """Return a list of all available Locale info sources. 31 32 First item is by definition the default source used when none is given. 33 Last item is by definition the game's PO headers 34 """ 35 return ['icu', 'babel', 'pofiles'] 36 37 @staticmethod 38 def getGameTranslations(): 39 """Return a list of available game translations (both ll and ll_CC) 40 41 Generated list is suitable to be used as a filter to 42 getLanguages() method. Do not confuse with the languages 43 data dict that is generated by this module. 44 45 This function is just here as a reference, but I won't use this 46 at all. You may even delete it, I truly don't care. 47 """ 48 game_translations = set() 49 for language in g.available_languages(): 50 game_translations.add(language) 51 if "_" in language: 52 game_translations.add(language.split("_",1)[0]) 53 return dict(game_translations) 54 55 @classmethod 56 def getAvailableLocales(cls, source): 57 """Return a list of all available locale codes for the given source""" 58 59 return cls.dispatch_source_method(source, 'getAvailableLocales', 60 default_func=lambda: []) 61 62 @classmethod 63 def getLocaleNames(cls, source, code): 64 """Return a tuple of locale english name and localized name""" 65 66 return cls.dispatch_source_method(source, 'getLocaleNames', 67 args=(code,), 68 default_func=lambda: None) 69 70 @classmethod 71 def getLanguages(cls, locales=None, tupletype=True, source=None): 72 """Return a languages dict of Locale info from a given source 73 74 locales input is a list of locale codes to be selected among 75 the available ones. Non-available codes are silently ignored. 76 If locales is omitted or empty, output will have all available 77 locales. 78 79 Output languages dict has locale codes as keys, and either a 80 2-tuple or a dict(English, native) as values. 81 82 Values represent the Display Name of the language expressed 83 in English and in native language for each locale code 84 """ 85 if locales: 86 locales = set(locales) & set(cls(source=source).getAvailableLocales()) 87 else: 88 locales = cls.getAvailableLocales(source) 89 90 output = {} 91 for code in locales: 92 names = cls.getLocaleNames(source, code) 93 if (names): 94 english, native = names 95 if not (english or native): continue 96 if native: native = native.title() 97 if tupletype: 98 output[code] = (english, native) 99 else: 100 output[code] = dict(english=english, native=native) 101 102 return output 103 104 @staticmethod 105 def saveLanguagesData(languages, filename): 106 """Docstring... for *this*? You gotta be kidding me... 107 108 Ok... there it goes: Save a languages dict, like the one 109 generated by getLanguages() method, to a JSON data file 110 Happy now? 111 """ 112 with open(filename, 'w') as fd: 113 json.dump(languages, fd, indent=0, separators=(',', ': '), 114 sort_keys=True) 115 fd.write('\n') # add EOF newline 116 117 @staticmethod 118 def loadLanguagesData(filename): 119 """Again? Can't you read code? 120 121 Ok, ok... Load and return a language dict from a JSON data file 122 """ 123 with open(filename) as fd: 124 return json.load(fd) 125 126 127 def __init__(self, code=None, source=None): 128 """Initialize a Locale of the given code from the given source 129 130 If source is blank or omitted, the default one from by 131 getAvailableSources() is used. 132 133 code must be string with ll or ll_CC format. If blank or 134 omitted, only getAvailableLocales() method will work, 135 all other attributes will be None. 136 """ 137 138 # Attributes 139 self.source = source 140 self.code = code 141 self.english_name = None 142 self.native_name = None 143 144 # Handle source 145 if not self.source: self.source = self.getAvailableSources()[0] 146 if self.source not in self.getAvailableSources(): 147 raise ValueError("{0} is not a valid source." 148 " Available sources are: {1}".format( 149 source, ", ".join(self.getAvailableSources()))) 150 self.source = self.source.lower() 151 152 # Override default attributes methods and members according to the given source 153 self.english_name, self.native_name = self.getLocaleNames(source) 154 155 @classmethod 156 def dispatch_source_method(cls, source, func_name, args=(), default_func=lambda: None): 157 import types 158 159 # Create temporary globals. 160 temp_globals = dict(globals()) 161 162 # Execute import before dispatch to update globals with the return. 163 do_import = getattr(cls, str(source) + '_import', lambda *args: None) 164 temp_globals.update(do_import(source)) 165 166 method_name = str(source) + '_' + func_name 167 method = getattr(cls, method_name, default_func) 168 169 temp_func = types.FunctionType(method.__code__, temp_globals, method_name) 170 temp_method = types.MethodType(temp_func, cls, cls.__class__) 171 172 return temp_method(*args) 173 174 # icu dispatched functions 175 176 @classmethod 177 def icu_import(cls, source): 178 try: 179 import icu # new module name, 1.0 onwards 180 except ImportError: 181 try: 182 import PyICU as icu # old module name, up to 0.9 183 except ImportError: 184 raise ImportError("'{0}' requires icu" 185 " or PyICU module".format(source)) 186 return locals() 187 188 @classmethod 189 def icu_getAvailableLocales(cls): 190 return icu.Locale.getAvailableLocales().keys 191 192 @classmethod 193 def icu_getLocaleNames(cls, code): 194 locale = icu.Locale(code) 195 return (locale.getDisplayName(), locale.getDisplayName(locale)) 196 197 # babel dispatched functions 198 199 @classmethod 200 def babel_import(cls, source): 201 try: 202 import babel 203 except ImportError: 204 raise ImportError("'{0}' requires babel module".format(source)) 205 206 return locals() 207 208 @classmethod 209 def babel_getAvailableLocales(cls): 210 return babel.localedata.list 211 212 @classmethod 213 def babel_getLocaleNames(cls, code): 214 locale = babel.Locale.parse(code) 215 return (locale.english_name, locale.get_display_name()) 216 217 # pofiles dispatched functions 218 219 @classmethod 220 def pofiles_import(cls, source): 221 try: 222 import polib 223 except ImportError: 224 import singularity.code.polib as polib 225 226 return locals() 227 228 @classmethod 229 def pofiles_getAvailableLocales(cls): 230 import os as os 231 return [dirname.split("_", 1)[1] 232 for file_dir in dirs.get_read_dirs("i18n") 233 for dirname in os.listdir(file_dir) 234 if osp.isdir(os.path.join(file_dir, dirname)) 235 and dirname.startswith("lang_") 236 and any(filename == "messages.po" and osp.isfile(osp.join(file_dir, dirname, filename)) 237 for filename in os.listdir(os.path.join(file_dir, dirname)))] 238 239 @classmethod 240 def pofiles_getLocaleNames(cls, code): 241 pofile = dirs.get_readable_i18n_files("messages.po", code, localized_item=False, only_last=True) 242 po = polib.pofile(pofile) 243 return (po.metadata['Language-Name'], po.metadata['Language-Native-Name']) 244 245def get_esdir(myname): 246 mydir = osp.dirname(myname) 247 esdir = osp.abspath(osp.join(osp.dirname(myname), '../..')) 248 return esdir 249 250def build_option_parser(): 251 description = '''Read language names from several sources and save them for E:S. 252 253If run as a script, tries to read language names (in both English and 254Native language) from several Locale info sources, like babel or PyICU. Then 255merges this info with E:S' translation PO files headers, and save it by 256updating a JSON file in data/languages.dat, which is later read by Options 257Screen of E:S to present each one of the available languages in their own 258native language. 259 260This module is importable as well. In this case it only sets game's directory 261and imports code.g. You can use the Locale class for all of its features. 262 263In either case, modules 'icu', 'babel' and 'polib' are optional but highly 264recommended. 265''' 266 267 parser = argparse.ArgumentParser(description=description) 268 parser.add_argument("-d", "--directory", dest="directory", default=None, 269 help="Use E:S root directory DIR (default dirname(__file__)/../..)", metavar="DIR") 270 271 return parser.parse_args() 272 273def main(args): 274 dirs.create_directories(False) 275 276 # Get locale data from the first source that works 277 languages = {} 278 sources = Locale.getAvailableSources() 279 for source in sources[:-1]: # we load 'pofiles' later 280 try: 281 languages = Locale.getLanguages(source=source) 282 break 283 except ImportError: 284 continue 285 286 datafile = dirs.get_readable_file_in_dirs("languages.json", "i18n") 287 288 try: 289 # load current data file and merge it 290 languages.update(Locale.loadLanguagesData(datafile)) 291 except IOError: 292 pass 293 294 # also merge with translations file 295 languages.update(Locale.getLanguages(source=sources[-1])) 296 297 try: 298 # Save updated data file 299 Locale.saveLanguagesData(languages, datafile) 300 301 print("{0:d} languages saved to {1}".format(len(languages), 302 osp.relpath(datafile))) 303 except IOError as reason: 304 sys.stderr.write("Could not save languages data file:" 305 " {0}\n".format(reason)) 306 307if __name__ == '__main__': 308 args = build_option_parser() 309 310 if (args.directory): 311 esdir = osp.abspath(args.directory) 312 else: 313 esdir = get_esdir(sys.argv[0]) 314else: 315 esdir = get_esdir(__file__) 316 317sys.path.insert(0, esdir) 318 319try: 320 from singularity.code import g, dirs 321except ImportError: 322 sys.exit("Could not find game's code.g") 323 324if __name__ == '__main__': 325 try: 326 sys.exit(main(args)) 327 except Exception as e: 328 ex_type, ex, tb = sys.exc_info() 329 traceback.print_tb(tb) 330 sys.exit(1) 331 except KeyboardInterrupt: 332 pass 333 except SystemExit as ex: 334 raise ex 335