1 /*
2 * go-locale.c :
3 *
4 * Copyright (C) 1998 Chris Lahey, Miguel de Icaza
5 * Copyright (C) 2003-2005 Jody Goldberg (jody@gnome.org)
6 * Copyright (C) 2005-2007 Morten Welinder (terra@gnome.org)
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License as
10 * published by the Free Software Foundation; either version 2 of the
11 * License, or (at your option) version 3.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
21 * USA
22 */
23
24 #include <goffice/goffice-config.h>
25 #include "go-locale.h"
26 #include <glib/gi18n-lib.h>
27 #ifdef HAVE_LANGINFO_H
28 # include <langinfo.h>
29 #endif
30 #ifdef G_OS_WIN32
31 # include <windows.h>
32 #endif
33 #include <string.h>
34
35 /*
36 * Points to the locale information for number display. All strings are
37 * in UTF-8 encoding.
38 */
39 static gboolean locale_info_cached = FALSE;
40 static GString *lc_decimal = NULL;
41 static GString *lc_thousand = NULL;
42 static gboolean lc_precedes;
43 static gboolean lc_space_sep;
44 static GString *lc_currency = NULL;
45
46 static gboolean date_format_cached = FALSE;
47 static GString *lc_date_format = NULL;
48 static gboolean time_format_cached = FALSE;
49 static GString *lc_time_format = NULL;
50
51 static gboolean date_order_cached = FALSE;
52
53 static gboolean locale_is_24h_cached = FALSE;
54
55 static gboolean boolean_cached = FALSE;
56 static char const *lc_TRUE = NULL;
57 static char const *lc_FALSE = NULL;
58
59 char const *
go_setlocale(int category,char const * val)60 go_setlocale (int category, char const *val)
61 {
62 locale_info_cached = FALSE;
63 date_format_cached = FALSE;
64 time_format_cached = FALSE;
65 date_order_cached = FALSE;
66 locale_is_24h_cached = FALSE;
67 boolean_cached = FALSE;
68 return setlocale (category, val);
69 }
70
71 /**
72 * _go_locale_shutdown: (skip)
73 */
74 void
_go_locale_shutdown(void)75 _go_locale_shutdown (void)
76 {
77 if (locale_info_cached) {
78 /* Mark everything as uncached. */
79 (void)go_setlocale (LC_ALL, NULL);
80 }
81 #define FREE1(var) if (var) { g_string_free (var, TRUE); var = NULL; }
82 FREE1 (lc_decimal);
83 FREE1 (lc_thousand);
84 FREE1 (lc_currency);
85 FREE1 (lc_date_format);
86 FREE1 (lc_time_format);
87 #undef FREE1
88 }
89
90
91 static void
convert1(GString * res,char const * lstr,char const * name,char const * def)92 convert1 (GString *res, char const *lstr, char const *name, char const *def)
93 {
94 char *tmp;
95
96 if (lstr == NULL || lstr[0] == 0) {
97 g_string_assign (res, def);
98 return;
99 }
100
101 tmp = g_locale_to_utf8 (lstr, -1, NULL, NULL, NULL);
102 if (tmp) {
103 g_string_assign (res, tmp);
104 g_free (tmp);
105 return;
106 }
107
108 g_warning ("Failed to convert locale's %s \"%s\" to UTF-8.", name, lstr);
109 g_string_assign (res, def);
110 }
111
112 static void
update_lc(void)113 update_lc (void)
114 {
115 struct lconv *lc = localeconv ();
116
117 if (!lc_decimal)
118 lc_decimal = g_string_new (NULL);
119
120 if (!lc_thousand)
121 lc_thousand = g_string_new (NULL);
122
123 if (!lc_currency)
124 lc_currency = g_string_new (NULL);
125
126 /*
127 * Extract all information here as lc is not guaranteed to stay
128 * valid after next localeconv call which could be anywhere.
129 */
130
131 convert1 (lc_decimal, lc->decimal_point, "decimal separator", ".");
132 if (g_utf8_strlen (lc_decimal->str, -1) != 1)
133 g_warning ("Decimal separator is not a single character.");
134
135 convert1 (lc_thousand, lc->mon_thousands_sep, "monetary thousands separator",
136 (lc_decimal->str[0] == ',' ? "." : ","));
137 if (g_utf8_strlen (lc_thousand->str, -1) != 1)
138 g_warning ("Monetary thousands separator is not a single character.");
139
140 if (g_string_equal (lc_thousand, lc_decimal)) {
141 g_string_assign (lc_thousand,
142 (lc_decimal->str[0] == ',') ? "." : ",");
143 g_warning ("Monetary thousands separator is the same as the decimal separator; converting '%s' to '%s'",
144 lc_decimal->str, lc_thousand->str);
145 }
146
147 /* Use != 0 rather than == 1 so that CHAR_MAX (undefined) is true */
148 lc_precedes = (lc->p_cs_precedes != 0);
149
150 /* Use == 1 rather than != 0 so that CHAR_MAX (undefined) is false */
151 lc_space_sep = (lc->p_sep_by_space == 1);
152
153 convert1 (lc_currency, lc->currency_symbol, "currency symbol", "$");
154
155 locale_info_cached = TRUE;
156 }
157
158 /**
159 * go_locale_get_decimal:
160 *
161 * Returns: (transfer none): Current locale's decimal separator.
162 */
163 GString const *
go_locale_get_decimal(void)164 go_locale_get_decimal (void)
165 {
166 if (!locale_info_cached)
167 update_lc ();
168
169 return lc_decimal;
170 }
171
172 /**
173 * go_locale_get_thousand:
174 *
175 * Returns: (transfer none): Current locale's thousands separator. This may
176 * be an empty string.
177 */
178 GString const *
go_locale_get_thousand(void)179 go_locale_get_thousand (void)
180 {
181 if (!locale_info_cached)
182 update_lc ();
183
184 return lc_thousand;
185 }
186
187 /**
188 * go_locale_get_currency :
189 * @precedes: a pointer to a boolean which is set to TRUE if the currency
190 * should precede
191 * @space_sep: a pointer to a boolean which is set to TRUE if the currency
192 * should have a space separating it from the value
193 *
194 * Play with the default logic so that things come out nicely for the default
195 * case.
196 *
197 * Returns: (transfer none): A string with the default currency
198 **/
199 GString const *
go_locale_get_currency(gboolean * precedes,gboolean * space_sep)200 go_locale_get_currency (gboolean *precedes, gboolean *space_sep)
201 {
202 if (!locale_info_cached)
203 update_lc ();
204
205 if (precedes)
206 *precedes = lc_precedes;
207
208 if (space_sep)
209 *space_sep = lc_space_sep;
210
211 return lc_currency;
212 }
213
214 #if defined(G_OS_WIN32)
215 static void
go_locale_win32_get_user_default(GString * res,unsigned int id)216 go_locale_win32_get_user_default (GString *res, unsigned int id)
217 {
218 GError *error = NULL;
219 WCHAR fmt_utf16[64];
220 int utf16_len = GetLocaleInfoW (LOCALE_USER_DEFAULT,
221 id, fmt_utf16, sizeof (fmt_utf16));
222 gsize utf8_len;
223 char *fmt_utf8 = g_convert ((gchar *)fmt_utf16, utf16_len*2,
224 "UTF-8", "UTF-16LE", NULL, &utf8_len, &error);
225 if (NULL != fmt_utf8)
226 g_string_append_len (res, fmt_utf8, utf8_len);
227 else if (NULL != error) {
228 g_warning ("error: %s", error->message);
229 g_error_free (error);
230 }
231 }
232 #endif
233
234 /**
235 * go_locale_get_date_format:
236 *
237 * Returns: (transfer none): Current locale's date format as a string.
238 */
239 GString const *
go_locale_get_date_format(void)240 go_locale_get_date_format (void)
241 {
242 if (!date_format_cached) {
243 if (lc_date_format)
244 g_string_truncate (lc_date_format, 0);
245 else
246 lc_date_format = g_string_new (NULL);
247
248 #if defined(G_OS_WIN32)
249 go_locale_win32_get_user_default (lc_date_format, LOCALE_SLONGDATE);
250 #elif defined(HAVE_LANGINFO_H)
251 {
252 char const *fmt = nl_langinfo (D_FMT);
253 /* It appears that sometimes we don't get the %s in the
254 * format as we're supposed to. */
255 const char *first_percent = strchr (fmt, '%');
256 if (first_percent)
257 fmt = first_percent;
258
259 while (*fmt) {
260 if (first_percent) {
261 if (*fmt != '%') {
262 g_string_append_c (lc_date_format, *fmt);
263 fmt++;
264 continue;
265 }
266 fmt++;
267 }
268
269 switch (*fmt) {
270 case 'a': g_string_append (lc_date_format, "ddd"); break;
271 case 'A': g_string_append (lc_date_format, "dddd"); break;
272 case 'b': g_string_append (lc_date_format, "mmm"); break;
273 case 'B': g_string_append (lc_date_format, "mmmm"); break;
274 case 'd': g_string_append (lc_date_format, "dd"); break;
275 case 'D': g_string_append (lc_date_format, "mm/dd/yy"); break;
276 case 'e': g_string_append (lc_date_format, "d"); break; /* Approx */
277 case 'F': g_string_append (lc_date_format, "yyyy-mm-dd"); break;
278 case 'h': g_string_append (lc_date_format, "mmm"); break;
279 case 'm': g_string_append (lc_date_format, "mm"); break;
280 case 't': g_string_append (lc_date_format, "\t"); break;
281 case 'y': g_string_append (lc_date_format, "yy"); break;
282 case 'Y': g_string_append (lc_date_format, "yyyy"); break;
283 case '%':
284 if (!first_percent)
285 break;
286 /* Fall through. */
287 default:
288 if (g_ascii_isalpha (*fmt))
289 g_warning ("Unhandled locale date code '%c'", *fmt);
290 else
291 g_string_append_c (lc_date_format, *fmt);
292 }
293 fmt++;
294 }
295 }
296 #endif
297
298 /* Sanity check */
299 if (!g_utf8_validate (lc_date_format->str, -1, NULL)) {
300 g_warning ("Produced non-UTF-8 date format. Please report.");
301 g_string_truncate (lc_date_format, 0);
302 }
303
304 /* Default */
305 if (lc_date_format->len == 0) {
306 static gboolean warning = TRUE;
307 g_string_append (lc_date_format, "yyyy/mm/dd");
308 if (warning) {
309 g_warning ("Using default system date format: %s",
310 lc_date_format->str);
311 warning = FALSE;
312 }
313 }
314
315 date_format_cached = TRUE;
316 }
317 return lc_date_format;
318 }
319
320 /**
321 * go_locale_get_time_format:
322 *
323 * Returns: (transfer none): Current locale's time format as a string.
324 */
325 GString const *
go_locale_get_time_format(void)326 go_locale_get_time_format (void)
327 {
328 if (!time_format_cached) {
329 if (lc_time_format)
330 g_string_truncate (lc_time_format, 0);
331 else
332 lc_time_format = g_string_new (NULL);
333
334 #if defined(G_OS_WIN32)
335 go_locale_win32_get_user_default (lc_time_format, LOCALE_STIME);
336 #elif defined(HAVE_LANGINFO_H)
337 {
338 char const *fmt = nl_langinfo (T_FMT);
339 /* It appears that sometimes we don't get the %s in the
340 * format as we're supposed to. */
341 const char *first_percent = strchr (fmt, '%');
342 if (first_percent)
343 fmt = first_percent;
344
345 while (*fmt) {
346 if (first_percent) {
347 if (*fmt != '%') {
348 g_string_append_c (lc_time_format, *fmt);
349 fmt++;
350 continue;
351 }
352 fmt++;
353 }
354
355 switch (*fmt) {
356 case 'H': g_string_append (lc_time_format, "hh"); break;
357 case 'I': g_string_append (lc_time_format, "hh"); break;
358 case 'k': g_string_append (lc_time_format, "h"); break; /* Approx */
359 case 'l': g_string_append (lc_time_format, "h"); break; /* Approx */
360 case 'M': g_string_append (lc_time_format, "mm"); break;
361 case 'p': g_string_append (lc_time_format, "AM/PM"); break;
362 case 'P': g_string_append (lc_time_format, "am/pm"); break;
363 case 'r': g_string_append (lc_time_format, "hh:mm:ss AM/PM"); break;
364 case 'S': g_string_append (lc_time_format, "ss"); break;
365 case 'T': g_string_append (lc_time_format, "hh:mm:ss"); break;
366 case 't': g_string_append (lc_time_format, "\t"); break;
367
368 case 'z': case 'Z':
369 /* Ignore these time-zone related items. */
370 break;
371
372 case '%':
373 if (!first_percent)
374 break;
375 /* Fall through. */
376 default:
377 if (g_ascii_isalpha (*fmt))
378 g_warning ("Unhandled locale time code '%c'", *fmt);
379 else
380 g_string_append_c (lc_time_format, *fmt);
381 }
382 fmt++;
383 }
384
385 /* Since we ignore some stuff, sometimes we get trailing
386 whitespace. Kill it. */
387 while (lc_time_format->len > 0) {
388 const char *s = lc_time_format->str + lc_time_format->len;
389 const char *p = g_utf8_prev_char (s);
390 if (!g_unichar_isspace (g_utf8_get_char (p)))
391 break;
392 g_string_truncate (lc_time_format,
393 p - lc_time_format->str);
394 }
395 }
396 #endif
397
398 /* Sanity check */
399 if (!g_utf8_validate (lc_time_format->str, -1, NULL)) {
400 g_warning ("Produced non-UTF-8 time format. Please report.");
401 g_string_truncate (lc_time_format, 0);
402 }
403
404 /* Default */
405 if (lc_time_format->len == 0) {
406 static gboolean warning = TRUE;
407 g_string_append (lc_time_format, "dddd, mmmm dd, yyyy");
408 if (warning) {
409 g_warning ("Using default system time format: %s",
410 lc_time_format->str);
411 warning = FALSE;
412 }
413 }
414
415 time_format_cached = TRUE;
416 }
417 return lc_time_format;
418 }
419
420 /*
421 * go_locale_month_before_day :
422 *
423 * A quick utility routine to guess whether the default date format
424 * uses day/month or month/day. Returns a value of the same meaning
425 * as go_format_month_before_day, i.e.,
426 *
427 * 0, if locale uses day before month
428 * 1, if locale uses month before day, unless the following applies
429 * 2, if locale uses year before month (before day)
430 */
431 int
go_locale_month_before_day(void)432 go_locale_month_before_day (void)
433 {
434 static int month_first = 1;
435 if (!date_order_cached) {
436 date_order_cached = TRUE;
437
438 #if defined(G_OS_WIN32)
439 {
440 TCHAR str[2];
441 GetLocaleInfo (LOCALE_USER_DEFAULT, LOCALE_IDATE, str, 2);
442 if (str[0] == L'1')
443 month_first = 0;
444 else if (str[0] == L'2')
445 month_first = 2;
446 else
447 month_first = 1;
448 }
449
450 #elif defined(HAVE_LANGINFO_H)
451 {
452 char const *ptr = nl_langinfo (D_FMT);
453 while (ptr && *ptr) {
454 char c = *ptr++;
455 switch (c) {
456 case 'd': case 'D': case 'e':
457 month_first = 0;
458 ptr = NULL;
459 break;
460 case 'm': case 'b': case 'B': case 'h':
461 month_first = 1;
462 ptr = NULL;
463 break;
464 case 'C': case 'G': case 'g':
465 case 'y': case 'Y':
466 month_first = 2;
467 ptr = NULL;
468 break;
469 default: ;
470 }
471 }
472 }
473 #else
474 {
475 static gboolean warning = TRUE;
476 if (warning) {
477 g_warning ("Incomplete locale library, dates will be month day year");
478 warning = FALSE;
479 }
480 }
481 #endif
482 }
483
484 return month_first;
485 }
486
487 /**
488 * go_locale_24h :
489 *
490 * Returns: %TRUE if the locale uses a 24h clock, %FALSE otherwise.
491 */
492 gboolean
go_locale_24h(void)493 go_locale_24h (void)
494 {
495 static gboolean locale_is_24h;
496
497 if (!locale_is_24h_cached) {
498 const GString *tf = go_locale_get_time_format ();
499
500 /* Crude. Figure out how to ask properly. */
501 locale_is_24h = !(strstr (tf->str, "AM/PM") ||
502 strstr (tf->str, "am/pm") ||
503 strstr (tf->str, "A/P") ||
504 strstr (tf->str, "a/p"));
505 locale_is_24h_cached = TRUE;
506 }
507
508 return locale_is_24h;
509 }
510
511
512 /* Use comma as the arg separator unless the decimal point is a
513 * comma, in which case use a semi-colon
514 */
515 char
go_locale_get_arg_sep(void)516 go_locale_get_arg_sep (void)
517 {
518 if (go_locale_get_decimal ()->str[0] == ',')
519 return ';';
520 return ',';
521 }
522
523 char
go_locale_get_col_sep(void)524 go_locale_get_col_sep (void)
525 {
526 if (go_locale_get_decimal ()->str[0] == ',')
527 return '\\';
528 return ',';
529 }
530
531 char
go_locale_get_row_sep(void)532 go_locale_get_row_sep (void)
533 {
534 return ';';
535 }
536
537 /**
538 * go_locale_boolean_name:
539 * @b: a boolean value
540 *
541 * Returns: (transfer none): Current locale's rendering of @b.
542 */
543 char const *
go_locale_boolean_name(gboolean b)544 go_locale_boolean_name (gboolean b)
545 {
546 if (!boolean_cached) {
547 lc_TRUE = _("TRUE");
548 lc_FALSE = _("FALSE");
549 boolean_cached = TRUE;
550 }
551 return b ? lc_TRUE : lc_FALSE;
552 }
553
554 /**
555 * go_locale_untranslated_booleans:
556 *
557 * Short circuit the current locale so that we can import files
558 * and still produce error messages in the current LC_MESSAGE
559 **/
560 void
go_locale_untranslated_booleans(void)561 go_locale_untranslated_booleans (void)
562 {
563 lc_TRUE = "TRUE";
564 lc_FALSE = "FALSE";
565 boolean_cached = TRUE;
566 }
567