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(); }