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 }