VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
Translations.cpp
Go to the documentation of this file.
7#include <filesystem>
8#include <string>
9#include <string_view>
10
11namespace VoltMod
12{
13
14// Flatten nested JSON objects into dotted keys and keep only string leaves.
15static void FlattenInto(const glz::generic& node, const std::string& prefix, StringMap<std::string>& out)
16{
17 if (!node.is_object())
18 {
19 return;
20 }
21
22 for (const auto& [key, value] : node.get_object())
23 {
24 std::string full = prefix.empty() ? key : prefix + "." + key;
25 if (value.is_object())
26 {
27 FlattenInto(value, full, out);
28 }
29 else if (value.is_string())
30 {
31 out[full] = value.get_string();
32 }
33 }
34}
35
36/**
37 * English text for the keys the framework itself emits, and each language's own name for a
38 * language picker. Translations::Get returns the raw key on a miss, so without this every plugin
39 * had to hand-copy all of them into every language file or players saw the literal string
40 * `cmd.noPermission`. A plugin's own file still wins - these are the floor, not an override.
41 */
43{
45 {"cmd.noPermission", "You do not have permission to use this command."},
46 {"cmd.tooManyArgs", "Too many arguments. Usage: {usage}"},
47 {"cmd.usage", "Usage: {usage}"},
48 {"cmd.usage.target", "target"},
49 {"cmd.usage.targets", "targets"},
50 {"cmd.usage.duration", "duration"},
51 {"cmd.usage.steamId", "steamId"},
52 {"cmd.usage.playerOrSteamId", "target|steamId"},
53 {"cmd.usage.int", "number"},
54 {"cmd.usage.u64", "id"},
55 {"cmd.usage.word", "value"},
56 {"cmd.usage.rest", "reason"},
57 {"cmd.badDuration", "Invalid duration. Use minutes (e.g. 30), 30s/5m/2h/7d, or 'perm'."},
58 {"cmd.badSteamId", "'{token}' is not a valid SteamID64."},
59 {"cmd.badNumber", "'{token}' is not a valid number."},
60 {"target.noMatch", "No player matches '{token}'."},
61 {"target.immune", "'{token}' is immune to that."},
62 {"target.ambiguous", "'{token}' matches {count} players - be more specific."},
63 {"target.dead", "'{token}' is not alive."},
64 {"target.bot", "'{token}' is a bot."},
65 {"menu.stepFailed", "That menu could not be opened."},
66 {"menu.on", "ON"},
67 {"menu.off", "OFF"},
68 {"menu.confirm", "Confirm"},
69 {"menu.cancel", "Cancel"},
70 {"language.en", "English"},
71 {"language.ru", "Russian"},
72 };
73 return defaults;
74}
75
77
78bool Translations::Load(std::string_view dirPath)
79{
80 _translations.clear();
81 namespace fs = std::filesystem;
82
84
85 if (!fs::exists(resolvedPath) || !fs::is_directory(resolvedPath))
86 {
87 Log::Warn("Translations directory not found: {}", resolvedPath.string());
88 return false;
89 }
90
91 int loaded = 0;
92 for (const auto& entry : fs::directory_iterator(resolvedPath))
93 {
94 if (entry.path().extension() != ".json")
95 {
96 continue;
97 }
98
99 std::string langCode = entry.path().stem().string();
100 auto text = ReadAllText(entry.path().string());
101 if (!text)
102 {
103 continue;
104 }
105
106 // Language files remain strict JSON, not JSONC.
108 if (!data)
109 {
110 // Skip one invalid language without discarding the others.
111 Log::Warn("Failed to parse {}: {}", entry.path().string(), data.error().Detail);
112 continue;
113 }
114
115 auto& langMap = _translations[langCode];
116 FlattenInto(*data, "", langMap);
117
118 ++loaded;
119 Log::Info("Loaded translations: {} ({} keys)", langCode, langMap.size());
120 }
121
122 Log::Info("Loaded {} language(s).", loaded);
123
124 // Report missing keys at load time, using "en" as the reference when present.
125 if (auto en = _translations.find("en"); en != _translations.end() && _translations.size() > 1)
126 {
127 for (const auto& [code, keys] : _translations)
128 {
129 if (code == "en")
130 {
131 continue;
132 }
133
134 size_t missing = 0;
135 for (const auto& [key, value] : en->second)
136 {
137 if (!keys.contains(key))
138 {
139 ++missing;
140 }
141 }
142
143 if (missing > 0)
144 {
145 Log::Warn("Translations: '{}' is missing {} key(s) present in en.", code, missing);
146 }
147 }
148 }
149
150 return loaded > 0;
151}
152
153void Translations::SetLanguage(std::string_view lang)
154{
155 _activeLang = lang;
156}
157
158const std::string& Translations::GetLanguage() const
159{
160 return _activeLang;
161}
162
163std::vector<std::string> Translations::GetAvailableLanguages() const
164{
165 std::vector<std::string> langs;
166 langs.reserve(_translations.size());
167 for (const auto& [code, _] : _translations)
168 {
169 langs.push_back(code);
170 }
171
172 return langs;
173}
174
175void Translations::SetPlayerLanguage(int slot, std::string_view lang)
176{
177 _languages.SetLanguage(slot, lang);
178}
179
180std::string_view Translations::PlayerLanguage(int slot) const
181{
182 return _languages.Language(slot);
183}
184
185std::optional<std::string_view> Translations::LookupIn(std::string_view lang, std::string_view key) const
186{
187 auto langIt = _translations.find(lang);
188 if (langIt != _translations.end())
189 {
190 auto keyIt = langIt->second.find(key);
191 if (keyIt != langIt->second.end())
192 {
193 return keyIt->second;
194 }
195 }
196 return std::nullopt;
197}
198
199std::string Translations::Get(std::string_view key) const
200{
201 return Get(key, -1); // negative slot uses the active language
202}
203
204std::optional<std::string_view> Translations::Resolve(std::string_view key, int slot) const
205{
206 const std::string_view picked = PlayerLanguage(slot);
207 const std::string_view lang = picked.empty() ? std::string_view(_activeLang) : picked;
208
209 for (const std::string_view candidate : {lang, std::string_view(_activeLang), std::string_view("en")})
210 {
211 if (auto v = LookupIn(candidate, key))
212 {
213 return v;
214 }
215 }
216
217 if (auto it = KitDefaults().find(key); it != KitDefaults().end())
218 {
219 return it->second;
220 }
221 return std::nullopt;
222}
223
224std::string Translations::Get(std::string_view key, int slot) const
225{
226 return std::string(Resolve(key, slot).value_or(key));
227}
228
229std::string Translations::GetOr(std::string_view key, int slot, std::string_view fallback) const
230{
231 return std::string(Resolve(key, slot).value_or(fallback));
232}
233
234std::string Translations::Get(std::string_view key, int slot, const Tokens& tokens) const
235{
237}
238
239std::string Translations::Get(std::string_view key, const Tokens& tokens) const
240{
241 return Get(key, -1, tokens);
242}
243
244} // namespace VoltMod
static Result< glz::generic > ParseDocument(std::string_view text)
Parse free-form JSON whose shape is not known at compile time.
Definition Json.hpp:108
virtual std::string_view Language(int slot) const =0
virtual void SetLanguage(int slot, std::string_view lang)=0
static std::string SubstituteTokens(std::string text, const Tokens &tokens)
Definition Strings.cpp:176
void SetLanguage(std::string_view lang)
std::string_view PlayerLanguage(int slot) const
Translations(PlayerLanguages &languages)
std::string Get(std::string_view key) const
std::vector< std::string > GetAvailableLanguages() const
bool Load(std::string_view dirPath)
void SetPlayerLanguage(int slot, std::string_view lang)
const std::string & GetLanguage() const
std::string GetOr(std::string_view key, int slot, std::string_view fallback) const
void Info(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:79
void Warn(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:88
static void FlattenInto(const glz::generic &node, const std::string &prefix, StringMap< std::string > &out)
std::filesystem::path ResolvePath(std::string_view relativePath)
a relative path against the base directory. If the path is already absolute, returns it as-is.
Definition Paths.cpp:14
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static const StringMap< std::string > & KitDefaults()
std::unordered_map< std::string, Value, StringHash, std::equal_to<> > StringMap
Definition StringMap.hpp:21
Result< std::string > ReadAllText(std::string_view path)
Read path (resolved via ResolvePath) into a string.
Definition File.cpp:12
std::map< std::string, std::string > Tokens
Definition Strings.hpp:14