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