1 /**
2  * Localization seam.
3  *
4  * Deft does not bundle a message-catalog parser — that would drag a dependency
5  * (and likely Phobos) into the library and force one catalog format on everyone.
6  * Instead it exposes a single pluggable hook: the application installs a
7  * `Translator` delegate with `setTranslator`, and every translatable string the
8  * framework emits is looked up through `tr`. The delegate is free to be backed by
9  * gettext, XLIFF, a plain associative array, or anything else — that choice (and
10  * any Phobos it needs) lives in the application, not here.
11  *
12  * Without a translator installed, `tr` returns its argument unchanged, so an
13  * un-localized app behaves exactly as before. The framework's own handful of
14  * strings (the standard dialog buttons) additionally fall back to the operating
15  * system's localized text, so they are translated even with no catalog at all.
16  *
17  * Install the translator once, before creating UI; reads are not synchronized.
18  */
19 module deft.i18n;
20 
21 /**
22  * Looks up a translation for `key`, returning the translated string. A delegate
23  * may throw to signal "no translation"; callers fall back to `key`.
24  */
25 alias Translator = string delegate(string key);
26 
27 private __gshared Translator g_translator;
28 
29 /// Install (or clear, with `null`) the application's translation delegate.
30 void setTranslator(Translator translator)
31 {
32 	g_translator = translator;
33 }
34 
35 /// The currently installed translation delegate, or `null`.
36 Translator translator()
37 {
38 	return g_translator;
39 }
40 
41 /**
42  * Translate `key` through the installed `Translator`.
43  *
44  * Returns the translated string, or `key` itself when no translator is installed
45  * or the translator returns null/empty or throws. `nothrow`: safe to call from
46  * anywhere in the UI, including while building controls.
47  */
48 string tr(string key) nothrow
49 {
50 	auto t = g_translator;
51 	if (t !is null)
52 	{
53 		try
54 		{
55 			auto translated = t(key);
56 			if (translated.length != 0)
57 				return translated;
58 		}
59 		catch (Exception)
60 		{
61 			// Fall through to returning the key unchanged.
62 		}
63 	}
64 	return key;
65 }
66 
67 unittest
68 {
69 	// Default: no translator installed, tr is the identity.
70 	setTranslator(null);
71 	assert(tr("Hello") == "Hello");
72 
73 	// An installed translator is consulted.
74 	setTranslator((string k) => k == "Hello" ? "Bonjour" : k);
75 	assert(tr("Hello") == "Bonjour");
76 	assert(tr("Unmapped") == "Unmapped"); // unknown key falls back to itself
77 
78 	// A translator that returns empty falls back to the key.
79 	setTranslator((string k) => "");
80 	assert(tr("Keep") == "Keep");
81 
82 	// A throwing translator must not propagate; it falls back to the key.
83 	setTranslator(delegate string(string k) { throw new Exception("boom"); });
84 	assert(tr("Safe") == "Safe");
85 
86 	setTranslator(null); // don't leak state into other tests
87 }