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