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 }