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 }