Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,349 @@
/* SPDX-FileCopyrightText: 2008 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup blt
*
* Main internationalization functions to set the locale and query available languages.
*/
#include <cstdlib>
#include <cstring>
#include <string>
#ifndef _WIN32
# include <clocale>
#endif
#include "RNA_types.hh"
#include "BLT_lang.hh" /* own include */
#include "BLT_translation.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_utildefines.h"
#include "BKE_appdir.hh"
#include "DNA_userdef_types.h"
#include "MEM_guardedalloc.h"
#include "CLG_log.h"
#ifdef WITH_INTERNATIONAL
# include "BLI_fileops.h"
# include "BLI_linklist.h"
# include "messages.hh"
#endif
namespace blender {
static CLG_LogRef LOG = {"translation"};
#ifdef WITH_INTERNATIONAL
/* Locale options. */
static const char **locales = nullptr;
static int num_locales = 0;
static EnumPropertyItem *locales_menu = nullptr;
static int num_locales_menu = 0;
static void free_locales()
{
if (locales_menu) {
int idx = num_locales_menu - 1; /* Last item does not need to be freed! */
while (idx--) {
MEM_delete(locales_menu[idx].identifier); /* Also frees locales's relevant value! */
MEM_delete(locales_menu[idx].name);
MEM_delete(locales_menu[idx].description);
}
}
MEM_SAFE_DELETE(locales_menu);
/* Allocated strings in #locales are shared with #locales_menu[idx].identifier, which are already
* freed above, or are static strings. */
MEM_SAFE_DELETE(locales);
num_locales = num_locales_menu = 0;
}
static void fill_locales()
{
std::optional<std::string> languages_path = BKE_appdir_folder_id(BLENDER_DATAFILES, "locale");
if (!languages_path.has_value()) {
CLOG_WARN(&LOG, "'locale' data path for translations not found");
return;
}
free_locales();
char languages[FILE_MAX];
BLI_path_join(languages, FILE_MAX, languages_path->c_str(), "languages");
LinkNode *lines = BLI_file_read_as_lines(languages);
LinkNode *line = lines;
int idx = 0;
/* This whole "parsing" code is a bit weak, in that it expects strictly formatted input file...
* Should not be a problem, though, as this file is script-generated! */
/* First loop to find highest locale ID */
while (line) {
int t;
char *str = (char *)line->link;
if (ELEM(str[0], '#', '\0')) {
line = line->next;
continue; /* Comment or void... */
}
t = atoi(str);
if (t >= num_locales) {
num_locales = t + 1;
}
num_locales_menu++;
line = line->next;
}
num_locales_menu++; /* The "closing" void item... */
/* And now, build locales and locale_menu! */
locales_menu = MEM_new_array_zeroed<EnumPropertyItem>(num_locales_menu, __func__);
line = lines;
/* Do not allocate locales with zero-sized mem,
* as LOCALE macro uses nullptr locales as invalid marker! */
if (num_locales > 0) {
locales = MEM_new_array_zeroed<const char *>(num_locales, __func__);
while (line) {
const char *loc, *desc, *sep1, *sep2, *sep3;
char *str = (char *)line->link;
if (ELEM(str[0], '#', '\0')) {
line = line->next;
continue;
}
const int id = atoi(str);
sep1 = strchr(str, ':');
if (sep1) {
sep1++;
sep2 = strchr(sep1, ':');
if (sep2) {
locales_menu[idx].value = id;
locales_menu[idx].icon = 0;
locales_menu[idx].name = BLI_strdupn(sep1, sep2 - sep1);
sep2++;
sep3 = strchr(sep2, ':');
if (sep3) {
locales_menu[idx].identifier = loc = BLI_strdupn(sep2, sep3 - sep2);
sep3++;
desc = BLI_sprintfN("Locale code: %s. Translation progress: %s", loc, sep3);
}
else {
locales_menu[idx].identifier = loc = BLI_strdup(sep2);
desc = BLI_strdup(sep2);
}
if (id == 0) {
/* The DEFAULT/Automatic item... */
if (loc[0] != '\0') {
MEM_delete(desc); /* Not used here. */
locales[id] = "";
/* Keep this tip in sync with the one in rna_userdef
* (rna_enum_language_default_items). */
locales_menu[idx].description = BLI_strdup(
"Automatically choose the system-defined language if available, or fall-back to "
"English (US)");
}
/* Menu "label", not to be stored in locales!
* NOTE: Not used since Blender 4.5. */
else {
locales_menu[idx].description = desc;
}
}
else {
locales[id] = loc;
locales_menu[idx].description = desc;
}
idx++;
}
}
line = line->next;
}
}
/* Add closing item to menu! */
locales_menu[idx].identifier = nullptr;
locales_menu[idx].value = locales_menu[idx].icon = 0;
locales_menu[idx].name = locales_menu[idx].description = "";
BLI_file_free_lines(lines);
}
#endif /* WITH_INTERNATIONAL */
const EnumPropertyItem *BLT_lang_RNA_enum_properties()
{
#ifdef WITH_INTERNATIONAL
return locales_menu;
#else
return nullptr;
#endif
}
void BLT_lang_init()
{
/* Make sure LANG is correct and wouldn't cause #std::runtime_error. */
#ifndef _WIN32
/* TODO(sergey): This code only ensures LANG is set properly, so later when
* Cycles will try to use file system API from boost there will be no runtime
* exception generated by #std::locale() which _requires_ having proper LANG
* set in the environment.
*
* Ideally we also need to ensure LC_ALL, LC_MESSAGES and others are also
* set to a proper value, but currently it's not a huge deal and doesn't
* cause any headache.
*
* Would also be good to find nicer way to check if LANG is correct.
*/
const char *lang = BLI_getenv("LANG");
if (lang != nullptr) {
char *old_locale = setlocale(LC_ALL, nullptr);
/* Make a copy so subsequent #setlocale() doesn't interfere. */
old_locale = BLI_strdup(old_locale);
if (setlocale(LC_ALL, lang) == nullptr) {
setenv("LANG", "C", 1);
CLOG_WARN(&LOG, "Falling back to standard locale (\"C\")");
}
setlocale(LC_ALL, old_locale);
MEM_delete(old_locale);
}
#endif
#ifdef WITH_INTERNATIONAL
fill_locales();
#endif
}
void BLT_lang_free()
{
#ifdef WITH_INTERNATIONAL
locale::free();
free_locales();
#endif
}
#ifdef WITH_INTERNATIONAL
static uint lang_from_userdef()
{
const uint language = uint(U.language);
if ((language >= ULANGUAGE_AUTO) && (language < num_locales)) {
return language;
}
return uint(ULANGUAGE_ENGLISH);
}
#endif
#ifdef WITH_INTERNATIONAL
# define ULANGUAGE lang_from_userdef()
# define LOCALE(_id) (locales ? locales[(_id)] : "")
#endif
void BLT_lang_set(const char *str)
{
#ifdef WITH_INTERNATIONAL
int ulang = ULANGUAGE;
std::string locale_name = str ? str : LOCALE(ulang);
/* #locale assumes UTF8, no need to put it in the name. */
const std::optional<std::string> messagepath = BKE_appdir_folder_id(BLENDER_DATAFILES, "locale");
locale::init(locale_name, {TEXT_DOMAIN_NAME}, {messagepath.value_or("")});
#else
(void)str;
#endif
}
const char *BLT_lang_get()
{
#ifdef WITH_INTERNATIONAL
if (BLT_translate()) {
const char *locale = LOCALE(ULANGUAGE);
if (locale[0] == '\0') {
/* Default locale, we have to find which one we are actually using! */
locale = locale::full_name();
}
return locale;
}
return "en_US"; /* Kind of default locale in Blender when no translation enabled. */
#else
return "";
#endif
}
#undef LOCALE
#undef ULANGUAGE
void BLT_lang_locale_explode(const char *locale,
char **language,
char **country,
char **variant,
char **language_country,
char **language_variant)
{
const char *m1, *m2;
char *_t = nullptr;
m1 = strchr(locale, '_');
m2 = strchr(locale, '@');
if (language || language_variant) {
if (m1 || m2) {
_t = m1 ? BLI_strdupn(locale, m1 - locale) : BLI_strdupn(locale, m2 - locale);
if (language) {
*language = _t;
}
}
else if (language) {
*language = BLI_strdup(locale);
}
}
if (country) {
if (m1) {
*country = m2 ? BLI_strdupn(m1 + 1, m2 - (m1 + 1)) : BLI_strdup(m1 + 1);
}
else {
*country = nullptr;
}
}
if (variant) {
if (m2) {
*variant = BLI_strdup(m2 + 1);
}
else {
*variant = nullptr;
}
}
if (language_country) {
if (m1) {
*language_country = m2 ? BLI_strdupn(locale, m2 - locale) : BLI_strdup(locale);
}
else {
*language_country = nullptr;
}
}
if (language_variant) {
if (m2) {
*language_variant = m1 ? BLI_strdupcat(_t, m2) : BLI_strdup(locale);
}
else {
*language_variant = nullptr;
}
}
if (_t && !language) {
MEM_delete(_t);
}
}
} // namespace blender

