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