1/*
2 * Copyright © 2016 Software Freedom Conservancy Inc.
3 * Copyright © 2020 Michael Gratton <mike@vee.net>
4 *
5 * This software is licensed under the GNU Lesser General Public License
6 * (version 2.1 or later). See the COPYING file in this distribution.
7 */
8
9extern const string _LANGUAGE_SUPPORT_DIRECTORY;
10extern const string _ISO_CODE_639_XML;
11extern const string _ISO_CODE_3166_XML;
12
13/**
14 * Internationalisation support functions.
15 */
16namespace Util.I18n {
17
18    private GLib.HashTable<string, string> language_names = null;
19    private GLib.HashTable<string, string> country_names = null;
20
21    public const string SYSTEM_LOCALE = "";
22
23    void init(string package_name, string program_path, string locale = SYSTEM_LOCALE) {
24        Intl.setlocale(LocaleCategory.ALL, locale);
25        Intl.bindtextdomain(package_name, get_langpack_dir_path(program_path));
26        Intl.bind_textdomain_codeset(package_name, "UTF-8");
27        Intl.textdomain(package_name);
28    }
29
30    // TODO: Geary should be able to use langpacks from the build directory
31    private string get_langpack_dir_path(string program_path) {
32        return _LANGUAGE_SUPPORT_DIRECTORY;
33    }
34
35    /**
36     * Returns a sorted list of installed spell-check dictionary languages.
37     *
38     * Each language is a POSIX-style locale name (ISO/IEC 15897).
39     * The list is generated by obtaining the list from Enchant via
40     * {@link Enchant.Broker.list_dicts}, discarding generic languages
41     * if a regional variant exists (for example, discard "en" if
42     * "en_US" is included), then sorted.
43     */
44    public string[] get_available_dictionaries() {
45        string[] dictionaries = {};
46
47        Enchant.Broker broker = new Enchant.Broker();
48        broker.list_dicts((lang_tag, provider_name, provider_desc, provider_file) => {
49                dictionaries += lang_tag;
50            });
51
52        // Whenever regional variants of the dictionaries are available use them
53        // in place of the generic ones, e.g., discard en if en_US, en_GB, ...
54        // are installed on the system.
55        GLib.GenericSet<string> regional_dictionaries =
56        new GLib.GenericSet<string>(GLib.str_hash, GLib.str_equal);
57        foreach (string dic in dictionaries) {
58            if ("_" in dic) {
59                int underscore = dic.index_of_char('_');
60                regional_dictionaries.add(dic.substring(0, underscore));
61            }
62        }
63
64        GLib.List<string> filtered_dictionaries = new GLib.List<string>();
65        foreach (string dic in dictionaries) {
66            if ("_" in dic || ! regional_dictionaries.contains(dic))
67                filtered_dictionaries.append(dic);
68        }
69
70        filtered_dictionaries.sort((dic_a, dic_b) => (dic_a < dic_b) ? -1 : 1);
71
72        dictionaries = {};
73        foreach (string dic in filtered_dictionaries) {
74            dictionaries += dic;
75        }
76
77        return dictionaries;
78    }
79
80    /**
81     * Returns a list of installed locale languages.
82     *
83     * Each language is a POSIX-style locale name (ISO/IEC 15897).
84     * The list is generated by executing `locale -a`.
85     */
86    public string[] get_available_locales() {
87        string[] locales = {};
88
89        try {
90            string? output = null;
91            GLib.Subprocess p = new GLib.Subprocess.newv({ "locale", "-a" },
92                                                         GLib.SubprocessFlags.STDOUT_PIPE);
93            p.communicate_utf8(null, null, out output, null);
94
95            foreach (string l in output.split("\n")) {
96                locales += l;
97            }
98        } catch (GLib.Error e) {
99            return locales;
100        }
101
102        return locales;
103    }
104
105    /*
106     * Strip the information about the encoding from the locale.
107     *
108     * That is, en_US.UTF-8 is mapped to en_US, while en_GB remains
109     * unchanged.
110     */
111    public string strip_encoding(string locale) {
112        int dot = locale.index_of_char('.');
113        return locale.substring(0, dot);
114    }
115
116    /**
117     * Returns a list of preferred spell-check languages.
118     *
119     * Each language is a POSIX-style locale name (ISO/IEC 15897).
120     * The list is generated by obtaining the list of POSIX locales
121     * specified by the `LANGUAGE` environment variable, and including
122     * each that has both an available dictionary and locale.
123     *
124     * @see get_available_dictionaries
125     * @see get_available_locales
126     */
127    public string[] get_user_preferred_languages() {
128        GLib.GenericSet<string> dicts = new GLib.GenericSet<string>(GLib.str_hash, GLib.str_equal);
129        foreach (string dict in get_available_dictionaries()) {
130            dicts.add(dict);
131        }
132
133        GLib.GenericSet<string> locales = new GLib.GenericSet<string>(GLib.str_hash, GLib.str_equal);
134        foreach (string locale in get_available_locales()) {
135            locales.add(strip_encoding(locale));
136        }
137
138        string[] output = {};
139        unowned string[] language_names = GLib.Intl.get_language_names();
140        foreach (string lang in language_names) {
141            // Check if we have the associated locale and the dictionary installed before actually
142            //  considering this language.
143            if (lang != "C" && dicts.contains(lang) && locales.contains(lang)) {
144                output += lang;
145            }
146        }
147        return output;
148    }
149
150    public string? language_name_from_locale (string locale) {
151        if (language_names == null) {
152            language_names = new HashTable<string, string>(GLib.str_hash, GLib.str_equal);
153
154            unowned Xml.Doc doc = Xml.Parser.parse_file(_ISO_CODE_639_XML);
155            if (doc == null) {
156                return null;
157            }
158            else {
159                unowned Xml.Node root = doc.get_root_element();
160                for (unowned Xml.Node entry = root.children; entry != null; entry = entry.next) {
161                    if (entry.type == Xml.ElementType.ELEMENT_NODE) {
162                        string? iso_639_1 = null;
163                        string? language_name = null;
164
165                        for (unowned Xml.Attr a = entry.properties; a != null; a = a.next) {
166                            switch (a.name) {
167                            case "iso_639_1_code":
168                                iso_639_1 = a.children->content;
169                                break;
170                            case "name":
171                                language_name = a.children->content;
172                                break;
173                            default:
174                                break;
175                            }
176
177                            if (language_name != null) {
178                                if (iso_639_1 != null) {
179                                    language_names.insert(iso_639_1, language_name);
180                                }
181                            }
182                        }
183                    }
184                }
185            }
186        }
187
188        // Look for the name of language matching only the part before the _
189        int pos = -1;
190        if ("_" in locale) {
191            pos = locale.index_of_char('_');
192        }
193
194        // Return a translated version of the language.
195        string language_name = GLib.dgettext("iso_639", language_names.get(locale.substring(0, pos)));
196
197        return language_name;
198    }
199
200    public string? country_name_from_locale(string locale) {
201        if (country_names == null) {
202            country_names = new HashTable<string, string>(GLib.str_hash, GLib.str_equal);
203
204            unowned Xml.Doc doc = Xml.Parser.parse_file(_ISO_CODE_3166_XML);
205
206            if (doc == null) {
207                return null;
208            }
209            else {
210                unowned Xml.Node root = doc.get_root_element();
211                for (unowned Xml.Node entry = root.children; entry != null; entry = entry.next) {
212                    if (entry.type == Xml.ElementType.ELEMENT_NODE) {
213                        string? iso_3166 = null;
214                        string? country_name = null;
215
216                        for (unowned Xml.Attr a = entry.properties; a != null; a = a.next) {
217                            switch (a.name) {
218                            case "alpha_2_code":
219                                iso_3166 = a.children->content;
220                                break;
221                            case "name":
222                                country_name = a.children->content;
223                                break;
224                            default:
225                                break;
226                            }
227
228                            if (country_name != null) {
229                                if (iso_3166 != null) {
230                                    country_names.insert(iso_3166, country_name);
231                                }
232                            }
233                        }
234                    }
235                }
236            }
237        }
238
239        // Look for the name of language matching only the part before the _
240        int pos = -1;
241        if ("_" in locale) {
242            pos = locale.index_of_char('_');
243        }
244
245        string country_name  = GLib.dgettext("iso_3166", country_names.get(locale.substring(pos+1)));
246
247        return country_name;
248    }
249
250    /**
251     * Returns the localised display name name for specific folder.
252     *
253     * If the folder has a special type, the result of {@link
254     * to_folder_type_display_name} is returned, otherwise the last
255     * folder path step is returned.
256     */
257    public string? to_folder_display_name(Geary.Folder folder) {
258        var name = to_folder_type_display_name(folder.used_as);
259        if (Geary.String.is_empty_or_whitespace(name)) {
260            name = folder.path.name;
261        }
262        return name;
263    }
264
265    /**
266     * Returns the localised name for a specific folder type, if any.
267     */
268    public unowned string? to_folder_type_display_name(Geary.Folder.SpecialUse use) {
269        switch (use) {
270            case INBOX:
271                return _("Inbox");
272
273            case DRAFTS:
274                return _("Drafts");
275
276            case SENT:
277                return _("Sent");
278
279            case FLAGGED:
280                return _("Starred");
281
282            case IMPORTANT:
283                return _("Important");
284
285            case ALL_MAIL:
286                return _("All Mail");
287
288            case JUNK:
289                return _("Junk");
290
291            case TRASH:
292                return _("Trash");
293
294            case OUTBOX:
295                return _("Outbox");
296
297            case SEARCH:
298                return _("Search");
299
300            case ARCHIVE:
301                return _("Archive");
302
303            case NONE:
304            default:
305                return null;
306        }
307    }
308
309}
310