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