mirror of
https://github.com/hyprwm/hyprutils.git
synced 2025-12-20 05:50:11 +01:00
parent
926689ddb9
commit
968f881222
6 changed files with 301 additions and 0 deletions
|
|
@ -120,6 +120,15 @@ add_test(
|
|||
COMMAND hyprutils_beziercurve "beziercurve")
|
||||
add_dependencies(tests hyprutils_beziercurve)
|
||||
|
||||
add_executable(hyprutils_i18n "tests/i18n.cpp")
|
||||
target_link_libraries(hyprutils_i18n PRIVATE hyprutils PkgConfig::deps)
|
||||
add_test(
|
||||
NAME "I18n"
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
|
||||
COMMAND hyprutils_i18n "i18n")
|
||||
add_dependencies(tests hyprutils_i18n)
|
||||
|
||||
|
||||
# Installation
|
||||
install(TARGETS hyprutils)
|
||||
install(DIRECTORY "include/hyprutils" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
|
|
|
|||
55
include/hyprutils/i18n/I18nEngine.hpp
Normal file
55
include/hyprutils/i18n/I18nEngine.hpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
#include "../memory/UniquePtr.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
|
||||
namespace Hyprutils::I18n {
|
||||
struct SI18nEngineImpl;
|
||||
|
||||
typedef std::unordered_map<std::string, std::string> translationVarMap;
|
||||
typedef std::function<std::string(const translationVarMap&)> translationFn;
|
||||
|
||||
class CI18nLocale {
|
||||
public:
|
||||
~CI18nLocale() = default;
|
||||
|
||||
std::string locale();
|
||||
std::string stem();
|
||||
std::string full();
|
||||
|
||||
private:
|
||||
CI18nLocale(std::string fullLocale);
|
||||
|
||||
std::string m_locale, m_rawFullLocale;
|
||||
|
||||
friend class CI18nEngine;
|
||||
};
|
||||
|
||||
class CI18nEngine {
|
||||
public:
|
||||
CI18nEngine();
|
||||
~CI18nEngine();
|
||||
/*
|
||||
Register a translation entry. The internal translation db is kept as a vector,
|
||||
so make sure your keys are linear, don't use e.g. 2 billion as that will call
|
||||
.resize() on the vec to 2 billion.
|
||||
|
||||
If you pass a Fn, you can do logic, e.g. "1 point" vs "2 points".
|
||||
*/
|
||||
void registerEntry(const std::string& locale, uint64_t key, std::string&& translationUTF8);
|
||||
void registerEntry(const std::string& locale, uint64_t key, translationFn&& translationFn);
|
||||
|
||||
void setFallbackLocale(const std::string& locale);
|
||||
|
||||
std::string localizeEntry(const std::string& locale, uint64_t key, const translationVarMap& map);
|
||||
|
||||
CI18nLocale getSystemLocale();
|
||||
|
||||
private:
|
||||
Memory::CUniquePointer<SI18nEngineImpl> m_impl;
|
||||
};
|
||||
}
|
||||
110
src/i18n/I18nEngine.cpp
Normal file
110
src/i18n/I18nEngine.cpp
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#include "I18nEngine.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <locale>
|
||||
#include <hyprutils/string/String.hpp>
|
||||
|
||||
using namespace Hyprutils::I18n;
|
||||
using namespace Hyprutils;
|
||||
using namespace Hyprutils::String;
|
||||
|
||||
CI18nEngine::CI18nEngine() : m_impl(Memory::makeUnique<SI18nEngineImpl>()) {
|
||||
;
|
||||
}
|
||||
CI18nEngine::~CI18nEngine() = default;
|
||||
|
||||
void CI18nEngine::registerEntry(const std::string& locale, uint64_t key, std::string&& translationUTF8) {
|
||||
auto& entryVec = m_impl->entries[locale];
|
||||
|
||||
if (entryVec.size() <= key)
|
||||
entryVec.resize(key + 1);
|
||||
|
||||
entryVec[key].entry = std::move(translationUTF8);
|
||||
entryVec[key].exists = true;
|
||||
}
|
||||
|
||||
void CI18nEngine::registerEntry(const std::string& locale, uint64_t key, translationFn&& translationFn) {
|
||||
auto& entryVec = m_impl->entries[locale];
|
||||
|
||||
if (entryVec.size() <= key)
|
||||
entryVec.resize(key + 1);
|
||||
|
||||
entryVec[key].fn = std::move(translationFn);
|
||||
entryVec[key].exists = true;
|
||||
}
|
||||
|
||||
void CI18nEngine::setFallbackLocale(const std::string& locale) {
|
||||
m_impl->fallbackLocale = locale;
|
||||
}
|
||||
|
||||
std::string CI18nEngine::localizeEntry(const std::string& locale, uint64_t key, const translationVarMap& map) {
|
||||
SI18nTranslationEntry* entry = nullptr;
|
||||
|
||||
if (m_impl->entries.contains(locale) && m_impl->entries[locale].size() > key)
|
||||
entry = &m_impl->entries[locale][key];
|
||||
|
||||
if (locale.contains('_')) {
|
||||
|
||||
if (!entry || !entry->exists) {
|
||||
// try to fall back to lang_LANG
|
||||
auto stem = locale.substr(0, locale.find('_'));
|
||||
auto stemUpper = stem;
|
||||
std::ranges::transform(stemUpper, stemUpper.begin(), ::toupper);
|
||||
auto newLocale = std::format("{}_{}", stem, stemUpper);
|
||||
if (m_impl->entries.contains(newLocale) && m_impl->entries[newLocale].size() > key)
|
||||
entry = &m_impl->entries[newLocale][key];
|
||||
}
|
||||
|
||||
if (!entry || !entry->exists) {
|
||||
// try to fall back to any lang prefixed with our prefix
|
||||
|
||||
auto stem = locale.substr(0, locale.find('_') + 1);
|
||||
for (const auto& [k, v] : m_impl->entries) {
|
||||
if (k.starts_with(stem) || k == stem) {
|
||||
if (m_impl->entries.contains(k) && m_impl->entries[k].size() > key)
|
||||
entry = &m_impl->entries[k][key];
|
||||
|
||||
if (entry && entry->exists)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// locale doesn't have a _, e.g. pl
|
||||
// find any locale that has the same stem
|
||||
for (const auto& [k, v] : m_impl->entries) {
|
||||
if (k.starts_with(locale + "_") || k == locale) {
|
||||
if (m_impl->entries.contains(k) && m_impl->entries[k].size() > key)
|
||||
entry = &m_impl->entries[k][key];
|
||||
|
||||
if (entry && entry->exists)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry || !entry->exists) {
|
||||
// fall back to general fallback
|
||||
if (m_impl->entries.contains(m_impl->fallbackLocale) && m_impl->entries[m_impl->fallbackLocale].size() > key)
|
||||
entry = &m_impl->entries[m_impl->fallbackLocale][key];
|
||||
}
|
||||
|
||||
if (!entry || !entry->exists)
|
||||
return "";
|
||||
|
||||
std::string rawStr = entry->entry;
|
||||
|
||||
if (entry->fn)
|
||||
rawStr = entry->fn(map);
|
||||
|
||||
for (const auto& e : map) {
|
||||
replaceInString(rawStr, "{" + e.first + "}", e.second);
|
||||
}
|
||||
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
CI18nLocale CI18nEngine::getSystemLocale() {
|
||||
return CI18nLocale(std::locale("").name());
|
||||
}
|
||||
16
src/i18n/I18nEngine.hpp
Normal file
16
src/i18n/I18nEngine.hpp
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <hyprutils/i18n/I18nEngine.hpp>
|
||||
|
||||
namespace Hyprutils::I18n {
|
||||
struct SI18nTranslationEntry {
|
||||
bool exists = false;
|
||||
std::string entry = "";
|
||||
translationFn fn = nullptr;
|
||||
};
|
||||
|
||||
struct SI18nEngineImpl {
|
||||
std::unordered_map<std::string, std::vector<SI18nTranslationEntry>> entries;
|
||||
std::string fallbackLocale = "en_US";
|
||||
};
|
||||
};
|
||||
44
src/i18n/I18nLocale.cpp
Normal file
44
src/i18n/I18nLocale.cpp
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#include <hyprutils/i18n/I18nEngine.hpp>
|
||||
|
||||
using namespace Hyprutils::I18n;
|
||||
|
||||
static std::string extractLocale(std::string locale) {
|
||||
// localeStr is very arbitrary... from my testing, it can be:
|
||||
// en_US.UTF-8
|
||||
// LC_CTYPE=en_US
|
||||
// POSIX
|
||||
// *
|
||||
//
|
||||
// We only return e.g. en_US or pl_PL, or pl
|
||||
|
||||
if (locale == "POSIX")
|
||||
return "en_US";
|
||||
if (locale == "*")
|
||||
return "en_US";
|
||||
|
||||
if (locale.contains('='))
|
||||
locale = locale.substr(locale.find('=') + 1);
|
||||
|
||||
if (locale.contains('.'))
|
||||
locale = locale.substr(0, locale.find('.'));
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
CI18nLocale::CI18nLocale(std::string fullLocale) : m_rawFullLocale(std::move(fullLocale)) {
|
||||
m_locale = extractLocale(m_rawFullLocale);
|
||||
}
|
||||
|
||||
std::string CI18nLocale::locale() {
|
||||
return m_locale;
|
||||
}
|
||||
|
||||
std::string CI18nLocale::stem() {
|
||||
if (m_locale.contains('_'))
|
||||
return m_locale.substr(0, m_locale.find('_'));
|
||||
return m_locale;
|
||||
}
|
||||
|
||||
std::string CI18nLocale::full() {
|
||||
return m_rawFullLocale;
|
||||
}
|
||||
67
tests/i18n.cpp
Normal file
67
tests/i18n.cpp
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#include <hyprutils/i18n/I18nEngine.hpp>
|
||||
#include "shared.hpp"
|
||||
#include <print>
|
||||
|
||||
using namespace Hyprutils::I18n;
|
||||
|
||||
enum eTxtKeys : uint64_t {
|
||||
TXT_KEY_HELLO,
|
||||
TXT_KEY_I_HAVE_APPLES,
|
||||
TXT_KEY_FALLBACK,
|
||||
};
|
||||
|
||||
int main(int argc, char** argv, char** envp) {
|
||||
int ret = 0;
|
||||
|
||||
CI18nEngine engine;
|
||||
|
||||
std::println("System locale: {}, stem: {}", engine.getSystemLocale().locale(), engine.getSystemLocale().stem());
|
||||
|
||||
engine.setFallbackLocale("en_US");
|
||||
|
||||
engine.registerEntry("en_US", TXT_KEY_HELLO, "Hello World!");
|
||||
engine.registerEntry("en_US", TXT_KEY_I_HAVE_APPLES, [](const translationVarMap& m) {
|
||||
if (std::stoi(m.at("count")) == 1)
|
||||
return "I have {count} apple.";
|
||||
else
|
||||
return "I have {count} apples.";
|
||||
});
|
||||
engine.registerEntry("en_US", TXT_KEY_FALLBACK, "Fallback string!");
|
||||
|
||||
engine.registerEntry("pl_PL", TXT_KEY_HELLO, "Witaj świecie!");
|
||||
engine.registerEntry("pl_PL", TXT_KEY_I_HAVE_APPLES, [](const translationVarMap& m) {
|
||||
const auto COUNT = std::stoi(m.at("count"));
|
||||
if (COUNT == 1)
|
||||
return "Mam {count} jabłko.";
|
||||
else if (COUNT < 5)
|
||||
return "Mam {count} jabłka.";
|
||||
else
|
||||
return "Mam {count} jabłek.";
|
||||
});
|
||||
|
||||
engine.registerEntry("es_XX", TXT_KEY_FALLBACK, "I don't speak spanish");
|
||||
engine.registerEntry("es_ES", TXT_KEY_FALLBACK, "I don't speak spanish here either");
|
||||
|
||||
EXPECT(engine.localizeEntry("en_US", TXT_KEY_HELLO, {}), "Hello World!");
|
||||
EXPECT(engine.localizeEntry("pl_PL", TXT_KEY_HELLO, {}), "Witaj świecie!");
|
||||
EXPECT(engine.localizeEntry("de_DE", TXT_KEY_HELLO, {}), "Hello World!");
|
||||
|
||||
EXPECT(engine.localizeEntry("en_US", TXT_KEY_I_HAVE_APPLES, {{"count", "1"}}), "I have 1 apple.");
|
||||
EXPECT(engine.localizeEntry("en_US", TXT_KEY_I_HAVE_APPLES, {{"count", "2"}}), "I have 2 apples.");
|
||||
|
||||
EXPECT(engine.localizeEntry("pl_PL", TXT_KEY_I_HAVE_APPLES, {{"count", "1"}}), "Mam 1 jabłko.");
|
||||
EXPECT(engine.localizeEntry("pl_PL", TXT_KEY_I_HAVE_APPLES, {{"count", "2"}}), "Mam 2 jabłka.");
|
||||
EXPECT(engine.localizeEntry("pl_PL", TXT_KEY_I_HAVE_APPLES, {{"count", "5"}}), "Mam 5 jabłek.");
|
||||
|
||||
EXPECT(engine.localizeEntry("pl", TXT_KEY_I_HAVE_APPLES, {{"count", "5"}}), "Mam 5 jabłek.");
|
||||
|
||||
EXPECT(engine.localizeEntry("pl_XX", TXT_KEY_I_HAVE_APPLES, {{"count", "5"}}), "Mam 5 jabłek.");
|
||||
EXPECT(engine.localizeEntry("en_XX", TXT_KEY_I_HAVE_APPLES, {{"count", "2"}}), "I have 2 apples.");
|
||||
|
||||
EXPECT(engine.localizeEntry("es_YY", TXT_KEY_FALLBACK, {}), "I don't speak spanish here either");
|
||||
EXPECT(engine.localizeEntry("es_XX", TXT_KEY_FALLBACK, {}), "I don't speak spanish");
|
||||
|
||||
EXPECT(engine.localizeEntry("pl_PL", TXT_KEY_FALLBACK, {}), "Fallback string!");
|
||||
|
||||
return ret;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue