1 #include "i18n.h"
2
3 #include "Directories.h"
4 #include "Logger.h"
5 #include "OptionsDB.h"
6 #include "StringTable.h"
7
8 #include <boost/locale.hpp>
9 #include <boost/algorithm/string/case_conv.hpp>
10
11 #include <mutex>
12
13 namespace {
14 std::map<std::string, std::shared_ptr<const StringTable>> stringtables;
15 std::recursive_mutex stringtable_access_mutex;
16 bool stringtable_filename_init = false;
17
18 // fallback stringtable to look up key in if entry is not found in currently configured stringtable
DevDefaultEnglishStringtablePath()19 boost::filesystem::path DevDefaultEnglishStringtablePath()
20 { return GetResourceDir() / "stringtables/en.txt"; }
21
22 // filename to use as default value for stringtable filename option.
23 // based on the system's locale. not necessarily the same as the
24 // "dev default" (english) stringtable filename for fallback lookup
25 // includes "<resource-dir>/stringtables/" directory part of path
GetDefaultStringTableFileName()26 boost::filesystem::path GetDefaultStringTableFileName() {
27 std::string lang;
28
29 // early return when unable to get locale language string
30 try {
31 lang = std::use_facet<boost::locale::info>(GetLocale()).language();
32 } catch(const std::bad_cast&) {
33 ErrorLogger() << "Bad locale cast when setting default language";
34 }
35
36 boost::algorithm::to_lower(lang);
37
38 // handle failed locale lookup or C locale
39 if (lang.empty() || lang == "c" || lang == "posix") {
40 WarnLogger() << "Lanuage not detected from locale: \"" << lang << "\"; falling back to default en";
41 lang = "en";
42 } else {
43 DebugLogger() << "Detected locale language: " << lang;
44 }
45
46 boost::filesystem::path lang_filename{ lang + ".txt" };
47 boost::filesystem::path default_stringtable_path{ GetResourceDir() / "stringtables" / lang_filename };
48
49 // default to english if locale-derived filename not present
50 if (!IsExistingFile(default_stringtable_path)) {
51 WarnLogger() << "Detected language file not present: " << PathToString(default_stringtable_path) << " Reverting to en.txt";
52 default_stringtable_path = DevDefaultEnglishStringtablePath();
53 }
54
55 if (!IsExistingFile(default_stringtable_path))
56 ErrorLogger() << "Default english stringtable file also not presennt!!: " << PathToString(default_stringtable_path);
57
58 DebugLogger() << "GetDefaultStringTableFileName returning: " << PathToString(default_stringtable_path);
59 return default_stringtable_path;
60 }
61
62 // sets the stringtable filename option default value.
63 // also checks the option-set stringtable path, and if it is blank or the
64 // specified file doesn't exist, tries to reinterpret the option value as
65 // a path in the standard location, or reverts to the default stringtable
66 // location if other attempts fail.
InitStringtableFileName()67 void InitStringtableFileName() {
68 stringtable_filename_init = true;
69
70 // set option default value based on system locale
71 auto default_stringtable_path = GetDefaultStringTableFileName();
72 GetOptionsDB().SetDefault("resource.stringtable.path", PathToString(default_stringtable_path));
73
74 // get option-configured stringtable path. may be the default empty
75 // string (set by call to: db.Add<std::string>("resource.stringtable.path" ...
76 // or this may have been overridden from one of the config XML files or from
77 // a command line argument.
78 std::string option_path = GetOptionsDB().Get<std::string>("resource.stringtable.path");
79 boost::filesystem::path stringtable_path{option_path};
80
81 // verify that option-derived stringtable file exists, with fallbacks
82 DebugLogger() << "Stringtable option path: " << option_path;
83
84 if (option_path.empty()) {
85 DebugLogger() << "Stringtable option path not specified yet, using default: " << PathToString(default_stringtable_path);
86 stringtable_path = PathToString(default_stringtable_path);
87 GetOptionsDB().Set("resource.stringtable.path", PathToString(stringtable_path));
88 return;
89 }
90
91 bool set_option = false;
92
93 if (!IsExistingFile(stringtable_path)) {
94 set_option = true;
95 // try interpreting path as a filename located in the stringtables directory
96 stringtable_path = GetResourceDir() / "stringtables" / option_path;
97 }
98 if (!IsExistingFile(stringtable_path)) {
99 set_option = true;
100 // try interpreting path as directory and filename in resources directory
101 stringtable_path = GetResourceDir() / option_path;
102 }
103 if (!IsExistingFile(stringtable_path)) {
104 set_option = true;
105 // fall back to default option value
106 ErrorLogger() << "Stringtable option path file is missing: " << PathToString(stringtable_path);
107 DebugLogger() << "Resetting to default: " << PathToString(default_stringtable_path);
108 stringtable_path = default_stringtable_path;
109 }
110
111 if (set_option)
112 GetOptionsDB().Set("resource.stringtable.path", PathToString(stringtable_path));
113 }
114
115 // get currently set stringtable filename option value, or the default value
116 // if the currenty value is empty
GetStringTableFileName()117 std::string GetStringTableFileName() {
118 std::lock_guard<std::recursive_mutex> stringtable_lock(stringtable_access_mutex);
119 // initialize option value and default on first call
120 if (!stringtable_filename_init)
121 InitStringtableFileName();
122
123 std::string option_path = GetOptionsDB().Get<std::string>("resource.stringtable.path");
124 if (option_path.empty())
125 return GetOptionsDB().GetDefault<std::string>("resource.stringtable.path");
126 else
127 return option_path;
128 }
129
GetStringTable(boost::filesystem::path stringtable_path)130 const StringTable& GetStringTable(boost::filesystem::path stringtable_path) {
131 std::lock_guard<std::recursive_mutex> stringtable_lock(stringtable_access_mutex);
132
133 if (!stringtable_filename_init)
134 InitStringtableFileName();
135
136 // ensure the default stringtable is loaded first
137 auto default_stringtable_filename{GetOptionsDB().GetDefault<std::string>("resource.stringtable.path")};
138 auto default_stringtable_it = stringtables.find(default_stringtable_filename);
139 if (default_stringtable_it == stringtables.end()) {
140 auto table = std::make_shared<StringTable>(default_stringtable_filename);
141 stringtables[default_stringtable_filename] = table;
142 default_stringtable_it = stringtables.find(default_stringtable_filename);
143 }
144
145 auto stringtable_filename = PathToString(stringtable_path);
146
147 // attempt to find requested stringtable...
148 auto it = stringtables.find(stringtable_filename);
149 if (it != stringtables.end())
150 return *(it->second);
151
152 // if not already loaded, load, store, and return,
153 // using default stringtable for fallback expansion lookups
154 auto table = std::make_shared<StringTable>(stringtable_filename, default_stringtable_it->second);
155 stringtables[stringtable_filename] = table;
156
157 return *table;
158 }
159
GetStringTable()160 const StringTable& GetStringTable()
161 { return GetStringTable(GetStringTableFileName()); }
162
GetDevDefaultStringTable()163 const StringTable& GetDevDefaultStringTable()
164 { return GetStringTable(DevDefaultEnglishStringtablePath()); }
165 }
166
GetLocale(const std::string & name)167 std::locale GetLocale(const std::string& name) {
168 static bool locale_init { false };
169 // Initialize backend and generator on first use, provide a log for current enivornment locale
170 static auto locale_backend = boost::locale::localization_backend_manager::global();
171 if (!locale_init)
172 locale_backend.select("std");
173 static boost::locale::generator locale_gen(locale_backend);
174 if (!locale_init) {
175 locale_gen.locale_cache_enabled(true);
176 try {
177 InfoLogger() << "Global locale: " << std::use_facet<boost::locale::info>(locale_gen("")).name();
178 } catch (const std::runtime_error&) {
179 ErrorLogger() << "Global locale: set to invalid locale, setting to C locale";
180 std::locale::global(std::locale::classic());
181 }
182 locale_init = true;
183 }
184
185 std::locale retval;
186 try {
187 retval = locale_gen(name);
188 } catch(const std::runtime_error&) {
189 ErrorLogger() << "Requested locale \"" << name << "\" is not a valid locale for this operating system";
190 return std::locale::classic();
191 }
192
193 TraceLogger() << "Requested " << (name.empty() ? "(default)" : name) << " locale"
194 << " returning " << std::use_facet<boost::locale::info>(retval).name();
195 return retval;
196 }
197
FlushLoadedStringTables()198 void FlushLoadedStringTables() {
199 std::lock_guard<std::recursive_mutex> stringtable_lock(stringtable_access_mutex);
200 stringtables.clear();
201 }
202
UserString(const std::string & str)203 const std::string& UserString(const std::string& str) {
204 std::lock_guard<std::recursive_mutex> stringtable_lock(stringtable_access_mutex);
205 if (GetStringTable().StringExists(str))
206 return GetStringTable()[str];
207 return GetDevDefaultStringTable()[str];
208 }
209
UserStringList(const std::string & key)210 std::vector<std::string> UserStringList(const std::string& key) {
211 std::lock_guard<std::recursive_mutex> stringtable_lock(stringtable_access_mutex);
212 std::vector<std::string> result;
213 std::istringstream template_stream(UserString(key));
214 std::string item;
215 while (std::getline(template_stream, item))
216 result.push_back(item);
217 return result;
218 }
219
UserStringExists(const std::string & str)220 bool UserStringExists(const std::string& str) {
221 std::lock_guard<std::recursive_mutex> stringtable_lock(stringtable_access_mutex);
222 return GetStringTable().StringExists(str) || GetDevDefaultStringTable().StringExists(str);
223 }
224
FlexibleFormat(const std::string & string_to_format)225 boost::format FlexibleFormat(const std::string &string_to_format) {
226 try {
227 boost::format retval(string_to_format);
228 retval.exceptions(boost::io::no_error_bits);
229 return retval;
230 } catch (const std::exception& e) {
231 ErrorLogger() << "FlexibleFormat caught exception when formatting: " << e.what();
232 }
233 boost::format retval(UserString("ERROR"));
234 retval.exceptions(boost::io::no_error_bits);
235 return retval;
236 }
237
Language()238 const std::string& Language() {
239 std::lock_guard<std::recursive_mutex> stringtable_lock(stringtable_access_mutex);
240 return GetStringTable().Language();
241 }
242
RomanNumber(unsigned int n)243 std::string RomanNumber(unsigned int n) {
244 //letter pattern (N) and the associated values (V)
245 static const std::string N[] = { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
246 static const unsigned int V[] = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
247 unsigned int remainder = n; //remainder of the number to be written
248 int i = 0; //pattern index
249 std::string retval = "";;
250 if (n == 0) return ""; //the romans didn't know there is a zero, read a book about history of the zero if you want to know more
251 //Roman numbers are written using patterns, you chosse the highest pattern lower that the number
252 //write it down, and substract it's value until you reach zero.
253
254 // safety check to avoid very long loops
255 if (n > 10000)
256 return "!";
257
258 //we start with the highest pattern and reduce the size every time it doesn't fit
259 while (remainder > 0) {
260 //check if number is larger than the actual pattern value
261 if (remainder >= V[i]) {
262 //write pattern down
263 retval += N[i];
264 //reduce number
265 remainder -= V[i];
266 } else {
267 //we need the next pattern
268 i++;
269 }
270 }
271 return retval;
272 }
273
274 namespace {
275 const double SMALL_UI_DISPLAY_VALUE = 1.0e-6;
276 const double LARGE_UI_DISPLAY_VALUE = 9.99999999e+9;
277 const double UNKNOWN_UI_DISPLAY_VALUE = std::numeric_limits<double>::infinity();
278
RoundMagnitude(double mag,int digits)279 double RoundMagnitude(double mag, int digits) {
280 // power of 10 of highest valued digit in number
281 // = 2 (100's) for 234.4
282 // = 4 (10000's) for 45324
283 int pow10 = static_cast<int>(floor(log10(mag)));
284 //std::cout << "magnitude power of 10: " << pow10 << std::endl;
285
286 // round number to fit in requested number of digits
287 // shift number by power of 10 so that ones digit is the lowest-value digit
288 // that will be retained in final number
289 // eg. 45324 with 3 digits -> pow10 = 4, digits = 3
290 // want to shift 3 to the 1's digits position, so need 4 - 3 + 1 shift = 2
291 double rounding_factor = pow(10.0, static_cast<double>(pow10 - digits + 1));
292 // shift, round, shift back. this leaves 0 after the lowest retained digit
293 mag = mag / rounding_factor;
294 mag = round(mag);
295 mag *= rounding_factor;
296 //std::cout << "magnitude after initial rounding to " << digits << " digits: " << mag << std::endl;
297
298 // rounding may have changed the power of 10 of the number
299 // eg. 9999 with 3 digits, shifted to 999.9, rounded to 1000,
300 // shifted back to 10000, now the power of 10 is 5 instead of 4.
301 // so, redo this calculation
302 pow10 = static_cast<int>(floor(log10(mag)));
303 rounding_factor = pow(10.0, static_cast<double>(pow10 - digits + 1));
304 mag = mag / rounding_factor;
305 mag = round(mag);
306 mag *= rounding_factor;
307 //std::cout << "magnitude after second rounding to " << digits << " digits: " << mag << std::endl;
308
309 return mag;
310 }
311 }
312
DoubleToString(double val,int digits,bool always_show_sign)313 std::string DoubleToString(double val, int digits, bool always_show_sign) {
314 std::string text; // = ""
315
316 // minimum digits is 2. Fewer than this and things can't be sensibly displayed.
317 // eg. 300 with 2 digits is 0.3k. With 1 digits, it would be unrepresentable.
318 digits = std::max(digits, 2);
319
320 // default result for sentinel value
321 if (val == UNKNOWN_UI_DISPLAY_VALUE)
322 return UserString("UNKNOWN_VALUE_SYMBOL");
323
324 double mag = std::abs(val);
325
326 // early termination if magnitude is 0
327 if (mag == 0.0 || RoundMagnitude(mag, digits + 1) == 0.0) {
328 std::string format = "%1." + std::to_string(digits - 1) + "f";
329 text += (boost::format(format) % mag).str();
330 return text;
331 }
332
333 // prepend signs if neccessary
334 int effective_sign = EffectiveSign(val);
335 if (effective_sign == -1)
336 text += "-";
337 else if (always_show_sign)
338 text += "+";
339
340 if (mag > LARGE_UI_DISPLAY_VALUE)
341 mag = LARGE_UI_DISPLAY_VALUE;
342
343 // if value is effectively 0, avoid unnecessary later processing
344 if (effective_sign == 0) {
345 text = "0.0";
346 for (int n = 2; n < digits; ++n)
347 text += "0"; // fill in 0's to required number of digits
348 return text;
349 }
350
351 //std::cout << std::endl << "DoubleToString val: " << val << " digits: " << digits << std::endl;
352 const double initial_mag = mag;
353
354 // round magnitude to appropriate precision for requested digits
355 mag = RoundMagnitude(initial_mag, digits);
356 int pow10 = static_cast<int>(floor(log10(mag)));
357
358
359 // determine base unit for number: the next lower power of 10^3 from the
360 // number (inclusive)
361 int pow10_digits_above_pow1000 = 0;
362 if (pow10 >= 0)
363 pow10_digits_above_pow1000 = pow10 % 3;
364 else
365 pow10_digits_above_pow1000 = (pow10 % 3) + 3; // +3 ensures positive result of mod
366 int unit_pow10 = pow10 - pow10_digits_above_pow1000;
367
368 if (digits == 2 && pow10_digits_above_pow1000 == 2) {
369 digits = 3;
370
371 // rounding to 2 digits when 3 digits must be shown to display the
372 // number will cause apparent rounding issues.
373 // re-do rounding for 3 digits of precision
374 mag = RoundMagnitude(initial_mag, digits);
375 pow10 = static_cast<int>(floor(log10(mag)));
376
377 if (pow10 >= 0)
378 pow10_digits_above_pow1000 = pow10 % 3;
379 else
380 pow10_digits_above_pow1000 = (pow10 % 3) + 3; // +3 ensures positive result of mod
381 unit_pow10 = pow10 - pow10_digits_above_pow1000;
382 }
383
384
385 // special limit: currently don't use any base unit powers below 0 (1's digit)
386 if (unit_pow10 < 0)
387 unit_pow10 = 0;
388
389 int lowest_digit_pow10 = pow10 - digits + 1;
390
391 //std::cout << "unit power of 10: " << unit_pow10
392 // << " pow10 digits above pow1000: " << pow10_digits_above_pow1000
393 // << " lowest_digit_pow10: " << lowest_digit_pow10
394 // << std::endl;
395
396 // fraction digits:
397 int fraction_digits = std::max(0, std::min(digits - 1, unit_pow10 - lowest_digit_pow10));
398 //std::cout << "fraction_digits: " << fraction_digits << std::endl;
399
400
401 // scale number by unit power of 10
402 // eg. if mag = 45324 and unit_pow10 = 3, get mag = 45.324
403 mag /= pow(10.0, static_cast<double>(unit_pow10));
404
405
406 std::string format;
407 format += "%" + std::to_string(digits) + "." +
408 std::to_string(fraction_digits) + "f";
409 text += (boost::format(format) % mag).str();
410
411 // append base scale SI prefix (as postfix)
412 switch (unit_pow10) {
413 case -15:
414 text += "f"; // femto
415 break;
416 case -12:
417 text += "p"; // pico
418 break;
419 case -9:
420 text += "n"; // nano
421 break;
422 case -6:
423 text += "\xC2\xB5"; // micro. mu in UTF-8
424 break;
425 case -3:
426 text += "m"; // milli
427 break;
428 case 3:
429 text += "k"; // kilo
430 break;
431 case 6:
432 text += "M"; // Mega
433 break;
434 case 9:
435 text += "G"; // Giga
436 break;
437 case 12:
438 text += "T"; // Tera
439 break;
440 default:
441 break;
442 }
443 return text;
444 }
445
EffectiveSign(double val)446 int EffectiveSign(double val) {
447 if (val == UNKNOWN_UI_DISPLAY_VALUE)
448 return 0;
449
450 if (std::abs(val) >= SMALL_UI_DISPLAY_VALUE) {
451 if (val >= 0)
452 return 1;
453 else
454 return -1;
455 } else {
456 return 0;
457 }
458 }
459
460