1 // Copyright 2013 Gushcha Anton 2 /* 3 Boost Software License - Version 1.0 - August 17th, 2003 4 5 Permission is hereby granted, free of charge, to any person or organization 6 obtaining a copy of the software and accompanying documentation covered by 7 this license (the "Software") to use, reproduce, display, distribute, 8 execute, and transmit the Software, and to prepare derivative works of the 9 Software, and to permit third-parties to whom the Software is furnished to 10 do so, all subject to the following: 11 12 The copyright notices in the Software and this entire statement, including 13 the above license grant, this restriction and the following disclaimer, 14 must be included in all copies of the Software, in whole or in part, and 15 all derivative works of the Software, unless such copies or derivative 16 works are solely in the form of machine-executable object code generated by 17 a source language processor. 18 19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 22 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 23 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 24 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 DEALINGS IN THE SOFTWARE. 26 */ 27 // Written in D programing language 28 /** 29 * Module provides functions to handle localization. Translated string 30 * is searched in special files with .lang extention by exact matching 31 * with original string. 32 * 33 * Lang files are loaded in memory at program start up from current 34 * directory. Additional localizations can be loaded with $(B loadLocaleFile) 35 * function. 36 * 37 * Example: 38 * -------- 39 * import std.stdio, std.opt, dtext; 40 * 41 * void main(string[] args) 42 * { 43 * string locale; 44 * getopt(args, 45 * "l|lang", &locale); 46 * 47 * defaultLocale = locale; 48 * 49 * writeln(_("Hello, world!")); \\ or use getdtext instead _ 50 * } 51 * -------- 52 * 53 * If text for translation cannot be found in specified locale name, the text will 54 * be saved and written down to a special fuzzy texts file at program shutdown. That 55 * should help to add new localization fast and without program recompilation. 56 * 57 * TODO: 58 * <ul> 59 * <li>Load localization files on demand;</li> 60 * <li>Add ability to unload unused locales.</li> 61 * </ul> 62 */ 63 module dtext; 64 65 /** 66 * Special locale that doesn't have own locale file. System won't 67 * create additional information (fuzzy file or locale map). 68 */ 69 enum BASE_LOCALE = "en_US"; 70 71 /** 72 * File extention which will be searched in current directory to load locale information. 73 */ 74 enum LOCALE_EXTENTION = ".lang"; 75 76 /** 77 * Returns translated string $(B s) for specified $(B locale). If locale is empty default 78 * locale will be taken. If locale name is equal to base locale $(B s) string is returned 79 * without modification. 80 * 81 * Localization strings are taken from special files previosly loaded into memory. 82 * 83 * If string $(B s) isn't persists in locale strings it will be put into fuzzy text map. 84 * Fuzzy strings is saved in separate file for each locale to be translated later. 85 * 86 * See_Also: BASE_LOCALE, defaultLocale properties. 87 * 88 * Example: 89 * -------- 90 * assert(getdtext("Hello, world!", "ru_RU") == "Привет, мир!"); 91 * assert(getdtext("Hello, world!", "es_ES") == "Hola, mundo!"); 92 * assert(getdtext("") == ""); 93 * -------- 94 */ 95 string getdtext(string s, string locale = "") 96 { 97 if(locale == "") locale = defaultLocale; 98 if(locale == BASE_LOCALE) return s; 99 100 if(locale in localeMap) 101 { 102 auto map = localeMap[locale]; 103 if(s in map) 104 { 105 return map[s]; 106 } 107 } 108 109 if(locale !in fuzzyText) fuzzyText[locale] = []; 110 if(fuzzyText[locale].find(s) == []) 111 fuzzyText[locale] ~= s; 112 return s; 113 } 114 115 /// Short name for getdtext 116 alias getdtext _; 117 118 /** 119 * Setups current locale name. If empty string is passed to 120 * $(B getdtext) then default locale will be taken. 121 * 122 * Example: 123 * -------- 124 * defaultLocale = "ru_RU"; 125 * defaultLocale = BASE_LOCALE; 126 * -------- 127 */ 128 void defaultLocale(string locale) @property 129 { 130 _defaultLocale = locale; 131 } 132 133 /** 134 * Returns current locale name. If empty string is passed to 135 * $(B getdtext) then default locale will be taken. 136 */ 137 string defaultLocale() @property 138 { 139 return _defaultLocale; 140 } 141 142 /** 143 * Manuall loads localization file with $(B name). May be usefull to 144 * load localization during program execution. 145 * 146 * Example: 147 * -------- 148 * loadLocaleFile("ru_RU"); 149 * loadLocaleFile("es_ES"); 150 * -------- 151 */ 152 void loadLocaleFile(string name) 153 { 154 if(!name.endsWith(LOCALE_EXTENTION)) name ~= LOCALE_EXTENTION; 155 156 auto data = slurp!(string, string)(name, `"%s" = "%s"`); 157 auto localeName = baseName(name, LOCALE_EXTENTION); 158 if(localeName !in localeMap) localeMap[localeName] = ["":""]; 159 auto map = localeMap[localeName]; 160 foreach(pair; data) 161 { 162 map[pair[0]] = pair[1]; 163 } 164 } 165 166 private 167 { 168 import std.file; 169 import std.algorithm; 170 import std.path; 171 import std.stdio; 172 173 string _defaultLocale = BASE_LOCALE; 174 175 string[string][string] localeMap; 176 string[][string] fuzzyText; 177 } 178 179 private string getFuzzyLocaleFileName(string locale) 180 { 181 return locale~".fuzzy"; 182 } 183 184 private void saveFuzzyText() 185 { 186 foreach(locale, strs; fuzzyText) 187 { 188 try 189 { 190 auto file = new File(getFuzzyLocaleFileName(locale), "wr"); 191 scope(exit) file.close; 192 193 foreach(i,s; strs) 194 if(i++ == strs.length-1) 195 file.write('"'~s~`" = "`~s~`"`); 196 else 197 file.writeln('"'~s~`" = "`~s~`"`); 198 } 199 catch(Exception e) 200 { 201 writeln("Failed to save fuzzy text for locale ", locale); 202 } 203 } 204 } 205 206 private void loadAllLocales() 207 { 208 auto locFiles = filter!`endsWith(a.name,".lang")`(dirEntries(".",SpanMode.breadth)); 209 foreach(entry; locFiles) 210 { 211 try 212 { 213 loadLocaleFile(entry.name); 214 } 215 catch(Exception e) 216 { 217 writeln("Failed to load localization file ", entry.name, ". Reason: ", e.msg); 218 } 219 } 220 } 221 222 shared static this() 223 { 224 version(unittest) {} 225 else 226 loadAllLocales(); 227 } 228 229 shared static ~this() 230 { 231 version(unittest) {} 232 else 233 saveFuzzyText(); 234 235 } 236 237 version(unittest) 238 { 239 import std.getopt; 240 import std.typecons; 241 import std.file; 242 243 void main(string[] args) 244 { 245 string locale; 246 getopt(args, 247 "l|lang", &locale); 248 249 defaultLocale = locale; 250 } 251 } 252 253 unittest 254 { 255 loadLocaleFile("ru_RU"); 256 loadLocaleFile("es_ES"); 257 258 assert(getdtext("Hello, world!", "ru_RU") == "Привет, мир!"); 259 assert(getdtext("Hello, world!", "es_ES") == "Hola, mundo!"); 260 assert(getdtext("") == ""); 261 262 // test fuzzy 263 assert(getdtext("Hello, world!", "unknown_UNKNOWN") == "Hello, world!"); 264 saveFuzzyText(); 265 266 auto data = slurp!(string, string)(getFuzzyLocaleFileName("unknown_UNKNOWN"), `"%s" = "%s"`); 267 assert(data == [Tuple!(string, string)("Hello, world!", "Hello, world!")]); 268 269 remove(getFuzzyLocaleFileName("unknown_UNKNOWN")); 270 }