diff --git a/CMakeLists.txt b/CMakeLists.txt index fe942083e76..5a9061d06ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1277,7 +1277,6 @@ set_and_warn_dependency(WITH_PYTHON WITH_MOD_FLUID OFF) # enable boost for cycles, audaspace or i18n # otherwise if the user disabled -set_and_warn_dependency(WITH_BOOST WITH_INTERNATIONAL OFF) set_and_warn_dependency(WITH_BOOST WITH_OPENVDB OFF) set_and_warn_dependency(WITH_BOOST WITH_QUADRIFLOW OFF) set_and_warn_dependency(WITH_BOOST WITH_USD OFF) diff --git a/build_files/cmake/platform/platform_apple.cmake b/build_files/cmake/platform/platform_apple.cmake index 814f0abb705..7d6f0cdde7d 100644 --- a/build_files/cmake/platform/platform_apple.cmake +++ b/build_files/cmake/platform/platform_apple.cmake @@ -256,9 +256,6 @@ if(WITH_BOOST) set(Boost_ROOT ${LIBDIR}/boost) set(Boost_NO_SYSTEM_PATHS ON) set(_boost_FIND_COMPONENTS) - if(WITH_INTERNATIONAL) - list(APPEND _boost_FIND_COMPONENTS locale) - endif() if(WITH_USD AND USD_PYTHON_SUPPORT) list(APPEND _boost_FIND_COMPONENTS python${PYTHON_VERSION_NO_DOTS}) endif() @@ -280,8 +277,8 @@ if(WITH_BOOST) endif() add_bundled_libraries(boost/lib) -if(WITH_INTERNATIONAL OR WITH_CODEC_FFMPEG) - string(APPEND PLATFORM_LINKFLAGS " -liconv") # boost_locale and ffmpeg needs it ! +if(WITH_CODEC_FFMPEG) + string(APPEND PLATFORM_LINKFLAGS " -liconv") # ffmpeg needs it ! endif() if(WITH_PUGIXML) diff --git a/build_files/cmake/platform/platform_unix.cmake b/build_files/cmake/platform/platform_unix.cmake index 25bd6c3a631..66dee945388 100644 --- a/build_files/cmake/platform/platform_unix.cmake +++ b/build_files/cmake/platform/platform_unix.cmake @@ -472,9 +472,6 @@ if(WITH_BOOST) endif() set(Boost_USE_MULTITHREADED ON) set(__boost_packages) - if(WITH_INTERNATIONAL) - list(APPEND __boost_packages locale) - endif() if(WITH_USD AND USD_PYTHON_SUPPORT) list(APPEND __boost_packages python${PYTHON_VERSION_NO_DOTS}) endif() diff --git a/build_files/cmake/platform/platform_win32.cmake b/build_files/cmake/platform/platform_win32.cmake index 4564ac2b075..1f1d8c926a5 100644 --- a/build_files/cmake/platform/platform_win32.cmake +++ b/build_files/cmake/platform/platform_win32.cmake @@ -680,9 +680,7 @@ if(NOT WITH_WINDOWS_FIND_MODULES) endif() if(WITH_BOOST) - if(WITH_INTERNATIONAL) - list(APPEND boost_extra_libs locale) - endif() + set(boost_extra_libs) set(Boost_USE_STATIC_RUNTIME ON) # prefix lib set(Boost_USE_MULTITHREADED ON) # suffix -mt set(Boost_USE_STATIC_LIBS ON) # suffix -s @@ -707,12 +705,6 @@ if(WITH_BOOST) ) endif() endif() - if(WITH_INTERNATIONAL) - set(BOOST_LIBRARIES ${BOOST_LIBRARIES} - optimized ${BOOST_LIBPATH}/${BOOST_PREFIX}boost_locale-${BOOST_POSTFIX}.lib - debug ${BOOST_LIBPATH}/${BOOST_PREFIX}boost_locale-${BOOST_DEBUG_POSTFIX}.lib - ) - endif() else() # we found boost using find_package set(BOOST_INCLUDE_DIR ${Boost_INCLUDE_DIRS}) set(BOOST_LIBRARIES ${Boost_LIBRARIES}) diff --git a/doc/license/Boost-license.txt b/doc/license/Boost-license.txt new file mode 100644 index 00000000000..36b7cd93cdf --- /dev/null +++ b/doc/license/Boost-license.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/doc/license/SPDX-license-identifiers.txt b/doc/license/SPDX-license-identifiers.txt index 62aa0bbb4cd..c63ed010b44 100644 --- a/doc/license/SPDX-license-identifiers.txt +++ b/doc/license/SPDX-license-identifiers.txt @@ -3,6 +3,7 @@ Corresponding SPDX license identifiers in the source code: Apache-2.0 Apache-2-license.txt https://spdx.org/licenses/Apache-2.0.html BSD-2-Clause BSD-2-Clause-license.txt https://spdx.org/licenses/BSD-2-Clause.html BSD-3-Clause BSD-3-Clause-license.txt https://spdx.org/licenses/BSD-3-Clause.html +BSL-1.0 Boost-license.txt https://spdx.org/licenses/BSL-1.0.html GPL-2.0-or-later GPL-license.txt https://spdx.org/licenses/GPL-2.0-or-later.html GPL-3.0-or-later GPL3-license.txt https://spdx.org/licenses/GPL-3.0-or-later.html LGPL-2.1-or-later LGPL2.1-license.txt https://spdx.org/licenses/LGPL-2.1-or-later.html diff --git a/intern/locale/CMakeLists.txt b/intern/locale/CMakeLists.txt index 06587b36218..5c69a3242d5 100644 --- a/intern/locale/CMakeLists.txt +++ b/intern/locale/CMakeLists.txt @@ -10,26 +10,22 @@ set(INC_SYS ) set(SRC - boost_locale_wrapper.cpp + blender_locale.cpp + messages.cpp - boost_locale_wrapper.h + blender_locale.h + messages.h ) set(LIB + PRIVATE bf::blenlib + PRIVATE bf::intern::guardedalloc ) -if(WIN32) - # This is set in platform_win32.cmake, will exist for 3.4+ library - # folders which are dynamic, but not for 3.3 which will be static. - if(EXISTS ${BOOST_34_TRIGGER_FILE}) - add_definitions(-DBOOST_ALL_DYN_LINK=1) - endif() -endif() - if(APPLE) # Cocoa code to read the locale on OSX list(APPEND SRC - osx_user_locale.mm + messages_apple.mm ) endif() @@ -41,14 +37,4 @@ if(WITH_GHOST_SDL) add_definitions(-DWITH_GHOST_SDL) endif() -if(WITH_INTERNATIONAL) - list(APPEND INC_SYS - ${BOOST_INCLUDE_DIR} - ) - list(APPEND LIB - ${BOOST_LIBRARIES} - ) - add_definitions(${BOOST_DEFINITIONS}) -endif() - blender_add_lib(bf_intern_locale "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") diff --git a/intern/locale/blender_locale.cpp b/intern/locale/blender_locale.cpp new file mode 100644 index 00000000000..b67bb6f7ad5 --- /dev/null +++ b/intern/locale/blender_locale.cpp @@ -0,0 +1,100 @@ +/* SPDX-FileCopyrightText: 2012 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup intern_locale + */ + +#include + +#include "blender_locale.h" +#include "messages.h" + +static std::string messages_path; +static std::string default_domain; +static std::string locale_str; + +/* NOTE: We cannot use short stuff like `boost::locale::gettext`, because those return + * `std::basic_string` objects, which c_ptr()-returned char* is no more valid + * once deleted (which happens as soons they are out of scope of this func). */ +static std::locale locale_global; +static blender::locale::MessageFacet const *facet_global = nullptr; + +static void bl_locale_global_cache() +{ + /* Cache facet in global variable. Not only is it better for performance, + * it also fixes crashes on macOS when doing translation from threads other + * than main. Likely because of some internal thread local variables. */ + try { + /* facet_global reference is valid as long as local_global exists, + * so we store both. */ + locale_global = std::locale(); + facet_global = &std::use_facet(locale_global); + } + // TODO: verify it's not installed for C case + /* `if std::has_facet(l) == false`, LC_ALL = "C" case. */ + catch (const std::bad_cast &e) { +#ifndef NDEBUG + std::cout << "bl_locale_global_cache:" << e.what() << " \n"; +#endif + (void)e; + facet_global = nullptr; + } + catch (const std::exception &e) { +#ifndef NDEBUG + std::cout << "bl_locale_global_cache:" << e.what() << " \n"; +#endif + (void)e; + facet_global = nullptr; + } +} + +void bl_locale_init(const char *_messages_path, const char *_default_domain) +{ + /* TODO: Do we need to modify locale for other things like numeric or time? + * And if so, do we need to set it to "C", or to the chosen language? */ + messages_path = _messages_path; + default_domain = _default_domain; +} + +void bl_locale_set(const char *locale_name) +{ + /* Get locale name from system if not specified. */ + std::string locale_full_name = locale_name ? locale_name : ""; + + try { + /* Retrieve and parse full locale name. */ + blender::locale::Info info(locale_full_name); + + /* Load .mo file for locale. */ + std::locale _locale = blender::locale::MessageFacet::install( + std::locale(), info, {default_domain}, {messages_path}); + std::locale::global(_locale); + + bl_locale_global_cache(); + + /* Generate the locale string, to known which one is used in case of default locale. */ + locale_str = info.to_full_name(); + } + catch (std::exception const &e) { + std::cout << "bl_locale_set(" << locale_full_name << "): " << e.what() << " \n"; + } +} + +const char *bl_locale_get(void) +{ + return locale_str.c_str(); +} + +const char *bl_locale_pgettext(const char *msgctxt, const char *msgid) +{ + if (facet_global) { + char const *r = facet_global->translate(0, msgctxt, msgid); + if (r) { + return r; + } + } + + return msgid; +} diff --git a/intern/locale/boost_locale_wrapper.h b/intern/locale/blender_locale.h similarity index 100% rename from intern/locale/boost_locale_wrapper.h rename to intern/locale/blender_locale.h diff --git a/intern/locale/boost_locale_wrapper.cpp b/intern/locale/boost_locale_wrapper.cpp deleted file mode 100644 index 97bef248aac..00000000000 --- a/intern/locale/boost_locale_wrapper.cpp +++ /dev/null @@ -1,136 +0,0 @@ -/* SPDX-FileCopyrightText: 2012 Blender Authors - * - * SPDX-License-Identifier: GPL-2.0-or-later */ - -/** \file - * \ingroup intern_locale - */ - -#include -#include -#include - -#include "boost_locale_wrapper.h" - -static std::string messages_path; -static std::string default_domain; -static std::string locale_str; - -/* NOTE: We cannot use short stuff like `boost::locale::gettext`, because those return - * `std::basic_string` objects, which c_ptr()-returned char* is no more valid - * once deleted (which happens as soons they are out of scope of this func). */ -typedef boost::locale::message_format char_message_facet; -static std::locale locale_global; -static char_message_facet const *facet_global = NULL; - -static void bl_locale_global_cache() -{ - /* Cache facet in global variable. Not only is it better for performance, - * it also fixes crashes on macOS when doing translation from threads other - * than main. Likely because of some internal thread local variables. */ - try { - /* facet_global reference is valid as long as local_global exists, - * so we store both. */ - locale_global = std::locale(); - facet_global = &std::use_facet(locale_global); - } - /* `if std::has_facet(l) == false`, LC_ALL = "C" case. */ - catch (const std::bad_cast &e) { -#ifndef NDEBUG - std::cout << "bl_locale_global_cache:" << e.what() << " \n"; -#endif - (void)e; - facet_global = NULL; - } - catch (const std::exception &e) { -#ifndef NDEBUG - std::cout << "bl_locale_global_cache:" << e.what() << " \n"; -#endif - (void)e; - facet_global = NULL; - } -} - -void bl_locale_init(const char *_messages_path, const char *_default_domain) -{ - /* Avoid using ICU backend, we do not need its power and it's rather heavy! */ - boost::locale::localization_backend_manager lman = - boost::locale::localization_backend_manager::global(); -#if defined(_WIN32) - lman.select("winapi"); -#else - lman.select("posix"); -#endif - boost::locale::localization_backend_manager::global(lman); - - messages_path = _messages_path; - default_domain = _default_domain; -} - -void bl_locale_set(const char *locale) -{ - boost::locale::generator gen; - std::locale _locale; - /* Specify location of dictionaries. */ - gen.add_messages_path(messages_path); - gen.add_messages_domain(default_domain); - // gen.set_default_messages_domain(default_domain); - - try { - if (locale && locale[0]) { - _locale = gen(locale); - } - else { -#if defined(__APPLE__) && !defined(WITH_HEADLESS) && !defined(WITH_GHOST_SDL) - std::string locale_osx = osx_user_locale() + std::string(".UTF-8"); - _locale = gen(locale_osx.c_str()); -#else - _locale = gen(""); -#endif - } - std::locale::global(_locale); - /* NOTE: boost always uses "C" LC_NUMERIC by default! */ - - bl_locale_global_cache(); - - /* Generate the locale string - * (useful to know which locale we are actually using in case of "default" one). */ -#define LOCALE_INFO std::use_facet(_locale) - - locale_str = LOCALE_INFO.language(); - if (LOCALE_INFO.country() != "") { - locale_str += "_" + LOCALE_INFO.country(); - } - if (LOCALE_INFO.variant() != "") { - locale_str += "@" + LOCALE_INFO.variant(); - } - -#undef LOCALE_INFO - } - /* Extra catch on `std::runtime_error` is needed for macOS/Clang as it seems that exceptions - * like `boost::locale::conv::conversion_error` (which inherit from `std::runtime_error`) are - * not caught by their ancestor `std::exception`. See #88877#1177108 */ - catch (std::runtime_error const &e) { - std::cout << "bl_locale_set(" << locale << "): " << e.what() << " \n"; - } - catch (std::exception const &e) { - std::cout << "bl_locale_set(" << locale << "): " << e.what() << " \n"; - } -} - -const char *bl_locale_get(void) -{ - return locale_str.c_str(); -} - -const char *bl_locale_pgettext(const char *msgctxt, const char *msgid) -{ - if (facet_global) { - char const *r = facet_global->get(0, msgctxt, msgid); - if (r) { - return r; - } - } - - return msgid; -} diff --git a/intern/locale/messages.cpp b/intern/locale/messages.cpp new file mode 100644 index 00000000000..202b55a3445 --- /dev/null +++ b/intern/locale/messages.cpp @@ -0,0 +1,554 @@ +/* SPDX-FileCopyrightText: 2009-2015 Artyom Beilis (Tonkikh) + * SPDX-FileCopyrightText: 2021-2023 Alexander Grund + * SPDX-FileCopyrightText: 2025 Blender Authors + * SPDX-License-Identifier: BSL-1.0 + * + * Adapted from boost::locale */ + +#include "messages.h" + +#include +#include +#include +#include +#include +#include + +#include "BLI_assert.h" +#include "BLI_fileops.h" +#include "BLI_hash.hh" +#include "BLI_map.hh" +#include "BLI_path_utils.hh" +#include "BLI_string_ref.hh" +#include "BLI_vector.hh" + +#ifdef _WIN32 +# include "BLI_winstuff.h" +#endif + +namespace blender::locale { + +/* Upper/lower case, intentionally restricted to ASCII. */ + +static constexpr bool is_upper_ascii(const char c) +{ + return 'A' <= c && c <= 'Z'; +} + +static constexpr bool is_lower_ascii(const char c) +{ + return 'a' <= c && c <= 'z'; +} + +static bool make_lower_ascii(char &c) +{ + if (is_upper_ascii(c)) { + c += 'a' - 'A'; + return true; + } + return false; +} + +static bool make_upper_ascii(char &c) +{ + if (is_lower_ascii(c)) { + c += 'A' - 'a'; + return true; + } + return false; +} + +static constexpr bool is_numeric_ascii(const char c) +{ + return '0' <= c && c <= '9'; +} + +/* Locale parsing. */ + +static bool parse_from_variant(Info &info, const std::string_view input) +{ + if (info.language == "C" || input.empty()) { + return false; + } + info.variant = input; + /* No assumptions, just make it lowercase. */ + for (char &c : info.variant) { + make_lower_ascii(c); + } + return true; +} + +static bool parse_from_encoding(Info &info, const std::string_view input) +{ + const auto end = input.find_first_of('@'); + std::string tmp(input.substr(0, end)); + if (tmp.empty()) { + return false; + } + /* tmp contains encoding, we ignore it. */ + if (end >= input.size()) { + return true; + } + BLI_assert(input[end] == '@'); + return parse_from_variant(info, input.substr(end + 1)); +} + +static bool parse_from_country(Info &info, const std::string_view input) +{ + if (info.language == "C") { + return false; + } + + const auto end = input.find_first_of("@."); + std::string tmp(input.substr(0, end)); + if (tmp.empty()) { + return false; + } + + for (char &c : tmp) { + make_upper_ascii(c); + } + + /* If it's ALL uppercase ASCII, assume ISO 3166 country id. */ + if (std::find_if_not(tmp.begin(), tmp.end(), is_upper_ascii) != tmp.end()) { + /* else handle special cases: + * - en_US_POSIX is an alias for C + * - M49 country code: 3 digits */ + if (info.language == "en" && tmp == "US_POSIX") { + info.language = "C"; + tmp.clear(); + } + else if (tmp.size() != 3u || + std::find_if_not(tmp.begin(), tmp.end(), is_numeric_ascii) != tmp.end()) + { + return false; + } + } + + info.country = tmp; + if (end >= input.size()) { + return true; + } + if (input[end] == '.') { + return parse_from_encoding(info, input.substr(end + 1)); + } + BLI_assert(input[end] == '@'); + return parse_from_variant(info, input.substr(end + 1)); +} + +static bool parse_from_script(Info &info, const std::string_view input) +{ + const auto end = input.find_first_of("-_@."); + std::string tmp(input.substr(0, end)); + /* Script is exactly 4 ASCII characters, otherwise it is not present. */ + if (tmp.length() != 4) { + return parse_from_country(info, input); + } + + for (char &c : tmp) { + if (!is_lower_ascii(c) && !make_lower_ascii(c)) { + return parse_from_country(info, input); + } + } + make_upper_ascii(tmp[0]); /* Capitalize first letter only. */ + info.script = tmp; + + if (end >= input.size()) { + return true; + } + if (input[end] == '-' || input[end] == '_') { + return parse_from_country(info, input.substr(end + 1)); + } + if (input[end] == '.') { + return parse_from_encoding(info, input.substr(end + 1)); + } + BLI_assert(input[end] == '@'); + return parse_from_variant(info, input.substr(end + 1)); +} + +static bool parse_from_lang(Info &info, const std::string_view input) +{ + const auto end = input.find_first_of("-_@."); + std::string tmp(input.substr(0, end)); + if (tmp.empty()) { + return false; + } + for (char &c : tmp) { + if (!is_lower_ascii(c) && !make_lower_ascii(c)) { + return false; + } + } + if (tmp != "c" && tmp != "posix") { /* Keep default if C or POSIX. */ + info.language = tmp; + } + + if (end >= input.size()) { + return true; + } + if (input[end] == '-' || input[end] == '_') { + return parse_from_script(info, input.substr(end + 1)); + } + if (input[end] == '.') { + return parse_from_encoding(info, input.substr(end + 1)); + } + BLI_assert(input[end] == '@'); + return parse_from_variant(info, input.substr(end + 1)); +} + +/* Info about a locale. */ + +Info::Info(const StringRef locale_full_name) +{ + std::string locale_name(locale_full_name); + + /* If locale name not specified, try to get the appropriate one from the system. */ +#if defined(__APPLE__) && !defined(WITH_HEADLESS) && !defined(WITH_GHOST_SDL) + if (locale_name.empty()) { + locale_name = macos_user_locale(); + } +#endif + + if (locale_name.empty()) { + const char *lc_all = BLI_getenv("LC_ALL"); + if (lc_all) { + locale_name = lc_all; + } + } + if (locale_name.empty()) { + const char *lang = BLI_getenv("LANG"); + if (lang) { + locale_name = lang; + } + } + +#ifdef _WIN32 + if (locale_name.empty()) { + char buf[128] = {}; + if (GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_SISO639LANGNAME, buf, sizeof(buf)) != 0) { + locale_name = buf; + if (GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_SISO3166CTRYNAME, buf, sizeof(buf)) != 0) { + locale_name += "_"; + locale_name += buf; + } + } + } +#endif + + parse_from_lang(*this, locale_name); +} + +std::string Info::to_full_name() const +{ + std::string result = language; + if (!script.empty()) { + result += '_' + script; + } + if (!country.empty()) { + result += '_' + country; + } + if (!variant.empty()) { + result += '@' + variant; + } + return result; +} + +/* .mo file reader. */ + +class MOFile { + uint32_t keys_offset_ = 0; + uint32_t translations_offset_ = 0; + + Vector data_; + bool native_byteorder_ = false; + size_t size_ = false; + + std::string error_; + + public: + MOFile(const std::string &filepath) + { + FILE *file = BLI_fopen(filepath.c_str(), "rb"); + if (!file) { + return; + } + + fseek(file, 0, SEEK_END); + const int64_t len = BLI_ftell(file); + if (len >= 0) { + fseek(file, 0, SEEK_SET); + data_.resize(len); + if (fread(data_.data(), 1, len, file) != len) { + data_.clear(); + error_ = "Failed to read file"; + } + } + else { + error_ = "Wrong file object"; + } + + fclose(file); + + if (error_.empty()) { + read_data(); + } + } + + const char *key(int id) + { + const uint32_t off = get(keys_offset_ + id * 8 + 4); + return data_.data() + off; + } + + StringRef value(int id) + { + const uint32_t len = get(translations_offset_ + id * 8); + const uint32_t off = get(translations_offset_ + id * 8 + 4); + if (len > data_.size() || off > data_.size() - len) { + error_ = "Bad mo-file format"; + return ""; + } + return StringRef(&data_[off], len); + } + + size_t size() const + { + return size_; + } + + bool empty() const + { + return size_ == 0; + } + + const std::string &error() const + { + return error_; + } + + private: + void read_data() + { + if (data_.size() < 4) { + error_ = "Invalid 'mo' file format - the file is too short"; + return; + } + + uint32_t magic; + memcpy(&magic, data_.data(), sizeof(magic)); + if (magic == 0x950412de) { + native_byteorder_ = true; + } + else if (magic == 0xde120495) { + native_byteorder_ = false; + } + else { + error_ = "Invalid file format - invalid magic number"; + return; + } + + // Read all format sizes + size_ = get(8); + keys_offset_ = get(12); + translations_offset_ = get(16); + } + + uint32_t get(int offset) + { + if (offset > data_.size() - 4) { + error_ = "Bad mo-file format"; + return 0; + } + uint32_t v; + memcpy(&v, &data_[offset], 4); + if (!native_byteorder_) { + v = ((v & 0xFF) << 24) | ((v & 0xFF00) << 8) | ((v & 0xFF0000) >> 8) | + ((v & 0xFF000000) >> 24); + } + + return v; + } +}; + +/* Message lookup key. */ + +struct MessageKeyRef { + StringRef context_; + StringRef str_; + + uint64_t hash() const + { + return get_default_hash(context_, str_); + } +}; + +struct MessageKey { + std::string context_; + std::string str_; + + MessageKey(const StringRef c) + { + const size_t pos = c.find(char(4)); + if (pos == StringRef::not_found) { + str_ = c; + } + else { + context_ = c.substr(0, pos); + str_ = c.substr(pos + 1); + } + } + + uint64_t hash() const + { + return get_default_hash(context_, str_); + } + + static uint64_t hash_as(const MessageKeyRef &key) + { + return key.hash(); + } +}; + +inline bool operator==(const MessageKey &a, const MessageKey &b) +{ + return a.context_ == b.context_ && a.str_ == b.str_; +} + +inline bool operator==(const MessageKeyRef &a, const MessageKey &b) +{ + return a.context_ == b.context_ && a.str_ == b.str_; +} + +/* std::locale facet for translation based on .mo files. */ + +class MOMessageFacet : public MessageFacet { + using Catalog = Map; + Vector catalogs_; + std::string error_; + + public: + MOMessageFacet(const Info &info, + const Vector &domains, + const Vector &paths) + { + const Vector catalog_paths = get_catalog_paths(info, paths); + for (size_t i = 0; i < domains.size(); i++) { + const std::string &domain_name = domains[i]; + const std::string filename = domain_name + ".mo"; + Catalog catalog; + for (const std::string &path : catalog_paths) { + if (load_file(path + "/" + filename, catalog)) { + break; + } + } + catalogs_.append(std::move(catalog)); + } + } + + const char *translate(const int domain, + const StringRef context, + const StringRef str) const override + { + if (domain < 0 || domain >= catalogs_.size()) { + return nullptr; + } + const MessageKeyRef key{context, str}; + const std::string *result = catalogs_[domain].lookup_ptr_as(key); + return (result) ? result->c_str() : nullptr; + } + + const std::string &error() + { + return error_; + } + + private: + Vector get_catalog_paths(const Info &info, const Vector &paths) + { + /* Find language folders. */ + Vector lang_folders; + if (!info.language.empty()) { + if (!info.variant.empty() && !info.country.empty()) { + lang_folders.append(info.language + "_" + info.country + "@" + info.variant); + } + if (!info.variant.empty()) { + lang_folders.append(info.language + "@" + info.variant); + } + if (!info.country.empty()) { + lang_folders.append(info.language + "_" + info.country); + } + lang_folders.append(info.language); + } + + /* Find catalogs in language folders. */ + Vector result; + result.reserve(lang_folders.size() * paths.size()); + for (const std::string &lang_folder : lang_folders) { + for (const std::string &search_path : paths) { + result.append(search_path + "/" + lang_folder + "/LC_MESSAGES"); + } + } + return result; + } + + bool load_file(const std::string &filepath, Catalog &catalog) + { + MOFile mo(filepath); + if (!mo.error().empty()) { + error_ = mo.error(); + return false; + } + if (mo.empty()) { + return false; + } + + /* Only support UTF-8 encoded files, as created by our msgfmt tool. */ + const std::string mo_encoding = extract(mo.value(0), "charset=", " \r\n;"); + if (mo_encoding.empty()) { + error_ = "Invalid mo-format, encoding is not specified"; + return false; + } + if (mo_encoding != "UTF-8") { + error_ = "supported mo-format, encoding must be UTF-8"; + return false; + } + + /* Create context + key to translated string mapping. */ + for (size_t i = 0; i < mo.size(); i++) { + const MessageKey key(mo.key(i)); + catalog.add(std::move(key), std::string(mo.value(i))); + } + + return true; + } + + static std::string extract(StringRef meta, const std::string &key, const StringRef separators) + { + const size_t pos = meta.find(key); + if (pos == StringRef::not_found) { + return ""; + } + meta = meta.substr(pos + key.size()); + const size_t end_pos = meta.find_first_of(separators); + return std::string(meta.substr(0, end_pos)); + } +}; + +/* Install facet into std::locale. */ + +std::locale::id MessageFacet::id; + +std::locale MessageFacet::install(const std::locale &locale, + const Info &info, + const Vector &domains, + const Vector &paths) +{ + MOMessageFacet *facet = new MOMessageFacet(info, domains, paths); + if (!facet->error().empty()) { + throw std::runtime_error(facet->error()); + return locale; + } + + return std::locale(locale, facet); +} + +} // namespace blender::locale diff --git a/intern/locale/messages.h b/intern/locale/messages.h new file mode 100644 index 00000000000..3a0f933406b --- /dev/null +++ b/intern/locale/messages.h @@ -0,0 +1,44 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * SPDX-License-Identifier: BSL-1.0 + * + * Adapted from boost::locale */ + +#include +#include + +#include "BLI_string_ref.hh" +#include "BLI_vector.hh" + +namespace blender::locale { + +/* Info about a locale. */ +struct Info { + Info(const StringRef locale_full_name); + + std::string language = "C"; + std::string script; + std::string country; + std::string variant; + + std::string to_full_name() const; + +#if defined(__APPLE__) && !defined(WITH_HEADLESS) && !defined(WITH_GHOST_SDL) + static std::string macos_user_locale(); +#endif +}; + +/* Message facet to install into std::locale for translation. */ +class MessageFacet : public std::locale::facet { + public: + static std::locale::id id; + static std::locale install(const std::locale &locale, + const Info &info, + const Vector &domains, /* Application names. */ + const Vector &paths); /* Search paths for .mo files. */ + + virtual const char *translate(const int domain, + const StringRef context, + const StringRef key) const = 0; +}; + +} // namespace blender::locale diff --git a/intern/locale/osx_user_locale.mm b/intern/locale/messages_apple.mm similarity index 74% rename from intern/locale/osx_user_locale.mm rename to intern/locale/messages_apple.mm index 5d1be240f44..c24b9777163 100644 --- a/intern/locale/osx_user_locale.mm +++ b/intern/locale/messages_apple.mm @@ -2,22 +2,24 @@ * * SPDX-License-Identifier: GPL-2.0-or-later */ -#include "boost_locale_wrapper.h" - /** \file * \ingroup intern_locale */ +#include "messages.h" + #import #include +#include -static char *user_locale = nullptr; +namespace blender::locale { +#if !defined(WITH_HEADLESS) && !defined(WITH_GHOST_SDL) /* Get current locale. */ -const char *osx_user_locale() +std::string Info::macos_user_locale() { - ::free(user_locale); + std::string result; @autoreleasepool { CFLocaleRef myCFLocale = CFLocaleCopyCurrent(); @@ -34,8 +36,11 @@ const char *osx_user_locale() nsIdentifier = [NSString stringWithFormat:@"%@_%@", nsIdentifier, nsIdentifier_country]; } - user_locale = ::strdup(nsIdentifier.UTF8String); + result = nsIdentifier.UTF8String; } - return user_locale; + return result + ".UTF-8"; } +#endif + +} // namespace blender::locale diff --git a/source/blender/blentranslation/intern/blt_lang.cc b/source/blender/blentranslation/intern/blt_lang.cc index 3fe958b78c3..90142e1197b 100644 --- a/source/blender/blentranslation/intern/blt_lang.cc +++ b/source/blender/blentranslation/intern/blt_lang.cc @@ -38,7 +38,7 @@ # include "BLI_fileops.h" # include "BLI_linklist.h" -# include "boost_locale_wrapper.h" +# include "blender_locale.h" /* Locale options. */ static const char **locales = nullptr; diff --git a/source/blender/blentranslation/intern/blt_translation.cc b/source/blender/blentranslation/intern/blt_translation.cc index bfe76f7f03c..cbc9fe65b50 100644 --- a/source/blender/blentranslation/intern/blt_translation.cc +++ b/source/blender/blentranslation/intern/blt_translation.cc @@ -24,7 +24,7 @@ #ifdef WITH_INTERNATIONAL # include "BLI_threads.h" -# include "boost_locale_wrapper.h" +# include "blender_locale.h" #endif /* WITH_INTERNATIONAL */ bool BLT_is_default_context(const char *msgctxt)