1 // Copyright 2013 Gushcha Anton
2 /*
3 Boost Software License - Version 1.0 - August 17th, 2003
4 
5 Permission is hereby granted, free of charge, to any person or organization
6 obtaining a copy of the software and accompanying documentation covered by
7 this license (the "Software") to use, reproduce, display, distribute,
8 execute, and transmit the Software, and to prepare derivative works of the
9 Software, and to permit third-parties to whom the Software is furnished to
10 do so, all subject to the following:
11 
12 The copyright notices in the Software and this entire statement, including
13 the above license grant, this restriction and the following disclaimer,
14 must be included in all copies of the Software, in whole or in part, and
15 all derivative works of the Software, unless such copies or derivative
16 works are solely in the form of machine-executable object code generated by
17 a source language processor.
18 
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
22 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
23 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
24 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 DEALINGS IN THE SOFTWARE.
26 */
27 // Written in D programing language
28 /**
29 *   Module provides functions to handle localization. Translated string
30 *   is searched in special files with .lang extention by exact matching
31 *   with original string.
32 *
33 *   Lang files are loaded in memory at program start up from current 
34 *   directory. Additional localizations can be loaded with $(B loadLocaleFile) 
35 *   function.
36 *
37 *   Example:
38 *   --------
39 *    import std.stdio, std.opt, dtext;
40 *
41 *    void main(string[] args) 
42 *    {
43 *        string locale;
44 *        getopt(args,
45 *            "l|lang", &locale);
46 *
47 *        defaultLocale = locale;
48 *
49 *        writeln(_("Hello, world!")); \\ or use getdtext instead _
50 *    }
51 *   --------
52 *
53 *   If text for translation cannot be found in specified locale name, the text will
54 *   be saved and written down to a special fuzzy texts file at program shutdown. That
55 *   should help to add new localization fast and without program recompilation.
56 *
57 *   TODO:
58 *   <ul>
59 *   <li>Load localization files on demand;</li>
60 *   <li>Add ability to unload unused locales.</li>
61 *   </ul>
62 */
63 module dtext;
64 
65 /**
66 *   Special locale that doesn't have own locale file. System won't
67 *   create additional information (fuzzy file or locale map).
68 */
69 enum BASE_LOCALE = "en_US";
70 
71 /**
72 *   File extention which will be searched in current directory to load locale information.
73 */
74 enum LOCALE_EXTENTION = ".lang";
75 
76 /**
77 *   Returns translated string $(B s) for specified $(B locale). If locale is empty default
78 *   locale will be taken. If locale name is equal to base locale $(B s) string is returned 
79 *   without modification.
80 *
81 *   Localization strings are taken from special files previosly loaded into memory.
82 *
83 *   If string $(B s) isn't persists in locale strings it will be put into fuzzy text map.
84 *   Fuzzy strings is saved in separate file for each locale to be translated later.
85 *
86 *   See_Also: BASE_LOCALE, defaultLocale properties.
87 *
88 *   Example:
89 *   --------
90 *   assert(getdtext("Hello, world!", "ru_RU") == "Привет, мир!");
91 *   assert(getdtext("Hello, world!", "es_ES") == "Hola, mundo!");
92 *   assert(getdtext("") == "");
93 *   --------
94 */
95 string getdtext(string s, string locale = "")
96 {
97     if(locale == "") locale = defaultLocale;
98     if(locale == BASE_LOCALE) return s;
99 
100     if(locale in localeMap)
101     {
102         auto map = localeMap[locale];
103         if(s in map)
104         {
105             return map[s];
106         }
107     } 
108 
109     if(locale !in fuzzyText) fuzzyText[locale] = [];
110     if(fuzzyText[locale].find(s) == [])
111         fuzzyText[locale] ~= s;
112     return s;
113 }
114 
115 /// Short name for getdtext
116 alias getdtext _;
117 
118 /**
119 *   Setups current locale name. If empty string is passed to
120 *   $(B getdtext) then default locale will be taken.
121 *
122 *   Example:
123 *   --------
124 *   defaultLocale = "ru_RU";
125 *   defaultLocale = BASE_LOCALE;
126 *   --------
127 */
128 void defaultLocale(string locale) @property
129 {
130     _defaultLocale = locale;
131 }
132 
133 /**
134 *   Returns current locale name. If empty string is passed to
135 *   $(B getdtext) then default locale will be taken.
136 */
137 string defaultLocale() @property
138 {
139     return _defaultLocale;
140 }
141 
142 /**
143 *   Manuall loads localization file with $(B name). May be usefull to
144 *   load localization during program execution. 
145 *
146 *   Example:
147 *   --------
148 *   loadLocaleFile("ru_RU");
149 *   loadLocaleFile("es_ES");
150 *   --------
151 */
152 void loadLocaleFile(string name)
153 {
154     if(!name.endsWith(LOCALE_EXTENTION)) name ~= LOCALE_EXTENTION;
155 
156     auto data = slurp!(string, string)(name, `"%s" = "%s"`);
157     auto localeName = baseName(name, LOCALE_EXTENTION);
158     if(localeName !in localeMap) localeMap[localeName] = ["":""];
159     auto map = localeMap[localeName];
160     foreach(pair; data)
161     {
162         map[pair[0]] = pair[1];
163     }
164 }
165 
166 private
167 {
168     import std.file;
169     import std.algorithm;
170     import std.path;
171     import std.stdio;
172     
173     string _defaultLocale = BASE_LOCALE;
174 
175     string[string][string] localeMap;
176     string[][string] fuzzyText;
177 }
178 
179 private string getFuzzyLocaleFileName(string locale)
180 {
181     return locale~".fuzzy";
182 }
183 
184 private void saveFuzzyText()
185 {
186     foreach(locale, strs; fuzzyText)
187     {
188         try
189         {
190             auto file = new File(getFuzzyLocaleFileName(locale), "wr");
191             scope(exit) file.close;
192 
193             foreach(i,s; strs)
194                 if(i++ == strs.length-1)
195                     file.write('"'~s~`" = "`~s~`"`);
196                 else
197                     file.writeln('"'~s~`" = "`~s~`"`);
198         }
199         catch(Exception e)
200         {
201             writeln("Failed to save fuzzy text for locale ", locale);
202         }
203     }
204 }
205 
206 private void loadAllLocales()
207 {
208     auto locFiles = filter!`endsWith(a.name,".lang")`(dirEntries(".",SpanMode.breadth));
209     foreach(entry; locFiles)
210     {
211         try
212         {
213             loadLocaleFile(entry.name);
214         } 
215         catch(Exception e)
216         {
217             writeln("Failed to load localization file ", entry.name, ". Reason: ", e.msg);
218         }
219     }
220 }
221 
222 shared static this() 
223 {
224     version(unittest) {}
225     else
226         loadAllLocales();
227 }
228 
229 shared static ~this()
230 {
231     version(unittest) {}
232     else
233         saveFuzzyText();
234 
235 }
236 
237 version(unittest)
238 {
239     import std.getopt;
240     import std.typecons;
241     import std.file;
242 
243     void main(string[] args) 
244     {
245         string locale;
246         getopt(args,
247             "l|lang", &locale);
248 
249         defaultLocale = locale;
250     }
251 }
252 
253 unittest
254 {
255     loadLocaleFile("ru_RU");
256     loadLocaleFile("es_ES");
257 
258     assert(getdtext("Hello, world!", "ru_RU") == "Привет, мир!");
259     assert(getdtext("Hello, world!", "es_ES") == "Hola, mundo!");
260     assert(getdtext("") == "");
261 
262     // test fuzzy
263     assert(getdtext("Hello, world!", "unknown_UNKNOWN") == "Hello, world!");
264     saveFuzzyText();
265 
266     auto data = slurp!(string, string)(getFuzzyLocaleFileName("unknown_UNKNOWN"), `"%s" = "%s"`);
267     assert(data == [Tuple!(string, string)("Hello, world!", "Hello, world!")]);
268 
269     remove(getFuzzyLocaleFileName("unknown_UNKNOWN"));
270 }