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