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 }