1 /** 2 Internationalization compatible with $(LINK2 https://www.gnu.org/software/gettext/, GNU gettext). 3 4 Insert the following line at the top of your main function: 5 --- 6 mixin(gettext.main); 7 --- 8 9 Translatable strings are marked by instantiating the `tr` template, like so: 10 --- 11 writeln(tr!"Translatable message"); 12 --- 13 14 A translation may require a particular plural form depending on a number. This can be 15 achieved by supplying both singular and plural forms as compile time arguments, and the 16 number as a runtime argument. 17 --- 18 writeln(tr!("one green bottle hanging on the wall", 19 "%d green bottles hanging on the wall")(n)); 20 --- 21 22 Plural forms can be used in format strings, but the argument that determines the form 23 must be supplied to `tr` and not to `format`. The corresponding format specifier will 24 not be seen by `format` as it will have been replaced with a string by `tr`: 25 --- 26 format(tr!("Welcome %s, you may make a wish", 27 "Welcome %s, you may make %d wishes")(n), name); 28 --- 29 The format specifier that selects the form is the last specifier in the format string 30 (here `%d`). In many sentences, however, the specifier that should select the form cannot 31 be the last. In these cases, format specifiers must be given a position argument, where 32 the highest position determines the form: 33 --- 34 foreach (i, where; [tr!"hand", tr!"bush"]) 35 format(tr!("One bird in the %1$s", "%2$d birds in the %1$s")(i + 1), where); 36 --- 37 Again, the specifier with the highest position argument will never be seen by format. 38 39 Two identical strings that have different meanings dependent on context may need to be 40 translated differently. Dis can be accomplished by disambiguating the string with a 41 context argument. It is also possible to attach a comment that will be seen by 42 the translator: 43 --- 44 auto message1 = tr!("Review the draft.", [Tr.context: "document"]); 45 auto message2 = tr!("Review the draft.", [Tr.context: "nautical", 46 Tr.note: `Nautical term! "Draft" = how deep the bottom` ~ 47 `of the ship is below the water level.`]); 48 --- 49 50 If you'd rather use an underscore to mark translatable strings, 51 [as the GNU gettext documentation suggests](https://www.gnu.org/software/gettext/manual/html_node/Mark-Keywords.html), 52 you can use an alias: 53 --- 54 import gettext : _ = tr; // Customary in GNU software. 55 writeln(_!"Translatable message"); 56 --- 57 58 Authors: 59 $(LINK2 https://github.com/veelo, Bastiaan Veelo) 60 Copyright: 61 SARC B.V., 2022 62 License: 63 $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 64 */ 65 66 module gettext; 67 68 /// Optional attribute categories. 69 enum Tr { 70 note, /// Pass a note to the translator. 71 context /// Disambiguate by giving a context. 72 } 73 74 version (xgettext) // String extraction mode. 75 { 76 /** $(NEVER_DOCUMENT) */ 77 bool scan(string mainFullPath = __FILE_FULL_PATH__, string mainFile = __FILE__) 78 { 79 import std.getopt; 80 import std.path : baseName, buildPath, pathSplitter, setExtension; 81 import core.runtime : Runtime; 82 83 auto args = Runtime.args; 84 potFile = buildPath("po", args[0].baseName); 85 86 mainPathPrefix = mainFullPath[0 .. $ - mainFile.length].pathSplitter.join("/") ~ "/"; 87 88 auto helpInformation = getopt(args, 89 "output|o", "Set the path and file name for the PO template file.", &potFile, 90 "width|w", "Set the output page width (default 80).", &pageWidth); 91 if (helpInformation.helpWanted) 92 { 93 ()@trusted{ 94 defaultGetoptPrinter("Usage:\n\tdub run --config=xgettext [-- <options>]\nOptions:", helpInformation.options); 95 }(); 96 } 97 else 98 writePOT(potFile.setExtension("pot")); 99 return args.length > 0; // Always true, but the compiler has no idea. 100 } 101 102 import std.typecons : Tuple; 103 import std.array : join; 104 import std.ascii : newline; 105 106 private enum Format {plain, c} 107 private alias Key = Tuple!(string, "singular", 108 string, "plural", 109 Format, "format", 110 string, "context"); 111 private string[][Key] translatableStrings; 112 private string[][Key] comments; 113 private string mainPathPrefix; 114 115 private string potFile; 116 private size_t pageWidth = 80; 117 118 private void writePOT(string potFile) @safe 119 { 120 import std.algorithm : cmp, map, sort; 121 import std.file : mkdirRecurse, write; 122 import std.path : baseName, dirName; 123 import std.stdio; 124 125 string header() @safe 126 { 127 import std.exception : ifThrown; 128 import std.array : join; 129 import std.json, std.process; 130 import std.string : strip; 131 132 string rootPackage = potFile.baseName; 133 134 JSONValue json; 135 auto piped = pipeProcess(["dub", "describe"], Redirect.stdout); 136 scope (exit) piped.pid.wait; 137 json = ()@trusted{ return piped.stdout.byLine.join.parseJSON; }(); 138 rootPackage = json["rootPackage"].str.ifThrown!JSONException(potFile.baseName); 139 foreach (_package; json["packages"].arrayNoRef) 140 if (_package["name"].str == rootPackage) 141 { 142 json = _package; 143 break; 144 } 145 146 string thisYear() 147 { 148 return __DATE__[$-4 .. $]; 149 } 150 string title() 151 { 152 return "# PO Template for " ~ rootPackage ~ "."; 153 } 154 string copyright() 155 { 156 return ("# " ~ json["copyright"].str) 157 .ifThrown!JSONException("# Copyright © YEAR THE PACKAGE'S COPYRIGHT HOLDER"); 158 } 159 string license() 160 { 161 return ("# This file is distributed under the " ~ json["license"].str ~ " license.") 162 .ifThrown!JSONException("# This file is distributed under the same license as the " ~ rootPackage ~ " package."); 163 } 164 string author() 165 { 166 return (){ return json["authors"].arrayNoRef.map!(a => "# " ~ a.str ~ ", " ~ thisYear ~ ".").join(newline); }() 167 .ifThrown!JSONException("# FIRST AUTHOR <EMAIL@ADDRESS>, " ~ thisYear ~ "."); 168 } 169 string idVersion() 170 { 171 auto gitResult = execute(["git", "describe"]); 172 auto _version = gitResult.status == 0 ? gitResult.output.strip : "PACKAGE VERSION"; 173 return (`"Project-Id-Version: ` ~ _version ~ `\n"`); 174 } 175 string bugs() 176 { 177 return `"Report-Msgid-Bugs-To: \n"`; 178 } 179 string creationDate() 180 { 181 import std.datetime; 182 return `"POT-Creation-Date: ` ~ Clock.currTime.toUTC.toISOExtString() ~ `\n"`; 183 } 184 return [title, copyright, license, author, `#`, `#, fuzzy`, `msgid ""`, `msgstr ""`, 185 idVersion, bugs, creationDate, 186 `"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"`, 187 `"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"`, 188 `"Language-Team: LANGUAGE <LL@li.org>\n"`, 189 `"Language: \n"`, 190 `"MIME-Version: 1.0\n"`, 191 `"Content-Type: text/plain; charset=UTF-8\n"`, 192 `"Content-Transfer-Encoding: 8bit\n"`, 193 ``, ``].join(newline); 194 } 195 mkdirRecurse(potFile.dirName); 196 ()@trusted { 197 translatableStrings.rehash; 198 comments.rehash; 199 }(); 200 write(potFile, header ~ translatableStrings.keys 201 .sort!((a, b) => cmp(translatableStrings[a][0], translatableStrings[b][0]) < 0) 202 .map!(key => messageFromKey(key)).join(newline)); 203 writeln(potFile ~ " generated."); 204 } 205 206 private string stringify(string str) @safe pure 207 { 208 import std.conv : text; 209 210 return text([str])[1 .. $-1]; 211 } 212 213 private string wrapPrefix(string text, string prefix) @safe 214 { 215 import std.algorithm : filter, map; 216 import std.string : lineSplitter; 217 218 // Cannot use std.string.wrap, we need to preserve trailing whitespace. 219 string wrap(string text) 220 { 221 import std.algorithm : splitWhen; 222 import std.array : appender; 223 import std.uni : isWhite; 224 import std.conv : to; 225 226 bool mayBreakAfter(dchar c) 227 { 228 return (c.isWhite || c == '-' || c == '/' || c == '?'); 229 } 230 231 auto result = appender!string; 232 auto len = 0; 233 234 auto wordsWithTrailingWhitespace = text.splitWhen!((a, b) => (mayBreakAfter(a) && !mayBreakAfter(b)) || 235 b == '\\' // Copy GNU xgettext malfunction 236 ); 237 while (!wordsWithTrailingWhitespace.empty) 238 { 239 result ~= `"`; 240 string nextWord = wordsWithTrailingWhitespace.front.to!string; 241 do 242 { 243 result ~= nextWord; 244 len += nextWord.length; 245 wordsWithTrailingWhitespace.popFront; 246 if (wordsWithTrailingWhitespace.empty) 247 break; 248 nextWord = wordsWithTrailingWhitespace.front.to!string; 249 } while (len + nextWord.length < pageWidth - 2); 250 result ~= `"` ~ newline; 251 len = 0; 252 } 253 return result[]; 254 } 255 256 if (prefix.length + 1 + text.stringify.length < pageWidth) 257 return prefix ~ " " ~ text.stringify ~ newline; 258 return prefix ~ ` ""` ~ newline ~ wrap(text.stringify[1 .. $ - 1]); 259 } 260 261 private string messageFromKey(Key key) @safe 262 { 263 import std.algorithm : commonPrefix, filter, map, min; 264 import std.string : lineSplitter, wrap; 265 266 string stripPrefix(string reference) 267 { 268 static size_t charsToStrip = size_t.max; 269 if (charsToStrip == size_t.max) 270 foreach (strs; translatableStrings) 271 foreach (str; strs) 272 charsToStrip = min(charsToStrip, commonPrefix(str, mainPathPrefix).length); 273 assert(reference.length > charsToStrip); 274 return reference[charsToStrip .. $]; 275 } 276 277 string message; 278 if (auto c = key in comments) 279 foreach (comment; *c) 280 message ~= `#. ` ~ comment ~ newline; 281 message ~= translatableStrings[key].map!stripPrefix.join(" "). 282 wrap(pageWidth - "#: ".length).lineSplitter.filter!(l => l.length). 283 map!(l => "#: " ~ l ~ newline).join; 284 if (key.format == Format.c) 285 message ~= `#, c-format` ~ newline; 286 if (key.context != null) 287 message ~= key.context.wrapPrefix(`msgctxt`); 288 if (key.plural == null) 289 { 290 message ~= key.singular.wrapPrefix(`msgid`) ~ 291 `msgstr ""` ~ newline; 292 } 293 else 294 { 295 message ~= key.singular.wrapPrefix(`msgid`) ~ 296 key.plural.wrapPrefix(`msgid_plural`) ~ 297 `msgstr[0] ""` ~ newline ~ 298 `msgstr[1] ""` ~ newline; 299 } 300 return message; 301 } 302 303 /** $(NEVER_DOCUMENT) */ 304 template tr(string singular, string[Tr] attributes = null, 305 int line = __LINE__, string file = __FILE_FULL_PATH__, string mod = __MODULE__, string func = __FUNCTION__, Args...) 306 { 307 alias tr = tr!(singular, null, attributes, 308 line, file, mod, func, Args); 309 } 310 311 /** $(NEVER_DOCUMENT) */ 312 template tr(string singular, string plural, string[Tr] attributes = null, 313 int line = __LINE__, string file = __FILE_FULL_PATH__, string mod = __MODULE__, string func = __FUNCTION__, Args...) 314 { 315 static struct StrInjector 316 { 317 static this() 318 { 319 string reference() 320 { 321 import std.conv : to; 322 import std.array : join; 323 import std.algorithm : commonPrefix; 324 import std.path : pathSplitter; 325 326 static if (func.length == 0) 327 return file.pathSplitter.join("/") ~ ":" ~ line.to!string; 328 else 329 return file.pathSplitter.join("/") ~ ":" ~ line.to!string ~ "(" ~ func[commonPrefix(func, mod).length + 1 .. $] ~ ")"; 330 } 331 Format format() 332 { 333 static if (Args.length > 0 || singular.hasFormatSpecifiers || (plural && plural.hasFormatSpecifiers)) 334 return Format.c; 335 else 336 return Format.plain; 337 } 338 string context() 339 { 340 if (auto c = Tr.context in attributes) 341 return *c; 342 return null; 343 } 344 translatableStrings.require(Key(singular, plural, format, context)) ~= reference; 345 if (auto c = Tr.note in attributes) 346 comments.require(Key(singular, plural, format, context)) ~= *c; 347 } 348 } 349 static if (plural == null) 350 enum tr = TranslatableString(singular); 351 else 352 enum tr = TranslatableStringPlural(singular, plural); 353 } 354 355 private bool hasFormatSpecifiers(string fmt) pure @safe 356 { 357 import std.format : FormatSpec; 358 import std.exception : ifThrown; 359 360 static void ns(const(char)[] arr) {} // the simplest output range 361 auto nullSink = &ns; 362 return FormatSpec!char(fmt).writeUpToNextSpec(nullSink).ifThrown!FormatException(false); 363 } 364 unittest 365 { 366 assert ("On %2$s I eat %3$s and walk for %1$d hours.".hasFormatSpecifiers); 367 assert ("On %%2$s I eat %%3$s and walk for %1$d hours.".hasFormatSpecifiers); 368 assert (!"On %%2$s I eat %%3$s and walk for hours.".hasFormatSpecifiers); 369 assert (!"98%".hasFormatSpecifiers); 370 } 371 } 372 else // Translation mode. 373 { 374 /** 375 Translate `message`. 376 377 This does *not* instantiate a new function for every marked string 378 (the signature is fabricated for the sake of documentation). 379 380 Returns: The translation of `message` if one exists in the selected 381 language, or `message` otherwise. 382 See_Also: [selectLanguage] 383 384 Examples: 385 --- 386 writeln(tr!"Translatable message"); 387 --- 388 */ 389 template tr(string singular, string[Tr] attributes = null) 390 { 391 enum tr = TranslatableString(singular, attributes); 392 } 393 /** 394 Translate a message in the correct plural form. 395 396 This does *not* instantiate a new function for every marked string 397 (the signature is fabricated for the sake of documentation). 398 399 The first argument should be in singular form, the second in plural 400 form. Note that the format specifier `%d` is optional. 401 402 Returns: The translation if one exists in the selected 403 language, or the corresponding original otherwise. 404 See_Also: [selectLanguage] 405 406 Examples: 407 --- 408 writeln(tr!("There is a goose!", "There are %d geese!")(n)); 409 --- 410 */ 411 template tr(string singular, string plural, string[Tr] attributes = null) 412 { 413 enum tr = TranslatableStringPlural(singular, plural); 414 } 415 } 416 import std.format : format, FormatException, FormatSpec; 417 418 /** 419 Represents a translatable string. 420 421 This struct can for the most part be considered an implementation detail of `gettext`. 422 A template instantiation like `tr!"Greetings"` actually results in a constructor call like 423 `TranslatableString("Greetings")` in the code. This struct is callable, so that a lookup 424 of the translation happens when the struct is evaluated. 425 426 The only reason that this struct is public is to make declarations of static arrays of 427 translatable strings less cryptic: 428 429 --- 430 enum RGB {red, green, blue} 431 432 // Explicit array of translatable strings: 433 immutable TranslatableString[Color.max + 1] colors1 = [RGB.red: tr!"Red", 434 RGB.green: tr!"Green", 435 RGB.blue: tr!"Blue"]; 436 // Array of translatable strings where the type is derived: 437 immutable typeof(tr!"Red")[Color.max + 1] colors2 = [RGB.red: tr!"Red", 438 RGB.green: tr!"Green", 439 RGB.blue: tr!"Blue"]; 440 --- 441 */ 442 /* 443 This struct+template trick allows the string to be passed as template parameter without instantiating 444 a separate function for every string. https://forum.dlang.org/post/t8pqvg$20r0$1@digitalmars.com 445 */ 446 @safe struct TranslatableString 447 { 448 private enum string soh = "\x01"; 449 private enum string eot = "\x04"; 450 private immutable(string)[] seq; 451 this (string str, string[Tr] attributes = null) nothrow 452 { 453 if (auto context = Tr.context in attributes) 454 str = soh ~ *context ~ eot ~ str; 455 seq = [str]; 456 } 457 this (string[] seq) nothrow 458 { 459 this.seq = seq.idup; 460 } 461 this (immutable(string)[] seq) nothrow 462 { 463 this.seq = seq; 464 } 465 string gettext() const 466 { 467 import std.algorithm : findSplitAfter, map, startsWith; 468 import std.array : join; 469 470 string proxy(string message) 471 { 472 if (message.startsWith(soh)) 473 return currentLanguage.gettext(message[1 .. $]).findSplitAfter(eot)[1]; 474 else 475 return currentLanguage.gettext(message); 476 } 477 478 return seq.map!(a => proxy(a)).join; 479 } 480 alias gettext this; 481 /** Forces evaluation as translated string. 482 483 In a limited set of circumstances, a `TranslatableString` may forcefully need to be interpreted as a string. 484 One of these cases is a *named* enum: 485 486 --- 487 enum E {member = tr!"translation"} 488 writeln(E.member); // "member" 489 writeln(E.member.toString); // "translation" 490 --- 491 Contrary, anonimous enums and manifest constants do not require this treatment: 492 --- 493 enum {member = tr!"translation"} 494 writeln(member); // "translation" 495 --- 496 */ 497 string toString() const // Called when a tr!"" literal or constant occurs in a writeln(). 498 { 499 return gettext; 500 } 501 /// idem 502 void toString(scope void delegate(scope const(char)[]) @safe sink) const 503 { 504 sink(gettext); 505 } 506 TranslatableString opBinary(string op : "~", RHS)(RHS rhs) nothrow 507 { 508 import std.traits : Unconst; 509 510 static if (is (RHS == TranslatableString)) 511 return TranslatableString(seq ~ rhs.seq); 512 else static if (is (Unconst!RHS == TranslatableString)) 513 return TranslatableString(seq ~ rhs.seq.dup); 514 // TODO Add a reserved context so that ordinary strings don't accidentally get translated. 515 else static if (is (RHS == string)) 516 return TranslatableString(seq ~ rhs); 517 else static if (is (Unconst!RHS == char)) 518 return TranslatableString(seq ~ [rhs].idup); 519 else 520 static assert (false, "Need implementation for " ~ RHS.stringof); 521 } 522 TranslatableString opBinaryRight(string op : "~", LHS)(LHS lhs) nothrow 523 { 524 import std.traits : Unconst; 525 526 static if (is (LHS == TranslatableString)) 527 return TranslatableString(lhs.seq ~ seq); 528 static if (is (Unconst!LHS == TranslatableString)) 529 return TranslatableString(lhs.seq.dup ~ seq); 530 // TODO Add a reserved context so that ordinary strings don't accidentally get translated. 531 else static if (is (LHS == string)) 532 return TranslatableString([lhs] ~ seq); 533 else static if (is (LHS == char[])) 534 return TranslatableString([lhs.idup] ~ seq); 535 else static if (is (LHS == char)) 536 return TranslatableString([[lhs.idup]] ~ seq); 537 else 538 static assert (false, "Need implementation for " ~ LHS.stringof); 539 } 540 } 541 @safe struct TranslatableStringPlural 542 { 543 string str, strpl; 544 this(string str, string strpl) 545 { 546 this.str = str; 547 this.strpl = strpl; 548 } 549 string opCall(size_t number) const 550 { 551 import std.algorithm : max; 552 553 const n = cast (int) (number > int.max ? (number % 1000000) + 1000000 : number); 554 const trans = currentLanguage.ngettext(str, strpl, n); 555 if (countFormatSpecifiers(trans) == countFormatSpecifiers(strpl)) 556 { 557 try 558 return format(trans.disableAllButLastSpecifier, n); 559 catch(FormatException e) 560 { 561 debug throw(e); 562 return strpl; // Fall back on untranslated message. 563 } 564 } 565 else 566 return trans; 567 } 568 } 569 570 private int countFormatSpecifiers(string fmt) pure @safe 571 { 572 static void ns(const(char)[] arr) {} // the simplest output range 573 auto nullSink = &ns; 574 int count = 0; 575 auto f = FormatSpec!char(fmt); 576 while (f.writeUpToNextSpec(nullSink)) 577 count++; 578 return count; 579 } 580 unittest 581 { 582 assert ("On %2$s I eat %3$s and walk for %1$d hours.".countFormatSpecifiers == 3); 583 assert ("On %%2$s I eat %%3$s and walk for %1$d hours.".countFormatSpecifiers == 1); 584 } 585 586 private immutable(Char)[] disableAllButLastSpecifier(Char)(const Char[] inp) @safe 587 { 588 import std.array : Appender; 589 import std.conv : to; 590 import std.exception : enforce; 591 import std.typecons : tuple; 592 593 enum Mode {undefined, inSequence, outOfSequence} 594 Mode mode = Mode.undefined; 595 596 Appender!(Char[]) outp; 597 outp.reserve(inp.length + 10); 598 // Traverse specs, disable all of them, note where the highest index is. Re-enable that one. 599 size_t lastSpecIndex = 0, highestSpecIndex = 0, highestSpecPos = 0, specs = 0; 600 auto f = FormatSpec!Char(inp); 601 while (f.trailing.length > 0) 602 { 603 if (f.writeUpToNextSpec(outp)) 604 { 605 // Mode check. 606 if (mode == Mode.undefined) 607 mode = f.indexStart > 0 ? Mode.outOfSequence : Mode.inSequence; 608 else 609 enforce!FormatException( mode == Mode.inSequence && f.indexStart == 0 || 610 (mode == Mode.outOfSequence && f.indexStart != lastSpecIndex), 611 `Cannot mix specifiers with and without a position argument in "` ~ inp ~ `"`); 612 // Track the highest. 613 if (f.indexStart == 0) 614 highestSpecPos = outp[].length + 1; 615 else 616 if (f.indexStart > highestSpecIndex) 617 { 618 highestSpecIndex = f.indexStart; 619 highestSpecPos = outp[].length + 1; 620 } 621 // disable 622 auto curFmtSpec = inp[outp[].length - specs .. $ - f.trailing.length]; 623 outp ~= '%'.to!Char ~ curFmtSpec; 624 lastSpecIndex = f.indexStart; 625 specs++; 626 } 627 628 } 629 return mode == Mode.inSequence ? 630 (outp[][0 .. highestSpecPos] ~ outp[][highestSpecPos + 1 .. $]).idup : 631 (outp[][0 .. highestSpecPos] ~ outp[][highestSpecPos + highestSpecIndex.to!string.length + 2 .. $]).idup; 632 } 633 unittest 634 { 635 import std.exception; 636 637 assert ("Я считаю %d яблоко.".disableAllButLastSpecifier == 638 "Я считаю %d яблоко."); 639 assert ("Last %s, in %s, I ate %d muffins".disableAllButLastSpecifier == 640 "Last %%s, in %%s, I ate %d muffins"); 641 assert ("I ate %3$d muffins on %1$s in %2$s.".disableAllButLastSpecifier == 642 "I ate %d muffins on %%1$s in %%2$s."); 643 assertThrown("An unpositioned %d specifier mixed with positioned specifier %3$s".disableAllButLastSpecifier); 644 assertThrown("A positioned specifier %3$s mixed with unpositioned %d specifier".disableAllButLastSpecifier); 645 } 646 647 /** 648 Code to be mixed in at the top of your `main()` function. 649 650 Examples: 651 --- 652 void main() 653 { 654 import gettext; 655 mixin(gettext.main); 656 657 // Your code. 658 } 659 --- 660 */ 661 enum main = q{ 662 version (xgettext) 663 { 664 import core.stdc.stdlib : exit; 665 666 if (scan) // Prevent unreachable code warning after mixin. 667 exit(0); 668 } 669 }; 670 671 import mofile; 672 673 private MoFile currentLanguage; 674 675 /** 676 Collect a list of available `*.mo` files. 677 678 If no `moPath` is given, files are searched inside the `mo` folder assumed 679 to exist besides the file location of the running executable. 680 */ 681 string[] availableLanguages(string moPath = null) 682 { 683 import std.algorithm: map; 684 import std.array : array; 685 import std.file : exists, isDir, dirEntries, SpanMode; 686 import std.path : buildPath, dirName; 687 688 if (moPath == null) 689 { 690 import core.runtime : Runtime; 691 moPath = buildPath(Runtime.args[0].dirName, "mo"); 692 } 693 694 if (moPath.exists && moPath.isDir) 695 return dirEntries(moPath, "*.mo", SpanMode.shallow).map!(a => a.name).array; 696 697 return null; 698 } 699 700 /** 701 Returns the language code for the current language. 702 */ 703 string languageCode() @safe 704 { 705 import std.string : lineSplitter; 706 import std.algorithm : find, startsWith; 707 auto l = currentLanguage.header.lineSplitter.find!(a => a.startsWith("Language: ")); 708 return l.empty ? "Default" : l.front["Language: ".length .. $]; 709 } 710 711 /** 712 Returns the language code for the translation contained in `moFile`. 713 */ 714 string languageCode(string moFile) @safe 715 { 716 import std.string : lineSplitter; 717 import std.algorithm : find, startsWith; 718 auto l = MoFile(moFile).header.lineSplitter.find!(a => a.startsWith("Language: ")); 719 return l.empty ? "Undefined" : l.front["Language: ".length .. $]; 720 } 721 722 /** 723 Switch to the language contained in `moFile`. 724 */ 725 void selectLanguage(string moFile) @safe 726 { 727 import std.file : exists, isFile; 728 729 currentLanguage = moFile.exists && moFile.isFile ? MoFile(moFile) : MoFile(); 730 }