View File

@@ -0,0 +1,251 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup blt
*
* Manages translation files and provides translation functions.
* (which are optional and can be disabled as a preference).
*/
#include <cstdlib>
#include <cstring>
#include <optional>
#include "BLT_translation.hh"
#include "DNA_userdef_types.h" /* For user settings. */
#ifdef WITH_PYTHON
# include "BPY_extern.hh"
#endif
#ifdef WITH_INTERNATIONAL
# include "BLI_threads.h"
# include "messages.hh"
#endif /* WITH_INTERNATIONAL */
namespace blender {
bool BLT_is_default_context(const StringRef msgctxt)
{
/* We use the "short" test, a more complete one could be:
* return (!msgctxt || !msgctxt[0] || STREQ(msgctxt, BLT_I18NCONTEXT_DEFAULT_BPYRNA))
*/
/* NOTE: trying without the void string check for now, it *should* not be necessary... */
return (msgctxt.is_empty() || msgctxt[0] == BLT_I18NCONTEXT_DEFAULT_BPYRNA[0]);
}
static std::optional<StringRefNull> pgettext(StringRef msgctxt, const StringRef msgid)
{
#ifdef WITH_INTERNATIONAL
if (msgid.is_empty()) {
return std::nullopt;
}
if (BLT_is_default_context(msgctxt)) {
msgctxt = BLT_I18NCONTEXT_DEFAULT;
}
if (const std::optional<StringRefNull> translation = locale::translate(0, msgctxt, msgid)) {
return translation;
}
# ifdef WITH_PYTHON
return BPY_app_translations_py_pgettext(msgctxt, msgid);
# else
return std::nullopt;
# endif
#else
UNUSED_VARS(msgctxt, msgid);
return std::nullopt;
#endif
}
const char *BLT_pgettext(const char *msgctxt, const char *msgid)
{
const std::optional<StringRefNull> translation = pgettext(msgctxt, msgid);
if (!translation) {
return msgid;
}
return translation->c_str();
}
StringRef BLT_pgettext(StringRef msgctxt, StringRef msgid)
{
const std::optional<StringRefNull> translation = pgettext(msgctxt, msgid);
if (!translation) {
return msgid;
}
return *translation;
}
bool BLT_translate()
{
#ifdef WITH_INTERNATIONAL
return BLI_thread_is_main();
#else
return false;
#endif
}
bool BLT_translate_iface()
{
#ifdef WITH_INTERNATIONAL
return BLT_translate() && (U.transopts & USER_TR_IFACE);
#else
return false;
#endif
}
bool BLT_translate_tooltips()
{
#ifdef WITH_INTERNATIONAL
return BLT_translate() && (U.transopts & USER_TR_TOOLTIPS);
#else
return false;
#endif
}
bool BLT_translate_reports()
{
#ifdef WITH_INTERNATIONAL
return BLT_translate() && (U.transopts & USER_TR_REPORTS);
#else
return false;
#endif
}
bool BLT_translate_new_dataname()
{
#ifdef WITH_INTERNATIONAL
return BLT_translate() && (U.transopts & USER_TR_NEWDATANAME);
#else
return false;
#endif
}
template<typename StringT> StringT translate_do(StringT msgctxt, StringT msgid)
{
#ifdef WITH_INTERNATIONAL
if (BLT_translate()) {
return BLT_pgettext(msgctxt, msgid);
}
return msgid;
#else
(void)msgctxt;
return msgid;
#endif
}
const char *BLT_translate_do(const char *msgctxt, const char *msgid)
{
return translate_do(msgctxt, msgid);
}
StringRef BLT_translate_do(StringRef msgctxt, StringRef msgid)
{
return translate_do(msgctxt, msgid);
}
template<typename StringT> StringT translate_do_iface(StringT msgctxt, StringT msgid)
{
#ifdef WITH_INTERNATIONAL
if (BLT_translate_iface()) {
return BLT_pgettext(msgctxt, msgid);
}
return msgid;
#else
(void)msgctxt;
return msgid;
#endif
}
const char *BLT_translate_do_iface(const char *msgctxt, const char *msgid)
{
return translate_do_iface(msgctxt, msgid);
}
StringRef BLT_translate_do_iface(StringRef msgctxt, StringRef msgid)
{
return translate_do_iface(msgctxt, msgid);
}
template<typename StringT> StringT translate_do_tooltip(StringT msgctxt, StringT msgid)
{
#ifdef WITH_INTERNATIONAL
if (BLT_translate_tooltips()) {
return BLT_pgettext(msgctxt, msgid);
}
return msgid;
#else
(void)msgctxt;
return msgid;
#endif
}
const char *BLT_translate_do_tooltip(const char *msgctxt, const char *msgid)
{
return translate_do_tooltip(msgctxt, msgid);
}
StringRef BLT_translate_do_tooltip(StringRef msgctxt, StringRef msgid)
{
return translate_do_tooltip(msgctxt, msgid);
}
template<typename StringT> StringT translate_do_report(StringT msgctxt, StringT msgid)
{
#ifdef WITH_INTERNATIONAL
if (BLT_translate_reports()) {
return BLT_pgettext(msgctxt, msgid);
}
return msgid;
#else
(void)msgctxt;
return msgid;
#endif
}
const char *BLT_translate_do_report(const char *msgctxt, const char *msgid)
{
return translate_do_report(msgctxt, msgid);
}
StringRef BLT_translate_do_report(StringRef msgctxt, StringRef msgid)
{
return translate_do_report(msgctxt, msgid);
}
template<typename StringT> StringT translate_do_new_dataname(StringT msgctxt, StringT msgid)
{
#ifdef WITH_INTERNATIONAL
if (BLT_translate_new_dataname()) {
return BLT_pgettext(msgctxt, msgid);
}
return msgid;
#else
(void)msgctxt;
return msgid;
#endif
}
const char *BLT_translate_do_new_dataname(const char *msgctxt, const char *msgid)
{
return translate_do_new_dataname(msgctxt, msgid);
}
StringRef BLT_translate_do_new_dataname(StringRef msgctxt, StringRef msgid)
{
return translate_do_new_dataname(msgctxt, msgid);
}
} // namespace blender

View File

@@ -0,0 +1,157 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup blt
*/
#include <algorithm>
#include <array>
#include <string>
#include <fmt/format.h>
#include "BLT_date_string.hh"
#include "BLT_translation.hh"
namespace blender::date_string {
struct LocalePatterns {
StringRef locale;
StringRef date;
};
static const LocalePatterns &get_locale_patterns(const StringRef locale_iso)
{
static constexpr std::array<LocalePatterns, 9> patterns = {{
{"", "{d:02} {b} {Y}"}, /* default */
{"en_US", "{d:02} {b} {Y}"}, /* English (US) */
{"ar_EG", "{d:02} {b} {Y}"}, /* Arabic (Egypt) */
{"zh_HANS", "{Y}年{m}月{d}日"}, /* Chinese (Simplified) */
{"zh_HANT", "{Y}年{m}月{d}日"}, /* Chinese (Traditional) */
{"hu_HU", "{Y}. {b} {d:02}"}, /* Hungarian */
{"ja_JP", "{Y}年{m}月{d}日"}, /* Japanese */
{"ko_KR", "{Y}년 {m}월 {d}일"}, /* Korean */
{"ur", "{d:02} {b} {Y}"}, /* Urdu */
}};
for (const LocalePatterns &pattern : patterns) {
if (pattern.locale == locale_iso) {
return pattern;
}
}
/* Fallback default pattern (index 0). */
return patterns[0];
}
std::string time(const std::tm &date_time, TimeFormat format)
{
std::string_view time_format_str;
switch (format) {
case TimeFormat::H24:
time_format_str = "{H:02}:{M:02}";
break;
case TimeFormat::H12:
time_format_str = "{I}:{M:02} {p}";
}
return fmt::format(fmt::runtime(time_format_str),
fmt::arg("H", date_time.tm_hour),
fmt::arg("M", date_time.tm_min),
fmt::arg("S", date_time.tm_sec),
fmt::arg("I", (date_time.tm_hour % 12) == 0 ? 12 : (date_time.tm_hour % 12)),
fmt::arg("p",
(date_time.tm_hour < 12) ? CTX_IFACE_(BLT_I18NCONTEXT_TIME, "AM") :
CTX_IFACE_(BLT_I18NCONTEXT_TIME, "PM")));
}
std::string date(const std::tm &date_time, const StringRef locale_iso, DateFormat format)
{
static constexpr std::array<StringRef, 12> months = {CTX_N_(BLT_I18NCONTEXT_TIME, "Jan"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Feb"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Mar"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Apr"),
CTX_N_(BLT_I18NCONTEXT_TIME, "May"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Jun"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Jul"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Aug"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Sep"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Oct"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Nov"),
CTX_N_(BLT_I18NCONTEXT_TIME, "Dec")};
BLI_assert(date_time.tm_mon >= 0 && date_time.tm_mon < 12);
const int month_index = std::clamp(date_time.tm_mon, 0, 11);
std::string_view date_format_str;
switch (format) {
case DateFormat::Default: {
const LocalePatterns &pattern = get_locale_patterns(locale_iso);
date_format_str = pattern.date;
break;
}
case DateFormat::LE_Slash:
date_format_str = "{d:02}/{m:02}/{Y}";
break;
case DateFormat::LE_Dot:
date_format_str = "{d:02}.{m:02}.{Y}";
break;
case DateFormat::LE_Dash:
date_format_str = "{d:02}-{m:02}-{Y}";
break;
case DateFormat::ME_Slash:
date_format_str = "{m:02}/{d:02}/{Y}";
break;
case DateFormat::BE_Slash:
date_format_str = "{Y}/{m:02}/{d:02}";
break;
case DateFormat::BE_Dot:
date_format_str = "{Y}.{m:02}.{d:02}";
break;
case DateFormat::BE_Dash:
date_format_str = "{Y}-{m:02}-{d:02}";
break;
}
return fmt::format(fmt::runtime(date_format_str),
fmt::arg("Y", date_time.tm_year + 1900),
fmt::arg("m", date_time.tm_mon + 1),
fmt::arg("b", CTX_IFACE_(BLT_I18NCONTEXT_TIME, months[month_index])),
fmt::arg("d", date_time.tm_mday));
}
std::string datetime(const std::tm &date_time,
const StringRef locale_iso,
DateFormat date_format,
TimeFormat time_format,
const std::tm *now,
const StringRef today,
const StringRef yesterday)
{
bool is_today = false;
bool is_yesterday = false;
if (now && !today.is_empty() && !yesterday.is_empty()) {
is_today = (date_time.tm_yday == now->tm_yday && date_time.tm_year == now->tm_year);
std::tm yesterday_tm = *now;
yesterday_tm.tm_mday--;
mktime(&yesterday_tm);
is_yesterday = (date_time.tm_yday == yesterday_tm.tm_yday &&
date_time.tm_year == yesterday_tm.tm_year);
}
const std::string time_s = time(date_time, time_format);
if (is_today) {
return fmt::format("{} {}", today, time_s);
}
if (is_yesterday) {
return fmt::format("{} {}", yesterday, time_s);
}
const std::string date_s = date(date_time, locale_iso, date_format);
return fmt::format("{} {}", date_s, time_s);
}
} // namespace blender::date_string

View File

@@ -0,0 +1,643 @@
/* 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 */
/** \file
* \ingroup blt
*/
#include "messages.hh"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <memory>
#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
#include "CLG_log.h"
namespace blender::locale {
static CLG_LogRef LOG = {"translation"};
/* 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';
}
/* Info about a locale. */
class Info {
public:
std::string language = "C";
std::string script;
std::string country;
std::string variant;
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) {
std::string region = buf;
if (locale_name == "zh") {
if (region == "TW" || region == "HK" || region == "MO") {
/* Traditional for Taiwan, Hong Kong, Macau. */
locale_name += "_HANT";
}
else {
/* Simplified for all other areas. */
locale_name += "_HANS";
}
}
else {
locale_name += "_" + region;
}
}
}
}
#endif
parse_from_lang(locale_name);
}
std::string 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;
}
private:
/* Locale parsing. */
bool parse_from_variant(const std::string_view input)
{
if (language == "C" || input.empty()) {
return false;
}
variant = input;
/* No assumptions, just make it lowercase. */
for (char &c : variant) {
make_lower_ascii(c);
}
return true;
}
bool parse_from_encoding(const std::string_view input)
{
const int64_t 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(input.substr(end + 1));
}
bool parse_from_country(const std::string_view input)
{
if (language == "C") {
return false;
}
const int64_t 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 (language == "en" && tmp == "US_POSIX") {
language = "C";
tmp.clear();
}
else if (tmp.size() != 3u ||
std::find_if_not(tmp.begin(), tmp.end(), is_numeric_ascii) != tmp.end())
{
return false;
}
}
country = tmp;
if (end >= input.size()) {
return true;
}
if (input[end] == '.') {
return parse_from_encoding(input.substr(end + 1));
}
BLI_assert(input[end] == '@');
return parse_from_variant(input.substr(end + 1));
}
bool parse_from_script(const std::string_view input)
{
const int64_t 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(input);
}
for (char &c : tmp) {
if (!is_lower_ascii(c) && !make_lower_ascii(c)) {
return parse_from_country(input);
}
}
make_upper_ascii(tmp[0]); /* Capitalize first letter only. */
script = tmp;
if (end >= input.size()) {
return true;
}
if (ELEM(input[end], '-', '_')) {
return parse_from_country(input.substr(end + 1));
}
if (input[end] == '.') {
return parse_from_encoding(input.substr(end + 1));
}
BLI_assert(input[end] == '@');
return parse_from_variant(input.substr(end + 1));
}
bool parse_from_lang(const std::string_view input)
{
const int64_t 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 (!ELEM(tmp, "c", "posix")) { /* Keep default if C or POSIX. */
language = tmp;
}
if (end >= input.size()) {
return true;
}
if (ELEM(input[end], '-', '_')) {
return parse_from_script(input.substr(end + 1));
}
if (input[end] == '.') {
return parse_from_encoding(input.substr(end + 1));
}
BLI_assert(input[end] == '@');
return parse_from_variant(input.substr(end + 1));
}
};
/* .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(this->context, this->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) {
this->str = c;
}
else {
this->context = c.substr(0, pos);
this->str = c.substr(pos + 1);
}
}
uint64_t hash() const
{
return get_default_hash(this->context, this->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;
}
/* Messages translation based on .mo files. */
class MOMessages {
using Catalog = Map<MessageKey, std::string>;
Vector<Catalog> catalogs_;
std::string error_;
public:
MOMessages(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));
}
}
std::optional<StringRefNull> translate(const int domain,
const StringRef context,
const StringRef str) const
{
if (domain < 0 || domain >= catalogs_.size()) {
return std::nullopt;
}
const MessageKeyRef key{context, str};
const std::string *result = catalogs_[domain].lookup_ptr_as(key);
if (!result) {
return std::nullopt;
}
return *result;
}
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()) {
return {};
}
/* Blender uses non-standard uppercase script zh_HANS instead of zh_Hans, try both. */
Vector<std::string> scripts = {info.script};
if (!info.script.empty()) {
std::string script_uppercase = info.script;
for (char &c : script_uppercase) {
make_upper_ascii(c);
}
scripts.append(script_uppercase);
}
for (const std::string &script : scripts) {
std::string language = info.language;
if (!script.empty()) {
language += "_" + script;
}
if (!info.variant.empty() && !info.country.empty()) {
lang_folders.append(language + "_" + info.country + "@" + info.variant);
}
if (!info.variant.empty()) {
lang_folders.append(language + "@" + info.variant);
}
if (!info.country.empty()) {
lang_folders.append(language + "_" + info.country);
}
lang_folders.append(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 UTF8 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;
}
CLOG_INFO(&LOG, "Load messages from \"%s\"", filepath.c_str());
/* 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));
}
};
/* Public API */
/* Lazily init inside function so it gets destructed before guardedalloc leak check. */
static std::unique_ptr<MOMessages> &global_messages()
{
static std::unique_ptr<MOMessages> global_messages_;
return global_messages_;
}
static std::string global_full_name;
void init(const StringRef locale_full_name,
const Vector<std::string> &domains,
const Vector<std::string> &paths)
{
Info info(locale_full_name);
if (global_full_name == info.to_full_name()) {
return;
}
global_messages() = std::make_unique<MOMessages>(info, domains, paths);
global_full_name = info.to_full_name();
if (global_messages()->error().empty()) {
CLOG_INFO(&LOG, "Locale %s used for translation", global_full_name.c_str());
}
else {
CLOG_ERROR(
&LOG, "Locale %s: %s", global_full_name.c_str(), global_messages()->error().c_str());
free();
}
}
void free()
{
global_messages().reset();
global_full_name = "";
}
std::optional<StringRefNull> translate(const int domain,
const StringRef context,
const StringRef key)
{
if (!global_messages()) {
return std::nullopt;
}
return global_messages()->translate(domain, context, key);
}
const char *full_name()
{
return global_full_name.c_str();
}
} // namespace blender::locale

