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:
Brecht Van Lommel
2024-12-14 17:32:49 +01:00
parent 9cdbb72c1a
commit 820f261371
15 changed files with 746 additions and 184 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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})

View 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.

View File

@@ -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

View File

@@ -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}")

View 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;
}

View File

@@ -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
View 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
View 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

View File

@@ -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

View File

@@ -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;

View File

@@ -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)