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