1 module served.utils.translate;
2 
3 import std.algorithm;
4 import std.ascii;
5 import std.conv;
6 import std.experimental.logger;
7 import std.string;
8 import std.traits;
9 
10 alias Translation = string[string];
11 
12 private Translation[string] translations;
13 
14 shared static this()
15 {
16 	translations = [
17 		"en": parseTranslation!(import("en.txt")),
18 		"de": parseTranslation!(import("de.txt")),
19 		"fr": parseTranslation!(import("fr.txt")),
20 		"ja": parseTranslation!(import("ja.txt")),
21 		"ru": parseTranslation!(import("ru.txt")),
22 	];
23 }
24 
25 private Translation parseTranslation(string s)()
26 {
27 	Translation tr;
28 	foreach (line; s.splitLines)
29 		if (line.length && line[0] != '#')
30 		{
31 			auto colon = line.indexOf(':');
32 			if (colon == -1)
33 				continue;
34 			tr[line[0 .. colon].idup] = line[colon + 1 .. $].idup;
35 		}
36 	return tr;
37 }
38 
39 string currentLanguage = "en";
40 
41 string translate(string s, Args...)(Args args)
42 {
43 	string* val;
44 	if (auto lang = currentLanguage in translations)
45 		val = s in *lang;
46 
47 	if (!val)
48 		val = s in translations["en"];
49 
50 	if (!val)
51 	{
52 		warningf("No translation for string '%s' for neither english nor selected language %s!",
53 				s, currentLanguage);
54 		return s;
55 	}
56 	return formatTranslation(*val, args);
57 }
58 
59 string formatTranslation(Args...)(string text, Args formatArgs)
60 {
61 	static immutable string escapeChars = `{\`;
62 	ptrdiff_t startIndex = text.indexOfAny(escapeChars);
63 	string ret = text;
64 	while (startIndex != -1)
65 	{
66 		ptrdiff_t end = startIndex + 1;
67 		if (text[startIndex] == '{')
68 		{
69 			// {plural #<form int> {arg num} {plurals...}}
70 			if (text[startIndex + 1 .. $].startsWith("plural"))
71 			{
72 				size_t length = "{plural".length;
73 				auto args = text[startIndex + length .. $];
74 
75 				// strip space & add length
76 				auto origLength = args.length;
77 				args = args.stripLeft;
78 				length += origLength - args.length;
79 
80 				// strip '#' & add length
81 				if (!args.startsWith("#"))
82 					throw new Exception(
83 							"Malformed plural argument: expected #<form number> to come after {plural");
84 				args = args[1 .. $];
85 				length++;
86 
87 				// parse form number & space & add length
88 				origLength = args.length;
89 				auto form = args.parse!int;
90 				args = args.stripLeft;
91 				length += origLength - args.length;
92 
93 				// strip {<argument>} & add length
94 				origLength = args.length;
95 				if (!args.startsWith("{"))
96 					throw new Exception(
97 							"Malformed plural argument: expected {<argument index>} to come after {plural #form");
98 				args = args[1 .. $];
99 				args = args.stripLeft;
100 				const n = args.parse!int;
101 				args = args.stripLeft;
102 				if (!args.startsWith("}"))
103 					throw new Exception(
104 							"Malformed plural argument: expected } to come after {plural #<form> {<argument index>");
105 				args = args[1 .. $];
106 				args = args.stripLeft;
107 				length += origLength - args.length;
108 
109 				int targetIndex;
110 			ArgIndexSwitch:
111 				switch (n)
112 				{
113 					static foreach (i, arg; formatArgs)
114 					{
115 				case i:
116 						static if (isIntegral!(typeof(arg)))
117 							targetIndex = resolvePlural(form, cast(int) arg);
118 						else static if (isSomeString!(typeof(arg)))
119 							targetIndex = resolvePlural(form, arg.to!int);
120 						else
121 							assert(false, "Cannot pluralize based on value of type " ~ typeof(arg).stringof);
122 						break ArgIndexSwitch;
123 					}
124 				default:
125 					targetIndex = 0;
126 					break ArgIndexSwitch;
127 				}
128 
129 				string insert;
130 				int argIndex;
131 				while (args.startsWith("{"))
132 				{
133 					origLength = args.length;
134 					int depth = 1;
135 					end = 0;
136 					while (end != -1)
137 					{
138 						end = args.indexOfAny("{}", end + 1);
139 						if (args[end] == '}')
140 							depth--;
141 						else if (args[end] == '{')
142 							depth++;
143 
144 						if (depth == 0)
145 							break;
146 					}
147 					if (end == -1)
148 						throw new Exception("Malformed plural: argument " ~ (argIndex + 1)
149 								.to!string ~ " missing closing '}' character.");
150 					const arg = formatTranslation(args[1 .. end], formatArgs);
151 
152 					args = args[end + 1 .. $].stripLeft;
153 					if (argIndex == 0 || argIndex == targetIndex)
154 						insert = arg;
155 					argIndex++;
156 					length += origLength - args.length;
157 				}
158 
159 				if (!args.startsWith("}"))
160 					throw new Exception("Malformed plural: missing closing '}' character after all arguments");
161 				args = args[1 .. $];
162 				length++;
163 
164 				text = text[0 .. startIndex] ~ insert ~ text[startIndex + length .. $];
165 				end = startIndex + insert.length;
166 			}
167 			else // {arg num}
168 			{
169 				end = text.indexOf('}', startIndex);
170 				if (end == -1)
171 					break;
172 
173 				if (text[startIndex + 1 .. end].all!isDigit)
174 				{
175 					auto n = text[startIndex + 1 .. end].to!int;
176 					string insert;
177 				ArgSwitch:
178 					switch (n)
179 					{
180 						static foreach (i, arg; formatArgs)
181 						{
182 					case i:
183 							insert = arg.to!string;
184 							break ArgSwitch;
185 						}
186 					default:
187 						insert = null;
188 						break ArgSwitch;
189 					}
190 
191 					text = text[0 .. startIndex] ~ insert ~ text[end + 1 .. $];
192 					end = startIndex + insert.length;
193 				}
194 			}
195 		}
196 		else if (text[startIndex] == '\\')
197 		{
198 			if (end >= text.length)
199 				break;
200 			const c = text[end];
201 			switch (c)
202 			{
203 			case 't':
204 				text = text[0 .. startIndex] ~ "\t" ~ text[end + 1 .. $];
205 				break;
206 			case 'r':
207 				text = text[0 .. startIndex] ~ "\r" ~ text[end + 1 .. $];
208 				break;
209 			case 'n':
210 				text = text[0 .. startIndex] ~ "\n" ~ text[end + 1 .. $];
211 				break;
212 			case '\\':
213 			case '{':
214 			default:
215 				text = text[0 .. startIndex] ~ text[end .. $];
216 				break;
217 			}
218 			end--;
219 		}
220 		else
221 			assert(false, "don't know why did startIndex end up here");
222 
223 		startIndex = text.indexOfAny(escapeChars, end + 1);
224 	}
225 	return text;
226 }
227 
228 unittest
229 {
230 	assert(formatTranslation("{0} {1}", "hello", "world") == "hello world");
231 
232 	assert(formatTranslation("DCD is outdated. (target={0}, installed={1})",
233 			"v1.12.0", "v1.11.1") == "DCD is outdated. (target=v1.12.0, installed=v1.11.1)");
234 }
235 
236 unittest
237 {
238 	string lit1 = `\n\nthere {plural   #1  {0} {is one item}  {are {0} items}}?`;
239 	string lit2 = `\n\nthere {plural#1 {0}{is one item} {are {0} items}}?`;
240 	assert(formatTranslation(lit1, 0) == "\n\nthere are 0 items?", formatTranslation(lit1, 0));
241 	assert(formatTranslation(lit1, 1) == "\n\nthere is one item?");
242 	assert(formatTranslation(lit2, 4) == "\n\nthere are 4 items?");
243 }
244 
245 /// Implements mozilla's plural forms.
246 /// See_Also: https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals
247 /// Returns: the index which plural word to use. For each rule from top to bottom.
248 int resolvePlural(int form, int n)
249 {
250 	switch (form)
251 	{
252 		// Asian, Persian, Turkic/Altaic, Thai, Lao
253 	case 0:
254 		return 0;
255 		// Germanic, Finno-Ugric, Language isolate, Latin/Greek, Semitic, Romanic, Vietnamese
256 	case 1:
257 		return n == 1 ? 0 : 1;
258 		// Romanic, Lingala
259 	case 2:
260 		return n == 0 || n == 1 ? 0 : 1;
261 		// Baltic
262 	case 3:
263 		if (n % 10 == 0)
264 			return 0;
265 		else if (n != 11 && n % 10 == 1)
266 			return 1;
267 		else
268 			return 2;
269 		// Celtic
270 	case 4:
271 		if (n == 1 || n == 11)
272 			return 0;
273 		else if (n == 2 || n == 12)
274 			return 1;
275 		else if ((n >= 3 && n <= 10) || (n >= 13 && n <= 19))
276 			return 2;
277 		else
278 			return 3;
279 		// Romanic
280 	case 5:
281 		if (n == 1)
282 			return 0;
283 		else if ((n % 100) >= 0 && (n % 100) <= 19)
284 			return 1;
285 		else
286 			return 2;
287 		// Baltic
288 	case 6:
289 		if (n != 11 && n % 10 == 1)
290 			return 0;
291 		else if (n % 10 == 0 || (n % 100 >= 11 && n % 100 <= 19))
292 			return 1;
293 		else
294 			return 2;
295 		// Belarusian, Russian, Ukrainian
296 	case 7:
297 		// Slavic
298 	case 19:
299 		if (n != 11 && n % 10 == 1)
300 			return 0;
301 		else if (n != 12 && n != 13 && n != 14 && (n % 10 >= 2 && n % 10 <= 4))
302 			return 1;
303 		else
304 			return 2;
305 		// Slavic
306 	case 8:
307 		if (n == 1)
308 			return 0;
309 		else if (n >= 2 && n <= 4)
310 			return 1;
311 		else
312 			return 2;
313 		// Slavic
314 	case 9:
315 		if (n == 1)
316 			return 0;
317 		else if (n >= 2 && n <= 4 && !(n >= 12 && n <= 14))
318 			return 1;
319 		else
320 			return 2;
321 		// Slavic
322 	case 10:
323 		if (n % 100 == 1)
324 			return 0;
325 		else if (n % 100 == 2)
326 			return 1;
327 		else if (n % 100 == 3 || n % 100 == 4)
328 			return 2;
329 		else
330 			return 3;
331 		// Celtic
332 	case 11:
333 		if (n == 1)
334 			return 0;
335 		else if (n == 2)
336 			return 1;
337 		else if (n >= 3 && n <= 6)
338 			return 2;
339 		else if (n >= 7 && n <= 10)
340 			return 3;
341 		else
342 			return 4;
343 		// Semitic
344 	case 12:
345 		if (n == 1)
346 			return 0;
347 		else if (n == 2)
348 			return 1;
349 		else if (n == 0)
350 			return 5;
351 		else
352 		{
353 			const d = n % 100;
354 			if (d >= 0 && d <= 2)
355 				return 4;
356 			else if (d >= 3 && d <= 10)
357 				return 2;
358 			else
359 				return 3;
360 		}
361 		// Semitic
362 	case 13:
363 		if (n == 1)
364 			return 0;
365 		else
366 		{
367 			const d = n % 100;
368 			if (d >= 1 && d <= 10)
369 				return 1;
370 			else if (d >= 11 && d <= 19)
371 				return 2;
372 			else
373 				return 3;
374 		}
375 		// unused
376 	case 14:
377 		if (n % 10 == 1)
378 			return 0;
379 		else if (n % 10 == 2)
380 			return 1;
381 		else
382 			return 2;
383 		// Icelandic, Macedonian
384 	case 15:
385 		if (n != 11 && n % 10 == 1)
386 			return 0;
387 		else
388 			return 1;
389 		// Celtic
390 	case 16:
391 		const a = n % 10;
392 		const b = n % 100;
393 		if (a == 1 && b != 11 && b != 71 && b != 91)
394 			return 0;
395 		else if (a == 2 && b != 12 && b != 72 && b != 92)
396 			return 1;
397 		else if (a.among!(3, 4, 9) && !b.among!(13, 14, 19, 73, 74, 79, 93, 94, 99))
398 			return 2;
399 		else if (n % 1_000_000 == 0)
400 			return 3;
401 		else
402 			return 4;
403 		// Ecuador indigenous languages
404 	case 17:
405 		return (n == 0) ? 0 : 1;
406 		// Welsh
407 	case 18:
408 		switch (n)
409 		{
410 		case 0:
411 			return 0;
412 		case 1:
413 			return 1;
414 		case 2:
415 			return 2;
416 		case 3:
417 			return 3;
418 		case 6:
419 			return 4;
420 		default:
421 			return 5;
422 		}
423 	default:
424 		throw new Exception("Unknown plural form");
425 	}
426 }