mirror of https://github.com/buggins/dlangide.git
409 lines
12 KiB
D
409 lines
12 KiB
D
/**
|
|
This module contains UI internationalization support implementation.
|
|
|
|
UIString struct provides string container which can be either plain unicode string or id of string resource.
|
|
|
|
Translation strings are being stored in translation files, consisting of simple key=value pair lines:
|
|
---
|
|
STRING_RESOURCE_ID=Translation text 1
|
|
ANOTHER_STRING_RESOURCE_ID=Translation text 2
|
|
---
|
|
|
|
Supports fallback to another translation file (e.g. default language).
|
|
|
|
If string resource is not found neither in main nor fallback translation files, UNTRANSLATED: RESOURCE_ID will be returned.
|
|
|
|
String resources must be placed in i18n subdirectory inside one or more resource directories (set using Platform.instance.resourceDirs
|
|
property on application initialization).
|
|
|
|
File names must be language code with extension .ini (e.g. en.ini, fr.ini, es.ini)
|
|
|
|
If several files for the same language are found in (different directories) their content will be merged. It's useful to merge string resources
|
|
from DLangUI framework with resources of application.
|
|
|
|
Set interface language using Platform.instance.uiLanguage in UIAppMain during initialization of application settings:
|
|
---
|
|
Platform.instance.uiLanguage = "en";
|
|
---
|
|
|
|
|
|
Synopsis:
|
|
|
|
----
|
|
import exlib.i18n;
|
|
|
|
// use global i18n object to get translation for string ID
|
|
dstring translated = i18n.get("STR_FILE_OPEN");
|
|
// as well, you can specify fallback value - to return if translation is not found
|
|
dstring translated = i18n.get("STR_FILE_OPEN", "Open..."d);
|
|
|
|
// UIString type can hold either string resource id or dstring raw value.
|
|
UIString text;
|
|
|
|
// assign resource id as string (will remove dstring value if it was here)
|
|
text = "ID_FILE_EXIT";
|
|
// or assign raw value as dstring (will remove id if it was here)
|
|
text = "some text"d;
|
|
// assign both resource id and fallback value - to use if string resource is not found
|
|
text = UIString("ID_FILE_EXIT", "Exit"d);
|
|
|
|
// i18n.get() will automatically be invoked when getting UIString value (e.g. using alias this).
|
|
dstring translated = text;
|
|
|
|
----
|
|
|
|
Copyright: Vadim Lopatin, 2014
|
|
License: Boost License 1.0
|
|
Authors: Vadim Lopatin, coolreader.org@gmail.com
|
|
*/
|
|
module exlib.i18n;
|
|
|
|
import exlib.logger;
|
|
import exlib.files;
|
|
|
|
private import exlib.linestream;
|
|
private import std.utf;
|
|
private import std.algorithm;
|
|
|
|
/**
|
|
Container for UI string - either raw value or string resource ID
|
|
|
|
Set resource id (string) or plain unicode text (dstring) to it, and get dstring.
|
|
|
|
*/
|
|
struct UIString {
|
|
/** if not null, use it, otherwise lookup by id */
|
|
private dstring _value;
|
|
/** id to find value in translator */
|
|
private string _id;
|
|
|
|
/** create string with i18n resource id */
|
|
this(string id) {
|
|
_id = id;
|
|
}
|
|
/** create string with raw value */
|
|
this(dstring value) {
|
|
_value = value;
|
|
}
|
|
/** create string with resource id and raw value as fallback for missing translations */
|
|
this(string id, dstring fallbackValue) {
|
|
_id = id;
|
|
_value = fallbackValue;
|
|
}
|
|
|
|
|
|
/// Returns string resource id
|
|
@property string id() const { return _id; }
|
|
/// Sets string resource id
|
|
@property void id(string ID) {
|
|
_id = ID;
|
|
_value = null;
|
|
}
|
|
/** Get value (either raw or translated by id) */
|
|
@property dstring value() const {
|
|
if (_id !is null) // translate ID to dstring
|
|
return i18n.get(_id, _value); // get from resource, use _value as fallback
|
|
return _value;
|
|
}
|
|
/** Set raw value using property */
|
|
@property void value(dstring newValue) {
|
|
_value = newValue;
|
|
}
|
|
/** Assign raw value */
|
|
ref UIString opAssign(dstring rawValue) {
|
|
_value = rawValue;
|
|
_id = null;
|
|
return this;
|
|
}
|
|
/** Assign string resource id */
|
|
ref UIString opAssign(string ID) {
|
|
_id = ID;
|
|
_value = null;
|
|
return this;
|
|
}
|
|
/** Default conversion to dstring */
|
|
alias value this;
|
|
}
|
|
|
|
/**
|
|
UIString item collection
|
|
|
|
Based on array.
|
|
*/
|
|
struct UIStringCollection {
|
|
private UIString[] _items;
|
|
private int _length;
|
|
|
|
/** Returns number of items */
|
|
@property int length() { return _length; }
|
|
/** Slice */
|
|
UIString[] opIndex() {
|
|
return _items[0 .. _length];
|
|
}
|
|
/** Slice */
|
|
UIString[] opSlice() {
|
|
return _items[0 .. _length];
|
|
}
|
|
/** Slice */
|
|
UIString[] opSlice(size_t start, size_t end) {
|
|
return _items[start .. end];
|
|
}
|
|
/** Read item by index */
|
|
UIString opIndex(size_t index) {
|
|
return _items[index];
|
|
}
|
|
/** Modify item by index */
|
|
UIString opIndexAssign(UIString value, size_t index) {
|
|
_items[index] = value;
|
|
return _items[index];
|
|
}
|
|
/** Return unicode string for item by index */
|
|
dstring get(size_t index) {
|
|
return _items[index].value;
|
|
}
|
|
/** Assign UIStringCollection */
|
|
void opAssign(ref UIStringCollection items) {
|
|
clear();
|
|
addAll(items);
|
|
}
|
|
/** Append UIStringCollection */
|
|
void addAll(ref UIStringCollection items) {
|
|
foreach (UIString item; items) {
|
|
add(item);
|
|
}
|
|
}
|
|
/** Assign array of string resource IDs */
|
|
void opAssign(string[] items) {
|
|
clear();
|
|
addAll(items);
|
|
}
|
|
/** Append array of string resource IDs */
|
|
void addAll(string[] items) {
|
|
foreach (string item; items) {
|
|
add(item);
|
|
}
|
|
}
|
|
/** Assign array of unicode strings */
|
|
void opAssign(dstring[] items) {
|
|
clear();
|
|
addAll(items);
|
|
}
|
|
/** Append array of unicode strings */
|
|
void addAll(dstring[] items) {
|
|
foreach (dstring item; items) {
|
|
add(item);
|
|
}
|
|
}
|
|
/** Remove all items */
|
|
void clear() {
|
|
_items.length = 0;
|
|
_length = 0;
|
|
}
|
|
/** Insert resource id item into specified position */
|
|
void add(string item, int index = -1) {
|
|
UIString s;
|
|
s = item;
|
|
add(s, index);
|
|
}
|
|
/** Insert unicode string item into specified position */
|
|
void add(dstring item, int index = -1) {
|
|
UIString s;
|
|
s = item;
|
|
add(s, index);
|
|
}
|
|
/** Insert UIString item into specified position */
|
|
void add(UIString item, int index = -1) {
|
|
if (index < 0 || index > _length)
|
|
index = _length;
|
|
if (_items.length < _length + 1) {
|
|
if (_items.length < 8)
|
|
_items.length = 8;
|
|
else
|
|
_items.length = _items.length * 2;
|
|
}
|
|
for (size_t i = _length; i > index; i--) {
|
|
_items[i] = _items[i + 1];
|
|
}
|
|
_items[index] = item;
|
|
_length++;
|
|
}
|
|
/** Remove item with specified index */
|
|
void remove(int index) {
|
|
if (index < 0 || index >= _length)
|
|
return;
|
|
for (size_t i = index; i < _length - 1; i++)
|
|
_items[i] = _items[i + 1];
|
|
_length--;
|
|
}
|
|
/** Return index of first item with specified text or -1 if not found. */
|
|
int indexOf(dstring str) {
|
|
for (int i = 0; i < _length; i++) {
|
|
if (_items[i].value.equal(str))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
/** Return index of first item with specified string resource id or -1 if not found. */
|
|
int indexOf(string strId) {
|
|
for (int i = 0; i < _length; i++) {
|
|
if (_items[i].id.equal(strId))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
/** Return index of first item with specified string or -1 if not found. */
|
|
int indexOf(UIString str) {
|
|
if (str.id !is null)
|
|
return indexOf(str.id);
|
|
return indexOf(str.value);
|
|
}
|
|
}
|
|
|
|
/** UI Strings internationalization translator */
|
|
synchronized class UIStringTranslator {
|
|
|
|
private UIStringList _main;
|
|
private UIStringList _fallback;
|
|
private string[] _resourceDirs;
|
|
|
|
/** Looks for i18n directory inside one of passed dirs, and uses first found as directory to read i18n files from */
|
|
void findTranslationsDir(string[] dirs ...) {
|
|
_resourceDirs.length = 0;
|
|
import std.file;
|
|
foreach(dir; dirs) {
|
|
string path = appendPath(dir, "i18n/");
|
|
if (exists(path) && isDir(path)) {
|
|
Log.i("Adding i18n dir ", path);
|
|
_resourceDirs ~= path;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Convert resource path - append resource dir if necessary */
|
|
string[] convertResourcePaths(string filename) {
|
|
if (filename is null)
|
|
return null;
|
|
bool hasPathDelimiters = false;
|
|
foreach(char ch; filename)
|
|
if (ch == '/' || ch == '\\')
|
|
hasPathDelimiters = true;
|
|
string[] res;
|
|
if (!hasPathDelimiters && _resourceDirs.length) {
|
|
foreach (dir; _resourceDirs)
|
|
res ~= dir ~ filename;
|
|
} else {
|
|
res ~= filename;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/// create empty translator
|
|
this() {
|
|
_main = new shared UIStringList();
|
|
_fallback = new shared UIStringList();
|
|
}
|
|
|
|
/** Load translation file(s) */
|
|
bool load(string mainFilename, string fallbackFilename = null) {
|
|
_main.clear();
|
|
_fallback.clear();
|
|
bool res = _main.load(convertResourcePaths(mainFilename));
|
|
if (fallbackFilename !is null) {
|
|
res = _fallback.load(convertResourcePaths(fallbackFilename)) || res;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/** Translate string ID to string (returns "UNTRANSLATED: id" for missing values) */
|
|
dstring get(string id, dstring fallbackValue = null) {
|
|
if (id is null)
|
|
return null;
|
|
dstring s = _main.get(id);
|
|
if (s !is null)
|
|
return s;
|
|
s = _fallback.get(id);
|
|
if (s !is null)
|
|
return s;
|
|
if (fallbackValue.length > 0)
|
|
return fallbackValue;
|
|
return "UNTRANSLATED: "d ~ toUTF32(id);
|
|
}
|
|
}
|
|
|
|
/** UI string translator */
|
|
private shared class UIStringList {
|
|
private dstring[string] _map;
|
|
/// remove all items
|
|
void clear() {
|
|
_map.destroy();
|
|
}
|
|
/// set item value
|
|
void set(string id, dstring value) {
|
|
_map[id] = value;
|
|
}
|
|
/// get item value, null if translation is not found for id
|
|
dstring get(string id) const {
|
|
if (id in _map)
|
|
return _map[id];
|
|
return null;
|
|
}
|
|
/// load strings from stream
|
|
bool load(std.stream.InputStream stream) {
|
|
dlangui.core.linestream.LineStream lines = dlangui.core.linestream.LineStream.create(stream, "");
|
|
int count = 0;
|
|
for (;;) {
|
|
dchar[] s = lines.readLine();
|
|
if (s is null)
|
|
break;
|
|
int eqpos = -1;
|
|
int firstNonspace = -1;
|
|
int lastNonspace = -1;
|
|
for (int i = 0; i < s.length; i++)
|
|
if (s[i] == '=') {
|
|
eqpos = i;
|
|
break;
|
|
} else if (s[i] != ' ' && s[i] != '\t') {
|
|
if (firstNonspace == -1)
|
|
firstNonspace = i;
|
|
lastNonspace = i;
|
|
}
|
|
if (eqpos > 0 && firstNonspace != -1) {
|
|
string id = toUTF8(s[firstNonspace .. lastNonspace + 1]);
|
|
dstring value = s[eqpos + 1 .. $].dup;
|
|
set(id, value);
|
|
count++;
|
|
}
|
|
}
|
|
return count > 0;
|
|
}
|
|
|
|
/// load strings from file (utf8, id=value lines)
|
|
bool load(string[] filenames) {
|
|
clear();
|
|
bool res = false;
|
|
foreach(filename; filenames) {
|
|
import std.stream;
|
|
import std.file;
|
|
try {
|
|
debug Log.d("Loading string resources from file ", filename);
|
|
if (!exists(filename) || !isFile(filename)) {
|
|
Log.e("File does not exist: ", filename);
|
|
continue;
|
|
}
|
|
std.stream.File f = new std.stream.File(filename);
|
|
scope(exit) { f.close(); }
|
|
res = load(f) || res;
|
|
} catch (StreamFileException e) {
|
|
Log.e("Cannot read string resources from file ", filename);
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
}
|
|
|
|
/** Global UI translator object */
|
|
shared UIStringTranslator i18n;
|
|
shared static this() {
|
|
i18n = new shared UIStringTranslator();
|
|
}
|