1 // Written in the D programming language.
2 
3 /**
4 This module contains UI internationalization support implementation.
5 
6 UIString struct provides string container which can be either plain unicode string or id of string resource.
7 
8 Translation strings are being stored in translation files, consisting of simple key=value pair lines:
9 ---
10 STRING_RESOURCE_ID=Translation text 1
11 ANOTHER_STRING_RESOURCE_ID=Translation text 2
12 ---
13 
14 Supports fallback to another translation file (e.g. default language).
15 
16 If string resource is not found neither in main nor fallback translation files, UNTRANSLATED: RESOURCE_ID will be returned.
17 
18 String resources must be placed in i18n subdirectory inside one or more resource directories (set using Platform.instance.resourceDirs
19 property on application initialization).
20 
21 File names must be language code with extension .ini (e.g. en.ini, fr.ini, es.ini)
22 
23 If several files for the same language are found in (different directories) their content will be merged. It's useful to merge string resources
24 from DLangUI framework with resources of application.
25 
26 Set interface language using Platform.instance.uiLanguage in UIAppMain during initialization of application settings:
27 ---
28 Platform.instance.uiLanguage = "en";
29 
30 /// create by id - string STR_MENU_HELP="Help" must be added to translation resources
31 UIString help1 = UIString.fromId("STR_MENU_HELP");
32 /// create by id and fallback string
33 UIString help2 = UIString.fromId("STR_MENU_HELP", "Help"d);
34 /// create from raw string
35 UIString help3 = UIString.fromRaw("Help"d);
36 
37 ---
38 
39 
40 Synopsis:
41 
42 ----
43 import dlangui.core.i18n;
44 
45 // use global i18n object to get translation for string ID
46 dstring translated = i18n.get("STR_FILE_OPEN");
47 // as well, you can specify fallback value - to return if translation is not found
48 dstring translated = i18n.get("STR_FILE_OPEN", "Open..."d);
49 
50 // UIString type can hold either string resource id or dstring raw value.
51 UIString text;
52 
53 // assign resource id as string (will remove dstring value if it was here)
54 text = "ID_FILE_EXIT";
55 // or assign raw value as dstring (will remove id if it was here)
56 text = "some text"d;
57 // assign both resource id and fallback value - to use if string resource is not found
58 text = UIString("ID_FILE_EXIT", "Exit"d);
59 
60 // i18n.get() will automatically be invoked when getting UIString value (e.g. using alias this).
61 dstring translated = text;
62 
63 ----
64 
65 Copyright: Vadim Lopatin, 2014
66 License:   Boost License 1.0
67 Authors:   Vadim Lopatin, coolreader.org@gmail.com
68 */
69 module dlangui.core.i18n;
70 
71 import dlangui.core.types;
72 import dlangui.core.logger;
73 import dlangui.core.files;
74 import dlangui.graphics.resources;
75 private import dlangui.core.linestream;
76 private import std.utf : toUTF32;
77 private import std.algorithm;
78 private import std.string;
79 private import std.file;
80 
81 /**
82    Container for UI string - either raw value or string resource ID
83 
84    Set resource id (string) or plain unicode text (dstring) to it, and get dstring.
85 
86 */
87 struct UIString {
88     /** if not null, use it, otherwise lookup by id */
89     private dstring _value;
90     /** id to find value in translator */
91     private string _id;
92 
93     deprecated("use UIString.fromId() instead")
94     /** create string with i18n resource id */
95     this(string id) {
96         _id = id;
97     }
98 
99     /** create string with raw value; deprecated, use fromRaw() instead */
100     deprecated("use UIString.fromRaw() instead")
101     this(dstring value) {
102         _value = value;
103     }
104     /** create string with resource id and raw value as fallback for missing translations */
105     this(string id, dstring fallbackValue) {
106         _id = id;
107         _value = fallbackValue;
108     }
109 
110 
111     /// Returns string resource id
112     @property string id() const { return _id; }
113     /// Sets string resource id
114     @property void id(string ID) {
115         _id = ID;
116         _value = null;
117     }
118     /** Get value (either raw or translated by id) */
119     @property dstring value() const {
120         if (_id !is null) // translate ID to dstring
121             return i18n.get(_id, _value); // get from resource, use _value as fallback
122         return _value;
123     }
124     /** Set raw value using property */
125     @property void value(dstring newValue) {
126         _value = newValue;
127     }
128     /** Assign raw value */
129     ref UIString opAssign(dstring rawValue) {
130         _value = rawValue;
131         _id = null;
132         return this;
133     }
134     /** Assign string resource id */
135     ref UIString opAssign(string ID) {
136         _id = ID;
137         _value = null;
138         return this;
139     }
140 
141     /// returns true if string is empty: neither resource nor string is assigned
142     bool empty() const {
143         return _value.length == 0 && _id.length == 0;
144     }
145 
146     /// create UIString from id - will be translated; fallback value can be provided for cases if translation is not found
147     static UIString fromId(string ID, dstring fallback = null) {
148         return UIString(ID, fallback);
149     }
150 
151     /// Create UIString from raw utf32 string value - will not be translated
152     static UIString fromRaw(dstring rawValue) {
153         return UIString(null, rawValue);
154     }
155 
156     /// Create UIString from raw utf8 string value - will not be translated
157     static UIString fromRaw(string rawValue) {
158         return UIString(null, toUTF32(rawValue));
159     }
160 
161     /** Default conversion to dstring */
162     alias value this;
163 }
164 
165 /**
166     UIString item collection
167 
168     Based on array.
169 */
170 struct UIStringCollection {
171     private UIString[] _items;
172     private int _length;
173 
174     /** Returns number of items */
175     @property int length() const { return _length; }
176 
177     /** Returns true if collection is empty */
178     @property bool empty() const { return _length == 0; }
179 
180     /** Slice */
181     UIString[] opIndex() {
182         return _items[0 .. _length];
183     }
184     /** Slice */
185     UIString[] opSlice() {
186         return _items[0 .. _length];
187     }
188     /** Slice */
189     UIString[] opSlice(size_t start, size_t end) {
190         return _items[start .. end];
191     }
192     /** Read item by index */
193     UIString opIndex(size_t index) const {
194         return _items[index];
195     }
196     /** Modify item by index */
197     UIString opIndexAssign(UIString value, size_t index) {
198         _items[index] = value;
199         return _items[index];
200     }
201     /** Return unicode string for item by index */
202     dstring get(size_t index) const {
203         return _items[index].value;
204     }
205     /** Assign UIStringCollection */
206     void opAssign(ref UIStringCollection items) {
207         clear();
208         addAll(items);
209     }
210     /** Append UIStringCollection */
211     void addAll(ref UIStringCollection items) {
212         foreach (UIString item; items) {
213             add(item);
214         }
215     }
216     /** Assign array of string resource IDs */
217     void opAssign(string[] items) {
218         clear();
219         addAll(items);
220     }
221     /** Assign array of unicode strings */
222     void opAssign(dstring[] items) {
223         clear();
224         addAll(items);
225     }
226     /** Assign array of UIString */
227     void opAssign(UIString[] items) {
228         clear();
229         addAll(items);
230     }
231     /** Assign array of StringListValue */
232     void opAssign(StringListValue[] items) {
233         clear();
234         addAll(items);
235     }
236     /** Append array of unicode strings */
237     void addAll(dstring[] items) {
238         foreach (item; items) {
239             add(item);
240         }
241     }
242     /** Append array of unicode strings */
243     void addAll(string[] items) {
244         foreach (item; items) {
245             add(item);
246         }
247     }
248     /** Append array of unicode strings */
249     void addAll(UIString[] items) {
250         foreach (item; items) {
251             add(item);
252         }
253     }
254     /** Append array of unicode strings */
255     void addAll(StringListValue[] items) {
256         foreach (item; items) {
257             add(item);
258         }
259     }
260     /** Remove all items */
261     void clear() {
262         _items.length = 0;
263         _length = 0;
264     }
265     /** Insert resource id item into specified position */
266     void add(string item, int index = -1) {
267         UIString s;
268         s = item;
269         add(s, index);
270     }
271     /** Insert unicode string item into specified position */
272     void add(dstring item, int index = -1) {
273         UIString s;
274         s = item;
275         add(s, index);
276     }
277     /** Insert StringListValue.label item into specified position */
278     void add(StringListValue item, int index = -1) {
279         add(item.label, index);
280     }
281     /** Insert UIString item into specified position */
282     void add(UIString item, int index = -1) {
283         if (index < 0 || index > _length)
284             index = _length;
285         if (_items.length < _length + 1) {
286             if (_items.length < 8)
287                 _items.length = 8;
288             else
289                 _items.length = _items.length * 2;
290         }
291         for (size_t i = _length; i > index; i--) {
292             _items[i] = _items[i + 1];
293         }
294         _items[index] = item;
295         _length++;
296     }
297     /** Remove item with specified index */
298     void remove(int index) {
299         if (index < 0 || index >= _length)
300             return;
301         foreach(i; index .. _length - 1)
302             _items[i] = _items[i + 1];
303         _length--;
304     }
305     /** Return index of first item with specified text or -1 if not found. */
306     int indexOf(dstring str) const {
307         foreach(i; 0 .. _length) {
308             if (_items[i].value.equal(str))
309                 return i;
310         }
311         return -1;
312     }
313     /** Return index of first item with specified string resource id or -1 if not found. */
314     int indexOf(string strId) const {
315         foreach(i; 0 .. _length) {
316             if (_items[i].id.equal(strId))
317                 return i;
318         }
319         return -1;
320     }
321     /** Return index of first item with specified string or -1 if not found. */
322     int indexOf(UIString str) const {
323         if (str.id !is null)
324             return indexOf(str.id);
325         return indexOf(str.value);
326     }
327 }
328 
329 /// string values string list adapter - each item can have optional string or integer id, and optional icon resource id
330 struct StringListValue {
331     /// integer id for item
332     int intId;
333     /// string id for item
334     string stringId;
335     /// icon resource id
336     string iconId;
337     /// label to show for item
338     UIString label;
339 
340     this(string id, dstring name, string iconId = null) {
341         this.stringId = id;
342         this.label.value = name;
343         this.iconId = iconId;
344     }
345     this(string id, string nameResourceId, string iconId = null) {
346         this.stringId = id;
347         this.label.id = nameResourceId;
348         this.iconId = iconId;
349     }
350     this(int id, dstring name, string iconId = null) {
351         this.intId = id;
352         this.label.value = name;
353         this.iconId = iconId;
354     }
355     this(int id, string nameResourceId, string iconId = null) {
356         this.intId = id;
357         this.label.id = nameResourceId;
358         this.iconId = iconId;
359     }
360     this(dstring name, string iconId = null) {
361         this.label.value = name;
362         this.iconId = iconId;
363     }
364 }
365 
366 /** UI Strings internationalization translator */
367 class UIStringTranslator {
368 
369     private UIStringList _main;
370     private UIStringList _fallback;
371     private string[] _resourceDirs;
372 
373     /** Looks for i18n directory inside one of passed dirs, and uses first found as directory to read i18n files from */
374     void findTranslationsDir(string[] dirs ...) {
375         _resourceDirs.length = 0;
376         foreach(dir; dirs) {
377             string path = appendPath(dir, "i18n/");
378             if (exists(path) && isDir(path)) {
379                 Log.i("Adding i18n dir ", path);
380                 _resourceDirs ~= path;
381             }
382         }
383     }
384 
385     /** Convert resource path - append resource dir if necessary */
386     string[] convertResourcePaths(string filename) {
387         if (filename is null)
388             return null;
389         bool hasPathDelimiters = false;
390         foreach(char ch; filename)
391             if (ch == '/' || ch == '\\')
392                 hasPathDelimiters = true;
393         string[] res;
394         if (!hasPathDelimiters) {
395             string fn = EMBEDDED_RESOURCE_PREFIX ~ "std_" ~ filename;
396             string s = cast(string)loadResourceBytes(fn);
397             if (s)
398                 res ~= fn;
399             fn = EMBEDDED_RESOURCE_PREFIX ~ filename;
400             s = cast(string)loadResourceBytes(fn);
401             if (s)
402                 res ~= fn;
403             foreach (dir; _resourceDirs) {
404                 fn = dir ~ filename;
405                 if (exists(fn) && isFile(fn))
406                     res ~= fn;
407             }
408         } else {
409             // full path
410             res ~= filename;
411         }
412         return res;
413     }
414 
415     /// create empty translator
416     this() {
417         _main = new UIStringList();
418         _fallback = new UIStringList();
419     }
420 
421     /** Load translation file(s) */
422     bool load(string mainFilename, string fallbackFilename = null) {
423         _main.clear();
424         _fallback.clear();
425         bool res = _main.load(convertResourcePaths(mainFilename));
426         if (fallbackFilename !is null) {
427             res = _fallback.load(convertResourcePaths(fallbackFilename)) || res;
428         }
429         return res;
430     }
431 
432     /** Translate string ID to string (returns "UNTRANSLATED: id" for missing values) */
433     dstring get(string id, dstring fallbackValue = null) {
434         if (id is null)
435             return null;
436         dstring s = _main.get(id);
437         if (s !is null)
438             return s;
439         s = _fallback.get(id);
440         if (s !is null)
441             return s;
442         if (fallbackValue.length > 0)
443             return fallbackValue;
444         return "UNTRANSLATED: "d ~ toUTF32(id);
445     }
446 }
447 
448 /** UI string translator */
449 private class UIStringList {
450     private dstring[string] _map;
451     /// remove all items
452     void clear() {
453         _map.destroy();
454     }
455     /// set item value
456     void set(string id, dstring value) {
457         _map[id] = value;
458     }
459     /// get item value, null if translation is not found for id
460     dstring get(string id) const {
461         if (id in _map)
462             return _map[id];
463         return null;
464     }
465     /// load strings from stream
466     bool load(dstring[] lines) {
467         int count = 0;
468         foreach (s; lines) {
469             int eqpos = -1;
470             int firstNonspace = -1;
471             int lastNonspace = -1;
472             for (int i = 0; i < s.length; i++)
473                 if (s[i] == '=') {
474                     eqpos = i;
475                     break;
476                 } else if (s[i] != ' ' && s[i] != '\t') {
477                     if (firstNonspace == -1)
478                         firstNonspace = i;
479                     lastNonspace = i;
480                 }
481             if (eqpos > 0 && firstNonspace != -1) {
482                 string id = toUTF8(s[firstNonspace .. lastNonspace + 1]);
483                 dstring value = s[eqpos + 1 .. $].dup;
484                 set(id, value);
485                 count++;
486             }
487         }
488         return count > 0;
489     }
490 
491     /// convert to utf32 and split by lines (detecting line endings)
492     static dstring[] splitLines(string src) {
493         dstring dsrc = toUTF32(src);
494         dstring[] split1 = split(dsrc, "\r\n");
495         dstring[] split2 = split(dsrc, "\r");
496         dstring[] split3 = split(dsrc, "\n");
497         if (split1.length >= split2.length && split1.length >= split3.length)
498             return split1;
499         if (split2.length > split3.length)
500             return split2;
501         return split3;
502     }
503 
504     /// load strings from file (utf8, id=value lines)
505     bool load(string[] filenames) {
506         clear();
507         bool res = false;
508         foreach(filename; filenames) {
509             try {
510                 debug Log.d("Loading string resources from file ", filename);
511                 string s = cast(string)loadResourceBytes(filename);
512                 if (!s) {
513                     Log.e("Cannot load i18n resource from file ", filename);
514                     continue;
515                 }
516                 res = load(splitLines(s)) || res;
517             } catch (Exception e) {
518                 Log.e("Cannot read string resources from file ", filename);
519             }
520         }
521         return res;
522     }
523 }
524 
525 //==============================================================
526 // Global Shared objects
527 
528 /** Global UI translator object */
529 private UIStringTranslator _i18n;
530 
531 @property UIStringTranslator i18n() {
532     if (!_i18n) {
533         _i18n = new UIStringTranslator();
534     }
535     return _i18n;
536 }