1 /**
2 This module contains UI internationalization support implementation.
3 
4 UIString struct provides string container which can be either plain unicode string or id of string resource.
5 
6 Translation strings are being stored in translation files, consisting of simple key=value pair lines:
7 ---
8 STRING_RESOURCE_ID=Translation text 1
9 ANOTHER_STRING_RESOURCE_ID=Translation text 2
10 ---
11 
12 Supports fallback to another translation file (e.g. default language).
13 
14 If string resource is not found neither in main nor fallback translation files, UNTRANSLATED: RESOURCE_ID will be returned.
15 
16 String resources must be placed in i18n subdirectory inside one or more resource directories (set using Platform.instance.resourceDirs 
17 property on application initialization).
18 
19 File names must be language code with extension .ini (e.g. en.ini, fr.ini, es.ini)
20 
21 If several files for the same language are found in (different directories) their content will be merged. It's useful to merge string resources 
22 from DLangUI framework with resources of application.
23 
24 Set interface language using Platform.instance.uiLanguage in UIAppMain during initialization of application settings:
25 ---
26 Platform.instance.uiLanguage = "en";
27 ---
28 
29 
30 Synopsis:
31 
32 ----
33 import exlib.i18n;
34 
35 // use global i18n object to get translation for string ID
36 dstring translated = i18n.get("STR_FILE_OPEN");
37 // as well, you can specify fallback value - to return if translation is not found
38 dstring translated = i18n.get("STR_FILE_OPEN", "Open..."d);
39 
40 // UIString type can hold either string resource id or dstring raw value.
41 UIString text;
42 
43 // assign resource id as string (will remove dstring value if it was here)
44 text = "ID_FILE_EXIT";
45 // or assign raw value as dstring (will remove id if it was here)
46 text = "some text"d;
47 // assign both resource id and fallback value - to use if string resource is not found
48 text = UIString("ID_FILE_EXIT", "Exit"d);
49 
50 // i18n.get() will automatically be invoked when getting UIString value (e.g. using alias this).
51 dstring translated = text;
52 
53 ----
54 
55 Copyright: Vadim Lopatin, 2014
56 License:   Boost License 1.0
57 Authors:   Vadim Lopatin, coolreader.org@gmail.com
58 */
59 module exlib.i18n;
60 
61 import exlib.logger;
62 import exlib.files;
63 
64 private import exlib.linestream;
65 private import std.utf;
66 private import std.algorithm;
67 
68 /** 
69    Container for UI string - either raw value or string resource ID 
70 
71    Set resource id (string) or plain unicode text (dstring) to it, and get dstring.
72 
73 */
74 struct UIString {
75     /** if not null, use it, otherwise lookup by id */
76     private dstring _value;
77     /** id to find value in translator */
78     private string _id;
79 
80     /** create string with i18n resource id */
81     this(string id) {
82         _id = id;
83     }
84     /** create string with raw value */
85     this(dstring value) {
86         _value = value;
87     }
88     /** create string with resource id and raw value as fallback for missing translations */
89     this(string id, dstring fallbackValue) {
90 		_id = id;
91         _value = fallbackValue;
92     }
93 
94 
95     /// Returns string resource id
96     @property string id() const { return _id; }
97     /// Sets string resource id
98     @property void id(string ID) {
99         _id = ID;
100         _value = null;
101     }
102     /** Get value (either raw or translated by id) */
103     @property dstring value() const { 
104         if (_id !is null) // translate ID to dstring
105             return i18n.get(_id, _value); // get from resource, use _value as fallback
106 		return _value;
107     }
108     /** Set raw value using property */
109     @property void value(dstring newValue) {
110         _value = newValue;
111     }
112     /** Assign raw value */
113     ref UIString opAssign(dstring rawValue) {
114         _value = rawValue;
115         _id = null;
116         return this;
117     }
118     /** Assign string resource id */
119     ref UIString opAssign(string ID) {
120         _id = ID;
121         _value = null;
122         return this;
123     }
124     /** Default conversion to dstring */
125     alias value this;
126 }
127 
128 /** 
129     UIString item collection 
130 
131     Based on array.
132 */
133 struct UIStringCollection {
134     private UIString[] _items;
135     private int _length;
136 
137     /** Returns number of items */
138     @property int length() { return _length; }
139     /** Slice */
140     UIString[] opIndex() {
141         return _items[0 .. _length];
142     }
143     /** Slice */
144     UIString[] opSlice() {
145         return _items[0 .. _length];
146     }
147     /** Slice */
148     UIString[] opSlice(size_t start, size_t end) {
149         return _items[start .. end];
150     }
151     /** Read item by index */
152     UIString opIndex(size_t index) {
153         return _items[index];
154     }
155     /** Modify item by index */
156     UIString opIndexAssign(UIString value, size_t index) {
157         _items[index] = value;
158         return _items[index];
159     }
160     /** Return unicode string for item by index */
161     dstring get(size_t index) {
162         return _items[index].value;
163     }
164     /** Assign UIStringCollection */
165     void opAssign(ref UIStringCollection items) {
166         clear();
167         addAll(items);
168     }
169     /** Append UIStringCollection */
170     void addAll(ref UIStringCollection items) {
171         foreach (UIString item; items) {
172             add(item);
173         }
174     }
175     /** Assign array of string resource IDs */
176     void opAssign(string[] items) {
177         clear();
178         addAll(items);
179     }
180     /** Append array of string resource IDs */
181     void addAll(string[] items) {
182         foreach (string item; items) {
183             add(item);
184         }
185     }
186     /** Assign array of unicode strings */
187     void opAssign(dstring[] items) {
188         clear();
189         addAll(items);
190     }
191     /** Append array of unicode strings */
192     void addAll(dstring[] items) {
193         foreach (dstring item; items) {
194             add(item);
195         }
196     }
197     /** Remove all items */
198     void clear() {
199         _items.length = 0;
200         _length = 0;
201     }
202     /** Insert resource id item into specified position */
203     void add(string item, int index = -1) {
204         UIString s;
205         s = item;
206         add(s, index);
207     }
208     /** Insert unicode string item into specified position */
209     void add(dstring item, int index = -1) {
210         UIString s;
211         s = item;
212         add(s, index);
213     }
214     /** Insert UIString item into specified position */
215     void add(UIString item, int index = -1) {
216         if (index < 0 || index > _length)
217             index = _length;
218         if (_items.length < _length + 1) {
219             if (_items.length < 8)
220                 _items.length = 8;
221             else
222                 _items.length = _items.length * 2;
223         }
224         for (size_t i = _length; i > index; i--) {
225             _items[i] = _items[i + 1];
226         }
227         _items[index] = item;
228         _length++;
229     }
230     /** Remove item with specified index */
231     void remove(int index) {
232         if (index < 0 || index >= _length)
233             return;
234         for (size_t i = index; i < _length - 1; i++)
235             _items[i] = _items[i + 1];
236         _length--;
237     }
238     /** Return index of first item with specified text or -1 if not found. */
239     int indexOf(dstring str) {
240         for (int i = 0; i < _length; i++) {
241             if (_items[i].value.equal(str))
242                 return i;
243         }
244         return -1;
245     }
246     /** Return index of first item with specified string resource id or -1 if not found. */
247     int indexOf(string strId) {
248         for (int i = 0; i < _length; i++) {
249             if (_items[i].id.equal(strId))
250                 return i;
251         }
252         return -1;
253     }
254     /** Return index of first item with specified string or -1 if not found. */
255     int indexOf(UIString str) {
256         if (str.id !is null)
257             return indexOf(str.id);
258         return indexOf(str.value);
259     }
260 }
261 
262 /** UI Strings internationalization translator */
263 synchronized class UIStringTranslator {
264 
265     private UIStringList _main;
266     private UIStringList _fallback;
267     private string[] _resourceDirs;
268 
269     /** Looks for i18n directory inside one of passed dirs, and uses first found as directory to read i18n files from */
270     void findTranslationsDir(string[] dirs ...) {
271         _resourceDirs.length = 0;
272         import std.file;
273         foreach(dir; dirs) {
274             string path = appendPath(dir, "i18n/");
275             if (exists(path) && isDir(path)) {
276 				Log.i("Adding i18n dir ", path);
277                 _resourceDirs ~= path;
278             }
279         }
280     }
281 
282     /** Convert resource path - append resource dir if necessary */
283     string[] convertResourcePaths(string filename) {
284         if (filename is null)
285             return null;
286         bool hasPathDelimiters = false;
287         foreach(char ch; filename)
288             if (ch == '/' || ch == '\\')
289                 hasPathDelimiters = true;
290         string[] res;
291         if (!hasPathDelimiters && _resourceDirs.length) {
292             foreach (dir; _resourceDirs)
293                 res ~= dir ~ filename;
294         } else {
295             res ~= filename;
296         }
297         return res;
298     }
299 
300     /// create empty translator
301     this() {
302         _main = new shared UIStringList();
303         _fallback = new shared UIStringList();
304     }
305 
306     /** Load translation file(s) */
307     bool load(string mainFilename, string fallbackFilename = null) {
308         _main.clear();
309         _fallback.clear();
310         bool res = _main.load(convertResourcePaths(mainFilename));
311         if (fallbackFilename !is null) {
312             res = _fallback.load(convertResourcePaths(fallbackFilename)) || res;
313         }
314         return res;
315     }
316 
317     /** Translate string ID to string (returns "UNTRANSLATED: id" for missing values) */
318     dstring get(string id, dstring fallbackValue = null) {
319         if (id is null)
320             return null;
321         dstring s = _main.get(id);
322         if (s !is null)
323             return s;
324         s = _fallback.get(id);
325         if (s !is null)
326             return s;
327 		if (fallbackValue.length > 0)
328 			return fallbackValue;
329         return "UNTRANSLATED: "d ~ toUTF32(id);
330     }
331 }
332 
333 /** UI string translator */
334 private shared class UIStringList {
335     private dstring[string] _map;
336     /// remove all items
337     void clear() {
338         _map.destroy();
339     }
340     /// set item value
341     void set(string id, dstring value) {
342         _map[id] = value;
343     }
344     /// get item value, null if translation is not found for id
345     dstring get(string id) const {
346         if (id in _map)
347             return _map[id];
348         return null;
349     }
350     /// load strings from stream
351     bool load(std.stream.InputStream stream) {
352         dlangui.core.linestream.LineStream lines = dlangui.core.linestream.LineStream.create(stream, "");
353         int count = 0;
354         for (;;) {
355             dchar[] s = lines.readLine();
356             if (s is null)
357                 break;
358             int eqpos = -1;
359             int firstNonspace = -1;
360             int lastNonspace = -1;
361             for (int i = 0; i < s.length; i++)
362                 if (s[i] == '=') {
363                     eqpos = i;
364                     break;
365                 } else if (s[i] != ' ' && s[i] != '\t') {
366                     if (firstNonspace == -1)
367                         firstNonspace = i;
368                     lastNonspace = i;
369                 }
370             if (eqpos > 0 && firstNonspace != -1) {
371                 string id = toUTF8(s[firstNonspace .. lastNonspace + 1]);
372                 dstring value = s[eqpos + 1 .. $].dup;
373                 set(id, value);
374                 count++;
375             }
376         }
377         return count > 0;
378     }
379 
380     /// load strings from file (utf8, id=value lines)
381     bool load(string[] filenames) {
382         clear();
383         bool res = false;
384         foreach(filename; filenames) {
385             import std.stream;
386             import std.file;
387             try {
388                 debug Log.d("Loading string resources from file ", filename);
389                 if (!exists(filename) || !isFile(filename)) {
390                     Log.e("File does not exist: ", filename);
391                     continue;
392                 }
393 	            std.stream.File f = new std.stream.File(filename);
394                 scope(exit) { f.close(); }
395                 res = load(f) || res;
396             } catch (StreamFileException e) {
397                 Log.e("Cannot read string resources from file ", filename);
398             }
399         }
400         return res;
401     }
402 }
403 
404 /** Global UI translator object */
405 shared UIStringTranslator i18n;
406 shared static this() {
407     i18n = new shared UIStringTranslator();
408 }