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