Refactor: Replace Boost Locale for translation
Use code adapted from boost::locale to implement locale name parsing and .mo file reading ourselves. Together with #132142 this removes the last direct Blender dependency on Boost. Pull Request: https://projects.blender.org/blender/blender/pulls/133347
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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})
|
||||
|
||||
23
doc/license/Boost-license.txt
Normal file
23
doc/license/Boost-license.txt
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
100
intern/locale/blender_locale.cpp
Normal file
100
intern/locale/blender_locale.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
/* SPDX-FileCopyrightText: 2012 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup intern_locale
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#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<blender::locale::MessageFacet>(locale_global);
|
||||
}
|
||||
// TODO: verify it's not installed for C case
|
||||
/* `if std::has_facet<blender::locale::MessageFacet>(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;
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/* SPDX-FileCopyrightText: 2012 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup intern_locale
|
||||
*/
|
||||
|
||||
#include <boost/locale.hpp>
|
||||
#include <iostream>
|
||||
#include <stdio.h>
|
||||
|
||||
#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> 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<char_message_facet>(locale_global);
|
||||
}
|
||||
/* `if std::has_facet<char_message_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<boost::locale::info>(_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;
|
||||
}
|
||||
554
intern/locale/messages.cpp
Normal file
554
intern/locale/messages.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#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<char> 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<MessageKey, std::string>;
|
||||
Vector<Catalog> catalogs_;
|
||||
std::string error_;
|
||||
|
||||
public:
|
||||
MOMessageFacet(const Info &info,
|
||||
const Vector<std::string> &domains,
|
||||
const Vector<std::string> &paths)
|
||||
{
|
||||
const Vector<std::string> 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<std::string> get_catalog_paths(const Info &info, const Vector<std::string> &paths)
|
||||
{
|
||||
/* Find language folders. */
|
||||
Vector<std::string> 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<std::string> 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<std::string> &domains,
|
||||
const Vector<std::string> &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
|
||||
44
intern/locale/messages.h
Normal file
44
intern/locale/messages.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
* SPDX-License-Identifier: BSL-1.0
|
||||
*
|
||||
* Adapted from boost::locale */
|
||||
|
||||
#include <locale>
|
||||
#include <string>
|
||||
|
||||
#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<std::string> &domains, /* Application names. */
|
||||
const Vector<std::string> &paths); /* Search paths for .mo files. */
|
||||
|
||||
virtual const char *translate(const int domain,
|
||||
const StringRef context,
|
||||
const StringRef key) const = 0;
|
||||
};
|
||||
|
||||
} // namespace blender::locale
|
||||
@@ -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 <Cocoa/Cocoa.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user