mirror of
https://github.com/hyprwm/hyprutils.git
synced 2025-12-20 16:20:07 +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")
|
COMMAND hyprutils_beziercurve "beziercurve")
|
||||||
add_dependencies(tests hyprutils_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
|
# Installation
|
||||||
install(TARGETS hyprutils)
|
install(TARGETS hyprutils)
|
||||||
install(DIRECTORY "include/hyprutils" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
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