View File

@@ -0,0 +1,31 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: BSL-1.0 */
/** \file
* \ingroup blt
*
* Adapted from `boost::locale`.
*/
#include <optional>
#include <string>
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
namespace blender::locale {
void init(const StringRef locale_full_name, /* Local name. */
const Vector<std::string> &domains, /* Application names. */
const Vector<std::string> &paths); /* Search paths for .mo files. */
void free();
std::optional<StringRefNull> translate(int domain, StringRef context, StringRef key);
const char *full_name();
#if defined(__APPLE__) && !defined(WITH_HEADLESS) && !defined(WITH_GHOST_SDL)
std::string macos_user_locale();
#endif
} // namespace blender::locale

View File

@@ -0,0 +1,46 @@
/* SPDX-FileCopyrightText: 2012 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup blt
*/
#include "messages.hh"
#import <Cocoa/Cocoa.h>
#include <cstdlib>
#include <string>
namespace blender::locale {
#if !defined(WITH_HEADLESS) && !defined(WITH_GHOST_SDL)
/* Get current locale. */
std::string macos_user_locale()
{
std::string result;
@autoreleasepool {
CFLocaleRef myCFLocale = CFLocaleCopyCurrent();
NSLocale *myNSLocale = (NSLocale *)myCFLocale;
[myNSLocale autorelease];
/* This produces gettext-invalid locale in recent macOS versions (11.4),
* like `ko-Kore_KR` instead of `ko_KR`. See #88877. */
// NSString *nsIdentifier = [myNSLocale localeIdentifier];
NSString *nsIdentifier = myNSLocale.languageCode;
NSString *nsIdentifier_country = myNSLocale.countryCode;
if (nsIdentifier.length != 0 && nsIdentifier_country.length != 0) {
nsIdentifier = [NSString stringWithFormat:@"%@_%@", nsIdentifier, nsIdentifier_country];
}
result = nsIdentifier.UTF8String;
}
return result + ".UTF-8";
}
#endif
} // namespace blender::locale