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