3#include <gemc/goptions/goptions.h>
4#include <gemc/guts/gutilities.h>
6#include <yaml-cpp/yaml.h>
16#include "gloggingConfig.h"
19#include <CoreFoundation/CoreFoundation.h>
23namespace fs = std::filesystem;
25std::string environment(
const char* name) {
26 const auto value = std::getenv(name);
27 return value ? value :
"";
30std::string normalize_language(std::string value) {
31 const auto suffix = value.find_first_of(
".@");
32 if (suffix != std::string::npos) value.resize(suffix);
33 for (
auto& character : value) {
34 if (character ==
'_') character =
'-';
35 if (character >=
'A' && character <=
'Z') character +=
'a' -
'A';
36 if (!((character >=
'a' && character <=
'z') || (character >=
'0' && character <=
'9') ||
37 character ==
'-'))
return "en";
39 if (value.empty() || value ==
"c" || value ==
"posix" || value.front() ==
'-' || value.back() ==
'-') {
45std::string native_language() {
47 const auto languages = CFLocaleCopyPreferredLanguages();
50 if (CFArrayGetCount(languages) > 0) {
51 const auto language =
static_cast<CFStringRef
>(CFArrayGetValueAtIndex(languages, 0));
52 std::array<char, 256> buffer{};
53 if (CFStringGetCString(language, buffer.data(), buffer.size(), kCFStringEncodingUTF8)) {
54 result = buffer.data();
61 std::array<wchar_t, LOCALE_NAME_MAX_LENGTH> buffer{};
62 if (GetUserDefaultLocaleName(buffer.data(), buffer.size())) {
64 for (
const auto character : buffer) {
65 if (!character)
break;
66 result +=
static_cast<char>(character);
74std::string option(
const GOptions* options,
const char* name) {
78std::string requested_language(
const GOptions* options) {
79 const auto explicit_language = option(options,
"language");
80 if (!explicit_language.empty() && explicit_language !=
"auto")
return normalize_language(explicit_language);
81 const auto override = environment(
"GEMC_LANGUAGE");
82 if (!
override.empty() &&
override !=
"auto")
return normalize_language(
override);
83 for (
const auto name : {
"LC_ALL",
"LC_MESSAGES",
"LANG"}) {
84 const auto value = environment(name);
85 if (!value.empty())
return normalize_language(value);
87 static const auto system_language = normalize_language(native_language());
88 return system_language;
91std::vector<fs::path> path_list(
const std::string& value) {
92 std::vector<fs::path> result;
93 std::istringstream input(value);
95 while (std::getline(input, path,
':')) {
96 if (!path.empty()) result.emplace_back(path);
101std::vector<fs::path> catalog_directories(
const GOptions* options) {
102 auto result = path_list(environment(
"GEMC_TRANSLATION_PATH"));
105 const auto prefix = executable_dir.filename() ==
"bin" ? executable_dir.parent_path() : executable_dir;
106 result.push_back(prefix / GEMC_CATALOG_DIRECTORY);
108 const auto build = fs::path(GEMC_CATALOG_BUILD_DIRECTORY).lexically_normal().generic_string();
109 if (executable_dir.generic_string() == build || executable_dir.generic_string().starts_with(build +
"/")) {
110 result.emplace_back(GEMC_CATALOG_SOURCE_DIRECTORY);
111 }
else if (fs::exists(prefix /
"build.ninja")) {
112 result.push_back(prefix.parent_path() /
"translations");
114 }
catch (
const std::exception&) {
117 auto plugin_paths = path_list(option(options,
"plugin_path") +
":" + environment(
"GEMC_PLUGIN_PATH"));
118 for (
const auto& directory : plugin_paths) {
119 result.push_back(directory.parent_path() / GEMC_CATALOG_DIRECTORY);
120 std::error_code error;
121 if (fs::exists(directory /
"build.ninja", error)) result.push_back(directory.parent_path() /
"translations");
123 std::vector<fs::path> unique;
124 for (
const auto& directory : result) {
125 std::error_code error;
126 const auto absolute = fs::absolute(directory, error).lexically_normal();
127 if (!error && std::find(unique.begin(), unique.end(), absolute) == unique.end()) unique.push_back(absolute);
132std::vector<fs::path> catalog_files(
const fs::path& directory,
const std::string& language) {
133 std::vector<fs::path> result{directory / (language +
".yaml")};
134 std::error_code error;
135 const fs::directory_iterator entries(directory, error);
137 for (
auto iterator = entries; iterator != fs::directory_iterator{} && !error; iterator.increment(error)) {
138 if (iterator->is_directory(error) && !error) result.push_back(iterator->path() / (language +
".yaml"));
141 std::sort(result.begin() + 1, result.end());
145std::set<unsigned> placeholders(std::string_view message) {
146 std::set<unsigned> result;
147 for (std::size_t index = 0; index < message.size(); ++index) {
148 if (message[index] !=
'%' || index + 1 == message.size())
continue;
149 if (message[index + 1] ==
'%') { ++index;
continue; }
150 if (message[index + 1] <
'0' || message[index + 1] >
'9')
continue;
151 unsigned position = 0;
152 if (message[index + 1] ==
'0')
throw std::invalid_argument(
"placeholder starts with zero");
153 while (index + 1 < message.size() && message[index + 1] >=
'0' && message[index + 1] <=
'9') {
154 position = position * 10 +
static_cast<unsigned>(message[++index] -
'0');
155 if (position > 99)
throw std::invalid_argument(
"placeholder exceeds %99");
157 result.insert(position);
162std::map<std::string, std::string, std::less<>> read_catalog(
const fs::path& path,
163 const std::string& language) {
164 const auto catalog = YAML::LoadFile(path.string());
165 if (!catalog.IsMap())
throw std::invalid_argument(
"catalog is not a mapping");
166 std::set<std::string> fields;
167 for (
const auto& field : catalog) {
168 if (!fields.insert(field.first.as<std::string>()).second)
throw std::invalid_argument(
"duplicate field");
170 if (catalog[
"schema"].as<int>() != 1 ||
171 normalize_language(catalog[
"language"].as<std::string>()) != language) {
172 throw std::invalid_argument(
"catalog schema or language does not match");
174 const auto messages = catalog[
"messages"];
175 if (!messages.IsMap())
throw std::invalid_argument(
"messages is not a mapping");
176 std::map<std::string, std::string, std::less<>> result;
177 for (
const auto& entry : messages) {
178 const auto english = entry.first.as<std::string>();
179 const auto translation = entry.second.as<std::string>();
180 if (english.find_first_not_of(
" \t\r\n") == std::string::npos ||
181 translation.find_first_not_of(
" \t\r\n") == std::string::npos ||
182 placeholders(english) != placeholders(translation)) {
183 throw std::invalid_argument(
"empty message or changed placeholders");
185 if (!result.emplace(english, translation).second)
throw std::invalid_argument(
"duplicate English template");
191GMessageCatalog::GMessageCatalog(std::string language,
const std::vector<fs::path>& directories)
192 : language_(std::move(language)) {
193 if (language_ ==
"en" || language_.starts_with(
"en-"))
return;
194 std::vector<std::string> variants{language_};
195 const auto separator = language_.find(
'-');
196 if (separator != std::string::npos) variants.push_back(language_.substr(0, separator));
197 for (
const auto& variant : variants) {
198 for (
const auto& directory : directories) {
199 for (
const auto& path : catalog_files(directory, variant)) {
200 std::error_code error;
201 if (!fs::is_regular_file(path, error) || error)
continue;
204 const auto messages = read_catalog(path, variant);
205 messages_.insert(messages.begin(), messages.end());
206 }
catch (
const std::exception& error) {
207 G4cerr <<
"Ignoring invalid translation catalog <" << path.string() <<
">: "
208 << error.what() << G4endl;
216 const auto language = requested_language(options);
217 using Key = std::array<std::string, 4>;
218 static std::mutex mutex;
219 static std::map<Key, std::shared_ptr<const GMessageCatalog>> cache;
220 const std::lock_guard lock(mutex);
221 const Key key{
language, environment(
"GEMC_TRANSLATION_PATH"), option(options,
"plugin_path"),
222 environment(
"GEMC_PLUGIN_PATH")};
223 const auto found = cache.find(key);
224 if (found != cache.end())
return found->second;
227 ? std::vector<fs::path>{} : catalog_directories(options);
228 auto catalog = std::shared_ptr<const GMessageCatalog>(
new GMessageCatalog(
language, directories));
229 cache.emplace(key, catalog);
234 const auto found = messages_.find(english);
235 return found != messages_.end() ? std::string_view(found->second) : english;
239 return catalog_directories(options);
std::string_view translate(std::string_view english) const
Returns a translated template, or the original view when no translation is available.
const std::string & language() const
Normalized requested language tag; a missing catalog still falls back to English per message.
static std::shared_ptr< const GMessageCatalog > load(const GOptions *options=nullptr)
Resolve language and catalog paths from options/environment and reuse the cached catalog.
static std::vector< std::filesystem::path > searchDirectories(const GOptions *options=nullptr)
Catalog search paths shared by logging and GUI translation startup.
bool doesOptionExist(const std::string &tag) const
std::optional< std::string > getOptionalScalarString(const std::string &tag) const
std::filesystem::path executable_path()