From b4fedff90e8b35e69620922f27376ef78ade6d0e Mon Sep 17 00:00:00 2001 From: John Ralls Date: Sun, 6 Jan 2019 09:52:43 -0800 Subject: [PATCH] Provide a single static instance of C++ locale. We can't use std::locale::global because all streams imbue it by default and if it's not 'C' (aka std::locale::classic) then we must imbue all the streams that we don't want localized, and that's most of them. Provides error checking for setting the C++ locale from the environment. This is necessary both because the environment might have an invalid locale, which would cause an unhandled exception crash. On windows std::locale("") can't handle some Microsoft-style locale strings (e.g. Spanish_Spain) so we use boost::locale's gen("") function to set the locale--though even that can't handle a Microsoft-style locale string with an appended charset (e.g. Spanish_Spain.1252) and that's what glibc's setlocale(LC_ALL, NULL) emits. --- gnucash/gnome/assistant-loan.cpp | 3 +- .../csv-imp/assistant-csv-trans-import.cpp | 3 +- libgnucash/core-utils/CMakeLists.txt | 2 + libgnucash/core-utils/gnc-filepath-utils.cpp | 4 +- libgnucash/core-utils/gnc-locale-utils.cpp | 78 +++++++++++++++++++ libgnucash/core-utils/gnc-locale-utils.hpp | 39 ++++++++++ libgnucash/core-utils/test/CMakeLists.txt | 1 + libgnucash/engine/gnc-datetime.cpp | 12 ++- libgnucash/engine/test/CMakeLists.txt | 4 + po/POTFILES.in | 1 + 10 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 libgnucash/core-utils/gnc-locale-utils.cpp create mode 100644 libgnucash/core-utils/gnc-locale-utils.hpp diff --git a/gnucash/gnome/assistant-loan.cpp b/gnucash/gnome/assistant-loan.cpp index 3509b18cc0..5505623c19 100644 --- a/gnucash/gnome/assistant-loan.cpp +++ b/gnucash/gnome/assistant-loan.cpp @@ -49,6 +49,7 @@ extern "C" #include "gnc-engine.h" } +#include #include #include #include @@ -2320,7 +2321,7 @@ struct cust_prec_punct : std::moneypunct_byname { template std::string to_str_with_prec (const gdouble val) { - auto loc = std::locale(std::locale(""), new cust_prec_punct("")); + auto loc = std::locale(gnc_get_locale(), new cust_prec_punct("")); std::wstringstream valstr; valstr.imbue(loc); valstr << std::put_money(val * pow(10, prec)); diff --git a/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp b/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp index 777caf4f01..00c1a126c2 100644 --- a/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp +++ b/gnucash/import-export/csv-imp/assistant-csv-trans-import.cpp @@ -64,6 +64,7 @@ extern "C" #include "gnc-tokenizer-fw.hpp" #include "gnc-tokenizer-csv.hpp" +#include #include namespace bl = boost::locale; @@ -2068,7 +2069,7 @@ CsvImpTransAssist::assist_summary_page_prepare () try { /* Translators: {1} will be replaced with a filename */ - text += (bl::format (bl::translate ("The transactions were imported from file '{1}'.")) % m_file_name).str(gen("")); + text += (bl::format (bl::translate ("The transactions were imported from file '{1}'.")) % m_file_name).str(gnc_get_locale()); text += ""; } catch (const bl::conv::conversion_error& err) diff --git a/libgnucash/core-utils/CMakeLists.txt b/libgnucash/core-utils/CMakeLists.txt index b08e38b833..6d02f48691 100644 --- a/libgnucash/core-utils/CMakeLists.txt +++ b/libgnucash/core-utils/CMakeLists.txt @@ -25,6 +25,7 @@ set (core_utils_SOURCES gnc-guile-utils.c gnc-jalali.c gnc-locale-utils.c + gnc-locale-utils.cpp gnc-path.c ) @@ -116,6 +117,7 @@ set(core_utils_noinst_HEADERS gnc-guile-utils.h gnc-jalali.h gnc-locale-utils.h + gnc-locale-utils.hpp gnc-path.h ) diff --git a/libgnucash/core-utils/gnc-filepath-utils.cpp b/libgnucash/core-utils/gnc-filepath-utils.cpp index d421bbcab9..7371534b35 100644 --- a/libgnucash/core-utils/gnc-filepath-utils.cpp +++ b/libgnucash/core-utils/gnc-filepath-utils.cpp @@ -64,6 +64,7 @@ extern "C" { #endif } +#include "gnc-locale-utils.hpp" #include #include #include @@ -587,9 +588,8 @@ static std::string migrate_gnc_datahome() gen.add_messages_path(gnc_path_get_datadir()); gen.add_messages_domain(PACKAGE); -// std::locale::global(gen("")); std::stringstream migration_msg; - migration_msg.imbue(gen("")); + migration_msg.imbue(gnc_get_locale()); /* Step 1: copy directory $HOME/.gnucash to $GNC_DATA_HOME */ auto full_copy = copy_recursive (old_dir, gnc_userdata_home); diff --git a/libgnucash/core-utils/gnc-locale-utils.cpp b/libgnucash/core-utils/gnc-locale-utils.cpp new file mode 100644 index 0000000000..e3de961903 --- /dev/null +++ b/libgnucash/core-utils/gnc-locale-utils.cpp @@ -0,0 +1,78 @@ +/********************************************************************\ + * gnc-locale-utils.cpp -- provide a default locale for C++ * + * Copyright (C) 2019 John Ralls +#include +#include "gnc-locale-utils.hpp" + +/* This function addresses two separate problems: First, if we set + * std::locale::global then all streams automagically imbue + * themselves with it and we have to re-imbue all of the backends and + * logging streams with std::locale::classic() so that data and log + * files aren't localized. Second, calling std::locale("") is slow, + * so we want to do it only once. Worse, the standard C++ library in + * Mingw64 chokes on at least some Microsoft-style locale strings + * (e.g. "Spanish_Spain") but libc's setlocale(LC_ALL, NULL) emits + * them even if originally fed a Unix-style locale ("es_ES"). + * + * The solution is this function which caches the setlocale() locale + * the first time it's called and which uses a boost::locale + * generator, which does know what to do with (sometimes adjusted) + * Microsoft locale strings. + */ +const std::locale& +gnc_get_locale() +{ + static std::locale cached; + static bool tried_already = false; + if (!tried_already) + { + boost::locale::generator gen; + tried_already = true; + try + { + cached = gen(""); + } + catch (const std::runtime_error& err) + { + std::string c_locale(setlocale(LC_ALL, nullptr)); + std::cerr << "[gnc_get_locale] Failed to create app-default locale from " << c_locale << " because " << err.what() << "\n"; + auto dot = c_locale.find("."); + if (dot != std::string::npos) + { + try + { + cached = gen(c_locale.substr(0, dot)); + } + catch (std::runtime_error& err2) + { + std::cerr << "[gnc_get_locale] Failed to create app-default locale from " << c_locale << " because " << err.what() << " so using the 'C' locale for C++.\n"; + } + } + else + { + std::cerr << "[gnc_get_locale] Using the 'C' locale for C++\n"; + } + } + } + return cached; +} diff --git a/libgnucash/core-utils/gnc-locale-utils.hpp b/libgnucash/core-utils/gnc-locale-utils.hpp new file mode 100644 index 0000000000..c16467eb46 --- /dev/null +++ b/libgnucash/core-utils/gnc-locale-utils.hpp @@ -0,0 +1,39 @@ +/********************************************************************\ + * gnc-locale-utils.hpp -- provide a default locale for C++ * + * Copyright (C) 2019 John Ralls + +/** Get the default application locale. + * + * If we set std::locale::global we have to imbue every stream that + * we want in the C locale, and that's a lot more than we want imbued + * with the application locale. Calling std::locale("") is expensive, + * so call this instead. + * + * @returns A static std::locale representing the one set with + * setlocale() in main(). + */ +const std::locale& gnc_get_locale(); + +#endif /* GNC_LOCALE_UTILS_HPP */ diff --git a/libgnucash/core-utils/test/CMakeLists.txt b/libgnucash/core-utils/test/CMakeLists.txt index fe92a2c8e6..342078797b 100644 --- a/libgnucash/core-utils/test/CMakeLists.txt +++ b/libgnucash/core-utils/test/CMakeLists.txt @@ -46,6 +46,7 @@ set(test_gnc_path_util_SOURCES ${MODULEPATH}/gnc-path.c ${MODULEPATH}/binreloc.c ${MODULEPATH}/gnc-filepath-utils.cpp + ${MODULEPATH}/gnc-locale-utils.cpp gtest-path-utilities.cpp ${GTEST_SRC}) diff --git a/libgnucash/engine/gnc-datetime.cpp b/libgnucash/engine/gnc-datetime.cpp index a9c4f6be80..c73fc0aed0 100644 --- a/libgnucash/engine/gnc-datetime.cpp +++ b/libgnucash/engine/gnc-datetime.cpp @@ -32,14 +32,19 @@ extern "C" #include #include #include +#include #include #include #include #include #include #include +#include #include "gnc-timezone.hpp" #include "gnc-datetime.hpp" +#include "qoflog.h" + +static const char* log_module = "gnc.engine"; #define N_(string) string //So that xgettext will find it @@ -440,8 +445,7 @@ GncDateTimeImpl::format(const char* format) const //The stream destructor frees the facet, so it must be heap-allocated. auto output_facet(new Facet(normalize_format(format).c_str())); // FIXME Rather than imbueing a locale below we probably should set std::locale::global appropriately somewhere. - // At that point the use of cachedLocale mechanism should be removed. - ss.imbue(std::locale(cachedLocale, output_facet)); + ss.imbue(std::locale(gnc_get_locale(), output_facet)); ss << m_time; return ss.str(); } @@ -454,7 +458,7 @@ GncDateTimeImpl::format_zulu(const char* format) const //The stream destructor frees the facet, so it must be heap-allocated. auto output_facet(new Facet(normalize_format(format).c_str())); // FIXME Rather than imbueing a locale below we probably should set std::locale::global appropriately somewhere. - ss.imbue(std::locale(std::locale(""), output_facet)); + ss.imbue(std::locale(gnc_get_locale(), output_facet)); ss << m_time.utc_time(); return ss.str(); } @@ -530,7 +534,7 @@ GncDateImpl::format(const char* format) const //The stream destructor frees the facet, so it must be heap-allocated. auto output_facet(new Facet(normalize_format(format).c_str())); // FIXME Rather than imbueing a locale below we probably should set std::locale::global appropriately somewhere. - ss.imbue(std::locale(std::locale(""), output_facet)); + ss.imbue(std::locale(gnc_get_locale(), output_facet)); ss << m_greg; return ss.str(); } diff --git a/libgnucash/engine/test/CMakeLists.txt b/libgnucash/engine/test/CMakeLists.txt index 5acda904b5..1a27ea7f48 100644 --- a/libgnucash/engine/test/CMakeLists.txt +++ b/libgnucash/engine/test/CMakeLists.txt @@ -104,6 +104,7 @@ set(gtest_qof_LIBS set(gtest_engine_INCLUDES ${MODULEPATH} + ${CMAKE_SOURCE_DIR}/libgnucash/core-utils ${CMAKE_BINARY_DIR}/common # for config.h ${CMAKE_SOURCE_DIR}/common # for platform.h ${GLIB2_INCLUDE_DIRS} @@ -156,6 +157,7 @@ set(test_gnc_rational_SOURCES ${MODULEPATH}/gnc-timezone.cpp ${MODULEPATH}/gnc-date.cpp ${MODULEPATH}/qoflog.cpp + ${CMAKE_SOURCE_DIR}/libgnucash/core-utils/gnc-locale-utils.cpp ${gtest_engine_win32_SOURCES} gtest-gnc-rational.cpp ${GTEST_SRC}) @@ -171,6 +173,7 @@ set(test_gnc_numeric_SOURCES ${MODULEPATH}/gnc-timezone.cpp ${MODULEPATH}/gnc-date.cpp ${MODULEPATH}/qoflog.cpp + ${CMAKE_SOURCE_DIR}/libgnucash/core-utils/gnc-locale-utils.cpp ${gtest_engine_win32_SOURCES} gtest-gnc-numeric.cpp ${GTEST_SRC}) @@ -189,6 +192,7 @@ set(test_gnc_datetime_SOURCES ${MODULEPATH}/gnc-timezone.cpp ${MODULEPATH}/gnc-date.cpp ${MODULEPATH}/qoflog.cpp + ${CMAKE_SOURCE_DIR}/libgnucash/core-utils/gnc-locale-utils.cpp ${gtest_engine_win32_SOURCES} gtest-gnc-datetime.cpp ${GTEST_SRC}) diff --git a/po/POTFILES.in b/po/POTFILES.in index 8e327ea055..e8c56acf38 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -613,6 +613,7 @@ libgnucash/core-utils/gnc-glib-utils.c libgnucash/core-utils/gnc-guile-utils.c libgnucash/core-utils/gnc-jalali.c libgnucash/core-utils/gnc-locale-utils.c +libgnucash/core-utils/gnc-locale-utils.cpp libgnucash/core-utils/gnc-path.c libgnucash/core-utils/gnc-prefs.c libgnucash/doc/doxygen_main_page.c