1 /+ 2 The MIT License (MIT) 3 4 Copyright (c) <2013> <Oleg Butko (deviator), Anton Akzhigitov (Akzwar)> 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy 7 of this software and associated documentation files (the "Software"), to deal 8 in the Software without restriction, including without limitation the rights 9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 copies of the Software, and to permit persons to whom the Software is 11 furnished to do so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in 14 all copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 THE SOFTWARE. 23 +/ 24 25 /++ 26 27 Simple Usage: 28 29 0. run program 30 31 0. copy and rename "translate/dir/base" to "translate/dir/lang.lt", 32 where <lang> is language to translate 33 34 0. in each line in "translate/dir/lang.lt" write translation of line, 35 for example `hello : привет` 36 37 0. profit 38 39 Example: 40 41 setTranslatePath( "<translate/dir>" ); 42 writeln( _!"hello" ); 43 Translator.setLocalization( "ru" ); 44 writeln( _!"hello" ); 45 writeln( _!"world" ); 46 47 +/ 48 49 module des.util.localization; 50 51 import std.string; 52 import std.conv; 53 import std.stdio; 54 import std.file; 55 import std.path; 56 import std.algorithm; 57 import std.exception; 58 import std.typecons; 59 60 import des.util.logsys; 61 62 /// convert key to word 63 interface WordConverter 64 { 65 /// 66 wstring opIndex( string key ); 67 } 68 69 /// 70 interface Localization : WordConverter 71 { 72 /// 73 string name() const @property; 74 75 /// 76 bool has( string key ); 77 78 /// return if opIndex can't find key 79 protected wstring notFound( string key ); 80 } 81 82 /// 83 class DictionaryLoaderException : Exception 84 { 85 /// 86 this( string msg, string file=__FILE__, size_t line=__LINE__ ) @safe pure nothrow 87 { super( msg, file, line ); } 88 } 89 90 /// localization handler 91 interface DictionaryLoader 92 { 93 /// 94 Localization[string] load(); 95 96 /// store using keys in program 97 void store( lazy string[] keys ); 98 } 99 100 /// 101 class BaseLocalization : Localization 102 { 103 protected: 104 /// 105 string dict_name; 106 107 /// 108 wstring[string] dict; 109 110 public: 111 112 /// 113 this( string dName, wstring[string] dict ) 114 { 115 dict_name = dName; 116 foreach( key, word; dict ) 117 this.dict[key] = word; 118 this.dict.rehash; 119 } 120 121 /// returns `dict_name` 122 string name() const @property { return dict_name; } 123 124 /// find in `dict` 125 bool has( string key ) { return !!( key in dict ); } 126 127 /// return `dict` element 128 wstring opIndex( string key ) 129 { return dict.get( key, notFound(key) ); } 130 131 protected: 132 133 /// return bad string 134 wstring notFound( string key ) 135 { 136 logger.error( "no translation for key '%s' in dict '%s'", key, name ); 137 return "[no_tr]"w ~ to!wstring(key); 138 } 139 } 140 141 /// load localizations from directory 142 class DirDictionaryLoader : DictionaryLoader 143 { 144 /// path to localization directory 145 string path; 146 147 /// extension of localization files 148 string ext; 149 150 /// 151 this( string path, string ext="lt" ) 152 { 153 this.path = path; 154 this.ext = ext; 155 } 156 157 /// 158 Localization[string] load() 159 { 160 baseDictType base; 161 162 try base = loadBase( path ); 163 catch( DictionaryLoaderException e ) 164 { 165 logger.error( e.msg ); 166 return (Localization[string]).init; 167 } 168 169 auto ret = loadLocalizations( path ); 170 checkLocalizations( ret, base ); 171 return ret; 172 } 173 174 /// 175 void store( lazy string[] keys ) 176 { 177 if( !path.exists ) 178 { 179 mkdirRecurse( path ); 180 logger.info( "create localization path '%s'", path ); 181 } 182 183 auto base_dict = buildNormalizedPath( path, "base" ); 184 auto f = File( base_dict, "w" ); 185 foreach( key; keys ) f.writeln( key ); 186 f.close(); 187 } 188 189 protected: 190 191 alias ubyte[string] baseDictType; 192 193 /// 194 baseDictType loadBase( string path ) 195 { 196 auto base_dict = buildNormalizedPath( path, "base" ); 197 if( !base_dict.exists ) 198 throw new DictionaryLoaderException( format( "no base list '%s'", base_dict ) ); 199 200 auto f = File( base_dict, "r" ); 201 scope(exit) f.close(); 202 203 baseDictType ret; 204 foreach( ln; f.byLine() ) 205 ret[ln.idup] = 1; 206 207 return ret; 208 } 209 210 /// 211 Localization[string] loadLocalizations( string path ) 212 { 213 auto loc_files = dirEntries( path, "*."~ext, SpanMode.shallow ); 214 215 Localization[string] ret; 216 217 foreach( lf; loc_files ) 218 { 219 auto label = getLabel( lf.name ); 220 ret[label] = loadFromFile( lf.name ); 221 } 222 223 return ret; 224 } 225 226 /// 227 string getLabel( string name ) { return baseName( name, "." ~ ext ); } 228 229 /// 230 Localization loadFromFile( string fname ) 231 { 232 auto f = File( fname ); 233 scope(exit) f.close(); 234 235 auto name = getLabel( fname ); 236 237 wstring[string] dict; 238 239 size_t i = 0; 240 foreach( ln; f.byLine() ) 241 processLine( dict, i++, fname, strip(ln.idup) ); 242 243 return new BaseLocalization( name, dict ); 244 } 245 246 /// 247 static auto splitLine( size_t no, string fname, string ln ) 248 { 249 auto bf = ln.split(":"); 250 enforce( bf.length == 2, new DictionaryLoaderException( "bad localization: " ~ ln, fname, no ) ); 251 return tuple( bf[0], to!wstring(bf[1]) ); 252 } 253 254 /// 255 static void processLine( ref wstring[string] d, size_t no, string fname, string line ) 256 { 257 if( line.length == 0 ) return; 258 auto ln = splitLine( no, fname, line ); 259 auto key = strip(ln[0]); 260 auto value = strip(ln[1]); 261 checkKeyExests( d, key, value ); 262 d[key] = value; 263 } 264 265 static void checkKeyExests( wstring[string] d, string key, wstring value ) 266 { 267 if( key !in d ) return; 268 269 throw new DictionaryLoaderException( 270 format( "key '%s' has duplicate values: '%s', '%s'", 271 key, d[key], value ) ); 272 } 273 274 void checkLocalizations( Localization[string] locs, baseDictType base ) 275 { 276 foreach( lang, loc; locs ) 277 foreach( key; base.keys ) 278 if( !loc.has(key) ) 279 logger.error( "dict '%s' has no key '%s'", lang, key ); 280 } 281 } 282 283 /// singleton class for localization 284 final class Translator 285 { 286 private: 287 static Translator self; 288 289 this(){} 290 291 DictionaryLoader dict_loader; 292 293 Localization[string] localizations; 294 Localization currentLocalization; 295 296 @property static Translator singleton() 297 { 298 if( self is null ) self = new Translator(); 299 return self; 300 } 301 302 void s_setDictionaryLoader( DictionaryLoader dl ) 303 { 304 dict_loader = dl; 305 s_reloadLocalizations(); 306 } 307 308 void s_reloadLocalizations() 309 { 310 if( dict_loader is null ) 311 { 312 logger.error( "dictionary loader not setted: no reloading" ); 313 return; 314 } 315 316 localizations = dict_loader.load(); 317 } 318 319 size_t[string] used_keys; 320 321 wstring s_opIndex(string str) 322 { 323 used_keys[str]++; 324 325 if( currentLocalization !is null ) 326 return currentLocalization[str]; 327 328 logger.info( "no current localization: use source string" ); 329 330 return to!wstring(str); 331 } 332 333 void s_setLocalization( string lang ) 334 { 335 if( lang in localizations ) 336 currentLocalization = localizations[lang]; 337 else 338 { 339 if( dict_loader is null ) 340 logger.error( "no dictionary loader -> no localization '%1$s', (copy 'base' to '%1$s')", lang ); 341 else logger.error( "no localization '%1$s', (copy 'base' to '%1$s.lt')", lang ); 342 } 343 } 344 345 string[] s_usedKeys() { return used_keys.keys; } 346 347 void s_store() 348 { 349 if( used_keys.length == 0 ) return; 350 351 if( dict_loader is null ) 352 { 353 logger.error( "dictionary loader not setted: no store" ); 354 return; 355 } 356 357 dict_loader.store( used_keys.keys ); 358 } 359 360 public: 361 362 static 363 { 364 /// 365 void setDictionaryLoader( DictionaryLoader dl ) 366 { singleton.s_setDictionaryLoader( dl ); } 367 368 /// 369 void reloadLocalizations() 370 { singleton.s_reloadLocalizations(); } 371 372 /// get traslation in current localization 373 wstring opIndex( string str ) 374 { return singleton.s_opIndex(str); } 375 376 /// 377 void setLocalization( string lang ) 378 { singleton.s_setLocalization( lang ); } 379 380 /// 381 @property string[] usedKeys() 382 { return singleton.s_usedKeys(); } 383 384 /// store used keys by DictionaryLoader 385 void store() { singleton.s_store(); } 386 } 387 388 debug static 389 { 390 private 391 { 392 struct KeyUsage { string file; size_t line; } 393 394 KeyUsage[][string] keys; 395 396 @property void useKey(string str)(string file, size_t line) 397 { 398 if( str !in keys ) keys[str] = []; 399 keys[str] ~= KeyUsage(file, line); 400 } 401 } 402 403 const(KeyUsage[][string]) getKeysUsage() { return keys; } 404 } 405 } 406 407 /++ main function for localization 408 when `debug(printlocalizationkeys)` output keys from pragma 409 +/ 410 @property wstring _(string str, string cfile=__FILE__, size_t cline=__LINE__)(string file=__FILE__, size_t line=__LINE__) 411 { 412 debug(printlocalizationkeys) 413 pragma(msg, ct_formatKey(str,cfile,cline) ); 414 debug Translator.useKey!str(file,line); 415 return Translator[str]; 416 } 417 418 private string ct_formatKey( string str, string file, size_t line ) 419 { return format( "localization key '%s' at %s:%d", str, file, line ); } 420 421 void setTranslatePath( string dir ) 422 { Translator.setDictionaryLoader( new DirDictionaryLoader( dir ) ); } 423 424 static ~this() { Translator.store(); }