glogging
Loading...
Searching...
No Matches
gmessageCatalog.cc
Go to the documentation of this file.
1#include "gmessageCatalog.h"
2
3#include <gemc/goptions/goptions.h>
4#include <gemc/guts/gutilities.h>
5#include <G4ios.hh>
6#include <yaml-cpp/yaml.h>
7
8#include <array>
9#include <algorithm>
10#include <cstdlib>
11#include <mutex>
12#include <set>
13#include <sstream>
14#include <stdexcept>
15#include <utility>
16#include "gloggingConfig.h"
17
18#if defined(__APPLE__)
19#include <CoreFoundation/CoreFoundation.h>
20#endif
21
22namespace {
23namespace fs = std::filesystem;
24
25std::string environment(const char* name) {
26 const auto value = std::getenv(name);
27 return value ? value : "";
28}
29
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";
38 }
39 if (value.empty() || value == "c" || value == "posix" || value.front() == '-' || value.back() == '-') {
40 return "en";
41 }
42 return value;
43}
44
45std::string native_language() {
46#if defined(__APPLE__)
47 const auto languages = CFLocaleCopyPreferredLanguages();
48 std::string result;
49 if (languages) {
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();
55 }
56 }
57 CFRelease(languages);
58 }
59 return result;
60#elif defined(_WIN32)
61 std::array<wchar_t, LOCALE_NAME_MAX_LENGTH> buffer{};
62 if (GetUserDefaultLocaleName(buffer.data(), buffer.size())) {
63 std::string result;
64 for (const auto character : buffer) {
65 if (!character) break;
66 result += static_cast<char>(character); // Windows locale tags are ASCII.
67 }
68 return result;
69 }
70#endif
71 return "en";
72}
73
74std::string option(const GOptions* options, const char* name) {
75 return options && options->doesOptionExist(name) ? options->getOptionalScalarString(name).value_or("") : "";
76}
77
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);
86 }
87 static const auto system_language = normalize_language(native_language());
88 return system_language;
89}
90
91std::vector<fs::path> path_list(const std::string& value) {
92 std::vector<fs::path> result;
93 std::istringstream input(value);
94 std::string path;
95 while (std::getline(input, path, ':')) {
96 if (!path.empty()) result.emplace_back(path);
97 }
98 return result;
99}
100
101std::vector<fs::path> catalog_directories(const GOptions* options) {
102 auto result = path_list(environment("GEMC_TRANSLATION_PATH"));
103 try {
104 const auto executable_dir = gutilities::executable_path().parent_path();
105 const auto prefix = executable_dir.filename() == "bin" ? executable_dir.parent_path() : executable_dir;
106 result.push_back(prefix / GEMC_CATALOG_DIRECTORY);
107 // Source catalogs are used only for a recognized build tree, including non-default build directories.
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");
113 }
114 } catch (const std::exception&) {
115 // An unavailable executable path must not prevent English diagnostics.
116 }
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");
122 }
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);
128 }
129 return unique;
130}
131
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);
136 if (!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"));
139 }
140 }
141 std::sort(result.begin() + 1, result.end());
142 return result;
143}
144
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");
156 }
157 result.insert(position);
158 }
159 return result;
160}
161
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");
169 }
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");
173 }
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");
184 }
185 if (!result.emplace(english, translation).second) throw std::invalid_argument("duplicate English template");
186 }
187 return result;
188}
189} // namespace
190
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;
202 try {
203 // First match wins: regional text precedes base-language text, then path precedence.
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;
209 }
210 }
211 }
212 }
213}
214
215std::shared_ptr<const GMessageCatalog> GMessageCatalog::load(const GOptions* options) {
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;
225 // Resolve filesystem paths only on a cache miss; loggers are also constructed in event data hot paths.
226 const auto directories = language == "en" || language.starts_with("en-")
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);
230 return catalog;
231}
232
233std::string_view GMessageCatalog::translate(std::string_view english) const {
234 const auto found = messages_.find(english);
235 return found != messages_.end() ? std::string_view(found->second) : english;
236}
237
238std::vector<std::filesystem::path> GMessageCatalog::searchDirectories(const GOptions* options) {
239 return catalog_directories(options);
240}
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()