Localization.cpp 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. #include "Localization.h"
  2. #include <algorithm>
  3. #include <cctype>
  4. #include <cstdlib>
  5. #include <regex>
  6. using namespace WallpaperEngine::Data::Parsers;
  7. namespace {
  8. std::string lower (std::string value) {
  9. std::ranges::transform (value, value.begin (), [] (unsigned char c) { return std::tolower (c); });
  10. return value;
  11. }
  12. void replaceAll (std::string& text, const std::string& from, const std::string& to) {
  13. for (std::size_t at = text.find (from); at != std::string::npos; at = text.find (from, at + to.size ())) {
  14. text.replace (at, from.size (), to);
  15. }
  16. }
  17. } // namespace
  18. Localization::Localization (const std::optional<JSON>& general, std::string locale) {
  19. if (general.has_value ()) {
  20. if (const auto localization = general->optional ("localization"); localization.has_value ()) {
  21. for (const auto& language : localization->items ()) {
  22. if (!language.value ().is_object ()) {
  23. continue;
  24. }
  25. Table table;
  26. for (const auto& entry : language.value ().items ()) {
  27. if (entry.value ().is_string ()) {
  28. table.emplace (entry.key (), entry.value ().template get<std::string> ());
  29. }
  30. }
  31. this->m_tables.emplace (lower (language.key ()), std::move (table));
  32. }
  33. }
  34. }
  35. if (this->m_tables.empty ()) {
  36. return;
  37. }
  38. locale = lower (locale);
  39. const std::string language = locale.substr (0, locale.find ('-'));
  40. // the exact language, then any variant of it (zh-chs for zh_CN), then English, then whatever the project has
  41. const auto exact = this->m_tables.find (locale);
  42. const auto variant = std::ranges::find_if (this->m_tables, [&] (const auto& entry) {
  43. return entry.first.starts_with (language + "-") || entry.first == language;
  44. });
  45. const auto english = this->m_tables.find ("en-us");
  46. const auto anyEnglish
  47. = std::ranges::find_if (this->m_tables, [] (const auto& entry) { return entry.first.starts_with ("en"); });
  48. if (english != this->m_tables.end ()) {
  49. this->m_english = &english->second;
  50. } else if (anyEnglish != this->m_tables.end ()) {
  51. this->m_english = &anyEnglish->second;
  52. }
  53. if (exact != this->m_tables.end ()) {
  54. this->m_preferred = &exact->second;
  55. } else if (variant != this->m_tables.end ()) {
  56. this->m_preferred = &variant->second;
  57. } else if (this->m_english != nullptr) {
  58. this->m_preferred = this->m_english;
  59. } else {
  60. this->m_preferred = &this->m_tables.begin ()->second;
  61. }
  62. if (this->m_english == nullptr) {
  63. this->m_english = this->m_preferred;
  64. }
  65. }
  66. std::string Localization::resolve (const std::string& text) const {
  67. if (text.empty ()) {
  68. return "";
  69. }
  70. for (const Table* table : { this->m_preferred, this->m_english }) {
  71. if (table == nullptr) {
  72. continue;
  73. }
  74. if (const auto found = table->find (text); found != table->end ()) {
  75. if (const auto cleaned = clean (found->second); !cleaned.empty ()) {
  76. return cleaned;
  77. }
  78. }
  79. }
  80. std::string cleaned = clean (text);
  81. // a key nobody translated: "ui_music_type_0" reads better as "Music type 0" than as it is
  82. if (cleaned.starts_with ("ui_") && cleaned.find (' ') == std::string::npos) {
  83. cleaned = cleaned.substr (3);
  84. std::ranges::replace (cleaned, '_', ' ');
  85. if (!cleaned.empty ()) {
  86. cleaned[0] = static_cast<char> (std::toupper (static_cast<unsigned char> (cleaned[0])));
  87. }
  88. }
  89. return cleaned;
  90. }
  91. std::string Localization::clean (const std::string& text) {
  92. static const std::regex smallPrint (R"(<small[^>]*>.*?</small>)", std::regex::icase);
  93. static const std::regex lineBreak (R"(<br\s*/?>)", std::regex::icase);
  94. static const std::regex tag (R"(<[^>]*>)");
  95. std::string result = std::regex_replace (text, smallPrint, "");
  96. result = std::regex_replace (result, lineBreak, " ");
  97. result = std::regex_replace (result, tag, " ");
  98. replaceAll (result, "&nbsp;", " ");
  99. replaceAll (result, "&lt;", "<");
  100. replaceAll (result, "&gt;", ">");
  101. replaceAll (result, "&quot;", "\"");
  102. replaceAll (result, "&#39;", "'");
  103. replaceAll (result, "&amp;", "&");
  104. // collapse whitespace, including the newlines some strings carry
  105. std::string collapsed;
  106. bool space = true;
  107. for (const char c : result) {
  108. if (std::isspace (static_cast<unsigned char> (c))) {
  109. if (!space) {
  110. collapsed += ' ';
  111. }
  112. space = true;
  113. } else {
  114. collapsed += c;
  115. space = false;
  116. }
  117. }
  118. if (!collapsed.empty () && collapsed.back () == ' ') {
  119. collapsed.pop_back ();
  120. }
  121. return collapsed;
  122. }
  123. std::string Localization::systemLocale () {
  124. for (const char* variable : { "LC_ALL", "LC_MESSAGES", "LANG" }) {
  125. const char* value = std::getenv (variable);
  126. if (value == nullptr || *value == '\0') {
  127. continue;
  128. }
  129. std::string locale = value;
  130. if (locale == "C" || locale == "POSIX") {
  131. break;
  132. }
  133. // "de_DE.UTF-8@euro" -> "de-de"
  134. locale = locale.substr (0, locale.find_first_of (".@"));
  135. std::ranges::replace (locale, '_', '-');
  136. return lower (locale);
  137. }
  138. return "en-us";
  139. }