1 // Copyright 2013 Gushcha Anton
2 module djtext.core;
3 
4 import std.experimental.logger;
5 
6 /**
7  * Special locale that doesn't have own locale file.
8  */
9 enum BASE_LOCALE = "en_US";
10 
11 /**
12  * Language file extension
13  */
14 enum LOCALE_EXTENSION = ".json";
15 
16 private string _defaultLocale = BASE_LOCALE;
17 
18 /**
19  * Stores translations.
20  *
21  * It is an array of associative array
22  *
23  * Examples:
24  * --------------------
25  *  // key -> string, value -> string[string]
26  *  string[string] h = localeMap["it"];
27  *  assert(h["Hello"] == "Ciao")
28  * --------------------
29  */
30 private string[string][string] localeMap;
31 /**
32  * Store fuzzy translations.
33  *
34  * It is an array of string array
35  * Examples:
36  * --------------------
37  * // key -> string, value -> string[]
38  * string[] it = fuzzyText["it"];
39  * assert(it[0] == "Testo");
40  * --------------------
41  */
42 private string[][string] fuzzyText;
43 
44 /**
45  * Returns translated string $(B s) for specified $(B locale). If locale is empty default
46  * locale will be taken. If locale name is equal to base locale $(B s) string is returned
47  * without modification.
48  *
49  * Localization strings are taken from special files previosly loaded into memory.
50  *
51  * If string $(B s) isn't persists in locale strings it will be put into fuzzy text map.
52  * Fuzzy strings is saved in separate file for each locale to be translated later.
53  *
54  * See_Also: BASE_LOCALE, defaultLocale properties.
55  *
56  * Example:
57  * --------
58  * assert(getdtext("Hello, world!", "ru_RU") == "Привет, мир!");
59  * assert(getdtext("Hello, world!", "es_ES") == "Hola, mundo!");
60  * assert(getdtext("") == "");
61  * --------
62  */
63 string getdtext(string s, string locale = "") {
64    import std.algorithm : canFind;
65 
66    if (locale == "") {
67       locale = defaultLocale;
68    }
69    if (locale == BASE_LOCALE) {
70       return s;
71    }
72 
73    if (locale in localeMap) {
74       auto map = localeMap[locale];
75       if (s in map) {
76          return map[s];
77       }
78    }
79    if (locale !in fuzzyText) {
80       fuzzyText[locale] = [];
81    }
82    if (!fuzzyText[locale].canFind(s)) {
83       fuzzyText[locale] ~= s;
84    }
85    return s;
86 }
87 
88 unittest {
89    getdtext("Hola", "es");
90    assert("es" in fuzzyText);
91    assert(is(typeof(fuzzyText["en"]) == string[]));
92    assert(fuzzyText["es"].length == 1);
93    string[] esArray = fuzzyText["es"];
94    assert(esArray[0] == "Hola");
95 }
96 
97 /// Short name for getdtext
98 alias _ = getdtext;
99 
100 /**
101  * Setups current locale name. If empty string is passed to
102  * $(B getdtext) then default locale will be taken.
103  *
104  * Example:
105  * --------
106  * defaultLocale = "ru_RU";
107  * defaultLocale = BASE_LOCALE;
108  * --------
109  */
110 void defaultLocale(string locale) {
111    _defaultLocale = locale;
112 }
113 
114 /**
115  * Returns current locale name. If empty string is passed to
116  * $(B getdtext) then default locale will be taken.
117  */
118 string defaultLocale() {
119    return _defaultLocale;
120 }
121 
122 /**
123  * Manuall loads localization file with $(B name).
124  *
125  * Example:
126  * --------
127  * loadLocaleFile("ru_RU");
128  * loadLocaleFile("es_ES");
129  * --------
130  */
131 void loadLocaleFile(string name) {
132    import std.path : baseName;
133    import std.file : readText;
134    import std..string : endsWith;
135    import std.json : parseJSON, JSONValue;
136 
137    if (!name.endsWith(LOCALE_EXTENSION)) {
138       name ~= LOCALE_EXTENSION;
139    }
140 
141    auto localeName = baseName(name, LOCALE_EXTENSION);
142    if (localeName !in localeMap) {
143       localeMap[localeName] = ["" : ""];
144    }
145    auto map = localeMap[localeName];
146 
147    string jsonString = readText(name);
148    JSONValue json = parseJSON(jsonString);
149 
150    foreach (string k, v; json) {
151       map[k] = v.str;
152    }
153 }
154 
155 void saveFuzzyText() {
156    import std.stdio : File, writeln;
157 
158    foreach (locale, strs; fuzzyText) {
159       try {
160          auto file = new File(getFuzzyLocaleFileName(locale), "wr");
161          scope (exit) {
162             file.close;
163          }
164          bool firstRow = true;
165          foreach (i, s; strs) {
166             if (firstRow) {
167                firstRow = false;
168             } else {
169                file.writeln(",");
170             }
171             file.write(`    "` ~ s ~ `" : "~` ~ s ~ `~"`);
172          }
173       } catch (Exception e) {
174          errorf("Failed to save fuzzy text for locale %s", locale);
175       }
176    }
177 }
178 
179 private string getFuzzyLocaleFileName(string locale) {
180    return locale ~ ".fuzzy";
181 }
182 
183 unittest {
184    loadAllLocales("./locale");
185    defaultLocale = "es";
186 
187    _("Hello, world!");
188    _("Hello, json!");
189    _("Hello, json!", "ru");
190    _("Hello, json!", "it");
191    _("Hello, dj!", "it");
192    saveFuzzyText();
193 }
194 
195 /**
196  * Loads all localization files in `dir`
197  *
198  * Params:
199  *  dir = The directory to iterate over.
200  */
201 void loadAllLocales(string dir) {
202    import std.algorithm : filter, each;
203    import std..string : endsWith;
204    import std.file : dirEntries, SpanMode;
205 
206    dirEntries(dir, SpanMode.shallow).filter!(f => f.name.endsWith(".json"))
207       .each!(f => loadLocaleFile(f.name));
208 }
209 
210 unittest {
211    loadAllLocales("./locale");
212    defaultLocale = "ru";
213    assert(_("Hello, world!") == "Привет, мир!");
214    assert(_("Hello, world!", "es") == "Hola, mundo!");
215    assert(getdtext("") == "");
216    assert(getdtext("cul") == "cul");
217 }
218 
219 unittest {
220    class Test {
221       string getHello() {
222          return _("Hello");
223       }
224    }
225 
226    //this setting also takes effect in the test module
227    defaultLocale = "it";
228    loadAllLocales("./locale");
229    assert(_("Hello") == "Ciao");
230 
231    auto x = new Test();
232    assert(x.getHello() == "Ciao");
233 }
234 
235 unittest {
236    loadLocaleFile("./locale/dup.json");
237    defaultLocale = "dup";
238    assert(_("Hello") == "Second");
239 }