1 // Copyright 2013 Gushcha Anton 2 module djtext.core; 3 4 import std.experimental.logger; 5 6 /** 7 * Special locale that doesn't have own locale file. 8 */ 9 enum BASE_LOCALE = "en_US"; 10 11 /** 12 * Language file extension 13 */ 14 enum LOCALE_EXTENSION = ".json"; 15 16 private string _defaultLocale = BASE_LOCALE; 17 18 /** 19 * Stores translations. 20 * 21 * It is an array of associative array 22 * 23 * Examples: 24 * -------------------- 25 * // key -> string, value -> string[string] 26 * string[string] h = localeMap["it"]; 27 * assert(h["Hello"] == "Ciao") 28 * -------------------- 29 */ 30 private string[string][string] localeMap; 31 /** 32 * Store fuzzy translations. 33 * 34 * It is an array of string array 35 * Examples: 36 * -------------------- 37 * // key -> string, value -> string[] 38 * string[] it = fuzzyText["it"]; 39 * assert(it[0] == "Testo"); 40 * -------------------- 41 */ 42 private string[][string] fuzzyText; 43 44 /** 45 * Returns translated string $(B s) for specified $(B locale). If locale is empty default 46 * locale will be taken. If locale name is equal to base locale $(B s) string is returned 47 * without modification. 48 * 49 * Localization strings are taken from special files previosly loaded into memory. 50 * 51 * If string $(B s) isn't persists in locale strings it will be put into fuzzy text map. 52 * Fuzzy strings is saved in separate file for each locale to be translated later. 53 * 54 * See_Also: BASE_LOCALE, defaultLocale properties. 55 * 56 * Example: 57 * -------- 58 * assert(getdtext("Hello, world!", "ru_RU") == "Привет, мир!"); 59 * assert(getdtext("Hello, world!", "es_ES") == "Hola, mundo!"); 60 * assert(getdtext("") == ""); 61 * -------- 62 */ 63 string getdtext(string s, string locale = "") { 64 import std.algorithm : canFind; 65 66 if (locale == "") { 67 locale = defaultLocale; 68 } 69 if (locale == BASE_LOCALE) { 70 return s; 71 } 72 73 if (locale in localeMap) { 74 auto map = localeMap[locale]; 75 if (s in map) { 76 return map[s]; 77 } 78 } 79 if (locale !in fuzzyText) { 80 fuzzyText[locale] = []; 81 } 82 if (!fuzzyText[locale].canFind(s)) { 83 fuzzyText[locale] ~= s; 84 } 85 return s; 86 } 87 88 unittest { 89 getdtext("Hola", "es"); 90 assert("es" in fuzzyText); 91 assert(is(typeof(fuzzyText["en"]) == string[])); 92 assert(fuzzyText["es"].length == 1); 93 string[] esArray = fuzzyText["es"]; 94 assert(esArray[0] == "Hola"); 95 } 96 97 /// Short name for getdtext 98 alias _ = getdtext; 99 100 /** 101 * Setups current locale name. If empty string is passed to 102 * $(B getdtext) then default locale will be taken. 103 * 104 * Example: 105 * -------- 106 * defaultLocale = "ru_RU"; 107 * defaultLocale = BASE_LOCALE; 108 * -------- 109 */ 110 void defaultLocale(string locale) { 111 _defaultLocale = locale; 112 } 113 114 /** 115 * Returns current locale name. If empty string is passed to 116 * $(B getdtext) then default locale will be taken. 117 */ 118 string defaultLocale() { 119 return _defaultLocale; 120 } 121 122 /** 123 * Manuall loads localization file with $(B name). 124 * 125 * Example: 126 * -------- 127 * loadLocaleFile("ru_RU"); 128 * loadLocaleFile("es_ES"); 129 * -------- 130 */ 131 void loadLocaleFile(string name) { 132 import std.path : baseName; 133 import std.file : readText; 134 import std..string : endsWith; 135 import std.json : parseJSON, JSONValue; 136 137 if (!name.endsWith(LOCALE_EXTENSION)) { 138 name ~= LOCALE_EXTENSION; 139 } 140 141 auto localeName = baseName(name, LOCALE_EXTENSION); 142 if (localeName !in localeMap) { 143 localeMap[localeName] = ["" : ""]; 144 } 145 auto map = localeMap[localeName]; 146 147 string jsonString = readText(name); 148 JSONValue json = parseJSON(jsonString); 149 150 foreach (string k, v; json) { 151 map[k] = v.str; 152 } 153 } 154 155 void saveFuzzyText() { 156 import std.stdio : File, writeln; 157 158 foreach (locale, strs; fuzzyText) { 159 try { 160 auto file = new File(getFuzzyLocaleFileName(locale), "wr"); 161 scope (exit) { 162 file.close; 163 } 164 bool firstRow = true; 165 foreach (i, s; strs) { 166 if (firstRow) { 167 firstRow = false; 168 } else { 169 file.writeln(","); 170 } 171 file.write(` "` ~ s ~ `" : "~` ~ s ~ `~"`); 172 } 173 } catch (Exception e) { 174 errorf("Failed to save fuzzy text for locale %s", locale); 175 } 176 } 177 } 178 179 private string getFuzzyLocaleFileName(string locale) { 180 return locale ~ ".fuzzy"; 181 } 182 183 unittest { 184 loadAllLocales("./locale"); 185 defaultLocale = "es"; 186 187 _("Hello, world!"); 188 _("Hello, json!"); 189 _("Hello, json!", "ru"); 190 _("Hello, json!", "it"); 191 _("Hello, dj!", "it"); 192 saveFuzzyText(); 193 } 194 195 /** 196 * Loads all localization files in `dir` 197 * 198 * Params: 199 * dir = The directory to iterate over. 200 */ 201 void loadAllLocales(string dir) { 202 import std.algorithm : filter, each; 203 import std..string : endsWith; 204 import std.file : dirEntries, SpanMode; 205 206 dirEntries(dir, SpanMode.shallow).filter!(f => f.name.endsWith(".json")) 207 .each!(f => loadLocaleFile(f.name)); 208 } 209 210 unittest { 211 loadAllLocales("./locale"); 212 defaultLocale = "ru"; 213 assert(_("Hello, world!") == "Привет, мир!"); 214 assert(_("Hello, world!", "es") == "Hola, mundo!"); 215 assert(getdtext("") == ""); 216 assert(getdtext("cul") == "cul"); 217 } 218 219 unittest { 220 class Test { 221 string getHello() { 222 return _("Hello"); 223 } 224 } 225 226 //this setting also takes effect in the test module 227 defaultLocale = "it"; 228 loadAllLocales("./locale"); 229 assert(_("Hello") == "Ciao"); 230 231 auto x = new Test(); 232 assert(x.getHello() == "Ciao"); 233 } 234 235 unittest { 236 loadLocaleFile("./locale/dup.json"); 237 defaultLocale = "dup"; 238 assert(_("Hello") == "Second"); 239 }