1 /*
2  *    This file is part of darktable,
3  *    Copyright (C) 2018-2020 darktable developers.
4  *
5  *    darktable is free software: you can redistribute it and/or modify
6  *    it under the terms of the GNU General Public License as published by
7  *    the Free Software Foundation, either version 3 of the License, or
8  *    (at your option) any later version.
9  *
10  *    darktable is distributed in the hope that it will be useful,
11  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *    GNU General Public License for more details.
14  *
15  *    You should have received a copy of the GNU General Public License
16  *    along with darktable.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "common/l10n.h"
20 #include "common/file_location.h"
21 #include "control/conf.h"
22 
23 #include <libintl.h>
24 #include <locale.h>
25 #include <gtk/gtk.h>
26 #include <json-glib/json-glib.h>
27 
28 #ifdef __APPLE__
29 #include "osx/osx.h"
30 #endif
31 
32 #ifdef _WIN32
33 #include "win/dtwin.h"
34 #include <windows.h>
35 #endif
36 
_dt_full_locale_name(const char * locale)37 static gchar* _dt_full_locale_name(const char *locale)
38 {
39 #if defined(__linux__) || defined(__APPLE__)
40   gchar *output = NULL;
41   GError *error = NULL;
42   if(!g_spawn_command_line_sync("locale -a", &output, NULL, NULL, &error))
43   {
44     if(error)
45     {
46       fprintf(stderr, "couldn't check locale: '%s'\n", error->message);
47       g_error_free(error);
48     }
49   }
50   else
51   {
52     if(output)
53     {
54       gchar **locales = g_strsplit (output, "\n", -1);
55       g_free(output);
56       int j = 0;
57       while(locales[j])
58       {
59         if(g_str_has_prefix(locales[j], locale))
60         {
61           // return first found variant - this is most likelly the best one
62           gchar *ret=g_strdup(locales[j]);
63           g_strfreev(locales);
64           return ret;
65         }
66         j++;
67       }
68     }
69   }
70   return NULL;
71 #else
72   // TODO: check a way to do above on windows
73   return NULL;
74 #endif
75 }
76 
set_locale(const char * ui_lang,const char * old_env)77 static void set_locale(const char *ui_lang, const char *old_env)
78 {
79   if(ui_lang && *ui_lang)
80   {
81     gchar *full_locale = _dt_full_locale_name(ui_lang);
82     if(full_locale)
83     {
84       g_setenv("LANG", full_locale, TRUE);
85       g_free(full_locale);
86     }
87     g_setenv("LANGUAGE", ui_lang, TRUE);
88     gtk_disable_setlocale();
89   }
90   else if(old_env && *old_env)
91     g_setenv("LANGUAGE", old_env, TRUE);
92   else
93     g_unsetenv("LANGUAGE");
94 
95   setlocale(LC_ALL, "");
96 }
97 
sort_languages(gconstpointer a,gconstpointer b)98 static gint sort_languages(gconstpointer a, gconstpointer b)
99 {
100   gchar *name_a = g_utf8_casefold(dt_l10n_get_name((const dt_l10n_language_t *)a), -1);
101   gchar *name_b = g_utf8_casefold(dt_l10n_get_name((const dt_l10n_language_t *)b), -1);
102 
103   int result = g_strcmp0(name_a, name_b);
104 
105   g_free(name_a);
106   g_free(name_b);
107 
108   return result;
109 }
110 
get_language_names(GList * languages)111 static void get_language_names(GList *languages)
112 {
113 #ifdef HAVE_ISO_CODES
114 
115   JsonReader *reader = NULL;
116   JsonParser *parser = NULL;
117   GError *error = NULL;
118   char *filename = NULL;
119 #ifdef __APPLE__
120   char *res_path = dt_osx_get_bundle_res_path();
121 #endif
122 
123 #if defined(_WIN32) && !defined(MSYS2_INSTALL)
124   char datadir[PATH_MAX] = { 0 };
125   dt_loc_get_datadir(datadir, sizeof(datadir));
126   filename = g_build_filename(datadir, "..",  "iso-codes", "json", "iso_639-2.json", NULL);
127 #else
128 #ifdef __APPLE__
129   if(res_path)
130     filename = g_build_filename(res_path, "share",  "iso-codes", "json", "iso_639-2.json", NULL);
131   else
132 #endif
133   filename = g_build_filename(ISO_CODES_LOCATION, "iso_639-2.json", NULL);
134 #endif
135 
136   if(!g_file_test(filename, G_FILE_TEST_EXISTS))
137   {
138     fprintf(stderr, "[l10n] error: can't open iso-codes file `%s'\n"
139                     "       there won't be nicely translated language names in the preferences.\n", filename);
140     goto end;
141   }
142 
143 #if defined(_WIN32) && !defined(MSYS2_INSTALL)
144   // on windows we are shipping the translations of iso-codes along ours
145   char localedir[PATH_MAX] = { 0 };
146   dt_loc_get_localedir(localedir, sizeof(localedir));
147   bindtextdomain("iso_639", localedir);
148 #else
149 #ifdef __APPLE__
150   if(res_path)
151   {
152     char localedir[PATH_MAX] = { 0 };
153     dt_loc_get_localedir(localedir, sizeof(localedir));
154     bindtextdomain("iso_639", localedir);
155   }
156   else
157 #endif
158   bindtextdomain("iso_639", ISO_CODES_LOCALEDIR);
159 #endif
160 
161   bind_textdomain_codeset("iso_639", "UTF-8");
162 
163   parser = json_parser_new();
164   if(!json_parser_load_from_file(parser, filename, &error))
165   {
166     fprintf(stderr, "[l10n] error: parsing json from `%s' failed\n%s\n", filename, error->message);
167     goto end;
168   }
169 
170   // go over the json
171   JsonNode *root = json_parser_get_root(parser);
172   if(!root)
173   {
174     fprintf(stderr, "[l10n] error: can't get root node of `%s'\n", filename);
175     goto end;
176   }
177 
178   reader = json_reader_new(root);
179 
180   if(!json_reader_read_member(reader, "639-2"))
181   {
182     fprintf(stderr, "[l10n] error: unexpected layout of `%s'\n", filename);
183     goto end;
184   }
185 
186   if(!json_reader_is_array(reader))
187   {
188     fprintf(stderr, "[l10n] error: unexpected layout of `%s'\n", filename);
189     goto end;
190   }
191 
192   char *saved_locale = strdup(setlocale(LC_ALL, NULL));
193 
194   int n_elements = json_reader_count_elements(reader);
195   for(int i = 0; i < n_elements; i++)
196   {
197     json_reader_read_element(reader, i);
198     if(!json_reader_is_object(reader))
199     {
200       fprintf(stderr, "[l10n] error: unexpected layout of `%s' (element %d)\n", filename, i);
201       free(saved_locale);
202       saved_locale = NULL;
203       goto end;
204     }
205 
206     const char *alpha_2 = NULL, *alpha_3 = NULL, *name = NULL;
207     if(json_reader_read_member(reader, "alpha_2"))
208       alpha_2 = json_reader_get_string_value(reader);
209     json_reader_end_member(reader); // alpha_2
210 
211     if(json_reader_read_member(reader, "alpha_3"))
212       alpha_3 = json_reader_get_string_value(reader);
213     json_reader_end_member(reader); // alpha_3
214 
215     if(json_reader_read_member(reader, "name"))
216       name = json_reader_get_string_value(reader);
217     json_reader_end_member(reader); // name
218 
219     if(name && (alpha_2 || alpha_3))
220     {
221       // check if alpha_2 or alpha_3 is in our translations
222       for(GList *iter = languages; iter; iter = g_list_next(iter))
223       {
224         dt_l10n_language_t *language = (dt_l10n_language_t *)iter->data;
225         if(!g_strcmp0(language->base_code, alpha_2) || !g_strcmp0(language->base_code, alpha_3))
226         {
227           // code taken in parts from GIMP's gimplanguagestore-parser.c
228           g_setenv("LANGUAGE", language->code, TRUE);
229           setlocale (LC_ALL, language->code);
230 
231           char *localized_name = g_strdup(dgettext("iso_639", name));
232 
233           /* If original and localized names are the same for other than English,
234            * maybe localization failed. Try now in the main dialect. */
235           if(g_strcmp0(name, localized_name) == 0 &&
236              g_strcmp0(language->code, language->base_code) != 0)
237           {
238             g_free(localized_name);
239 
240             g_setenv("LANGUAGE", language->base_code, TRUE);
241             setlocale (LC_ALL, language->base_code);
242 
243             localized_name = g_strdup(dgettext("iso_639", name));
244           }
245 
246           /*  there might be several language names; use the first one  */
247           char *semicolon = strchr(localized_name, ';');
248 
249           if(semicolon)
250           {
251             char *tmp = localized_name;
252             localized_name = g_strndup(localized_name, semicolon - localized_name);
253             g_free(tmp);
254           }
255 
256           // we initialize the name to the language code to have something on systems lacking iso-codes, so free it!
257           g_free(language->name);
258           language->name = g_strdup_printf("%s (%s)%s", localized_name, language->code, language->is_default ? " *" : "");
259           g_free(localized_name);
260 
261           // we can't break out of the loop here. at least pt is in our list twice!
262         }
263       }
264     }
265     else
266       fprintf(stderr, "[l10n] error: element %d has no name, skipping\n", i);
267 
268     json_reader_end_element(reader);
269   }
270 
271   if(saved_locale)
272   {
273     setlocale(LC_ALL, saved_locale);
274     free(saved_locale);
275     saved_locale = NULL;
276   }
277 
278   json_reader_end_member(reader); // 639-2
279 
280 end:
281   // cleanup
282 #ifdef __APPLE__
283   g_free(res_path);
284 #endif
285   g_free(filename);
286   if(error) g_error_free(error);
287   if(reader) g_object_unref(reader);
288   if(parser) g_object_unref(parser);
289 
290 #endif // HAVE_ISO_CODES
291 }
292 
dt_l10n_init(gboolean init_list)293 dt_l10n_t *dt_l10n_init(gboolean init_list)
294 {
295   dt_l10n_t *result = (dt_l10n_t *)calloc(1, sizeof(dt_l10n_t));
296   result->selected = -1;
297   result->sys_default = -1;
298 
299   char *ui_lang = dt_conf_get_string("ui_last/gui_language");
300   const char *old_env = g_getenv("LANGUAGE");
301 
302 #if defined(_WIN32)
303   // get the default locale if no language preference was specified in the config file
304   if(!ui_lang || !*ui_lang)
305   {
306     const wchar_t *wcLocaleName = NULL;
307     wcLocaleName = dtwin_get_locale();
308     if(wcLocaleName != NULL)
309     {
310       gchar *langLocale;
311       langLocale = g_utf16_to_utf8(wcLocaleName, -1, NULL, NULL, NULL);
312       if(langLocale != NULL)
313       {
314         g_free(ui_lang);
315         ui_lang = g_strdup(langLocale);
316       }
317     }
318   }
319 #endif // defined (_WIN32)
320 
321 
322   // prepare the list of available gui translations from which the user can pick in prefs
323   if(init_list)
324   {
325     dt_l10n_language_t *selected = NULL;
326     dt_l10n_language_t *sys_default = NULL;
327 
328     dt_l10n_language_t *language = (dt_l10n_language_t *)calloc(1, sizeof(dt_l10n_language_t));
329     language->code = g_strdup("C");
330     language->base_code = g_strdup("C");
331     language->name = g_strdup("English");
332     result->languages = g_list_append(result->languages, language);
333 
334     if(g_strcmp0(ui_lang, "C") == 0) selected = language;
335 
336     const gchar * const * default_languages = g_get_language_names();
337 
338     char localedir[PATH_MAX] = { 0 };
339     dt_loc_get_localedir(localedir, sizeof(localedir));
340     GDir *dir = g_dir_open(localedir, 0, NULL);
341     if(dir)
342     {
343       const gchar *locale;
344       while((locale = g_dir_read_name(dir)))
345       {
346         gchar *testname = g_build_filename(localedir, locale, "LC_MESSAGES", GETTEXT_PACKAGE ".mo", NULL);
347         if(g_file_test(testname, G_FILE_TEST_EXISTS))
348         {
349           language = (dt_l10n_language_t *)calloc(1, sizeof(dt_l10n_language_t));
350           result->languages = g_list_prepend(result->languages, language);
351 
352           // some languages have a regional part in the filename, we don't want that for name lookup
353           char *delimiter = strchr(locale, '_');
354           if(delimiter)
355             language->base_code = g_strndup(locale, delimiter - locale);
356           else
357             language->base_code = g_strdup(locale);
358           delimiter = strchr(language->base_code, '@');
359           if(delimiter)
360           {
361             char *tmp = language->base_code;
362             language->base_code = g_strndup(language->base_code, delimiter - language->base_code);
363             g_free(tmp);
364           }
365 
366           // check if this is the system default
367           if(sys_default == NULL)
368           {
369             for(const gchar * const * iter = default_languages; *iter; iter++)
370             {
371               if(g_strcmp0(*iter, locale) == 0)
372               {
373                 language->is_default = TRUE;
374                 sys_default = language;
375                 break;
376               }
377             }
378           }
379 
380           language->code = g_strdup(locale);
381           language->name = g_strdup_printf("%s%s", locale, language->is_default ? " *" : "");
382 
383           if(g_strcmp0(ui_lang, language->code) == 0)
384             selected = language;
385         }
386         g_free(testname);
387       }
388       g_dir_close(dir) ;
389     }
390     else
391       fprintf(stderr, "[l10n] error: can't open directory `%s'\n", localedir);
392 
393     // default to English if no other language matched
394     if(!sys_default)
395     {
396       sys_default = g_list_last(result->languages)->data;
397       sys_default->is_default = TRUE;
398       gchar* name = sys_default->name;
399       sys_default->name = g_strdup_printf("%s *", name);
400       g_free(name);
401     }
402 
403     // now try to find language names and translations!
404     get_language_names(result->languages);
405 
406     // set the requested gui language.
407     // this has to happen before sorting the list as the sort result may depend on the language.
408     set_locale(ui_lang, old_env);
409 
410     // sort the list of languages
411     result->languages = g_list_sort(result->languages, sort_languages);
412 
413     // find the index of the selected and default languages
414     int i = 0;
415     for(GList *iter = result->languages; iter; iter = g_list_next(iter))
416     {
417       if(iter->data == sys_default) result->sys_default = i;
418       if(iter->data == selected) result->selected = i;
419       i++;
420     }
421 
422     if(selected == NULL)
423       result->selected = result->sys_default;
424   }
425   else
426     set_locale(ui_lang, old_env);
427 
428   g_free(ui_lang);
429 
430   return result;
431 }
432 
dt_l10n_get_name(const dt_l10n_language_t * language)433 const char *dt_l10n_get_name(const dt_l10n_language_t *language)
434 {
435   if(!language) return NULL;
436 
437   return language->name ? language->name : language->code;
438 }
439 
440 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
441 // vim: shiftwidth=2 expandtab tabstop=2 cindent
442 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
443