Add Chromium-only Blender WebEngine parity work
This commit is contained in:
349
blender-5.2.0/source/blender/blentranslation/intern/blt_lang.cc
Normal file
349
blender-5.2.0/source/blender/blentranslation/intern/blt_lang.cc
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
643
blender-5.2.0/source/blender/blentranslation/intern/messages.cc
Normal file
643
blender-5.2.0/source/blender/blentranslation/intern/messages.cc
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user