Creating a subtyped string for i18n

A specialized string is a type with a text representation. For example, user-visible text is a string, but not every string is user-visible text. When adding internationalization (often shortened to i18n) to an application, using a string subtype can help ensure that all messages are available to translators. Moreover, we'll use D's templates and compile-time string imports to efficiently load the translation file.

How to do it…

Execute the following steps to create a subtyped string for i18n:

  1. Create a struct with a private string member and a public getter property for that string.
  2. Add alias this to the string getting property.
  3. Write a private function that loads the translation table and returns the string wrapped in our struct.
  4. Write a template that takes a key, file, and line number that assigns the result of the translation function to a member with the same name. You may also use pragma(msg) to print out the whole list of translatable strings.
  5. When you write functions to print strings to the user, accept the TranslatedString type rather than string.
  6. Write a message using the translation template.

    Tip

    On Windows, if you get garbled content in the console, you may need to enable UTF-8 output with SetConsoleOutputCP(65001); from the Windows API.

The following is the code to create a subtyped string for i18n:

struct TranslatedString {
    private string value;
    @property string getValue() { return value; }
    alias getValue this;
}

private TranslatedString localize(string key, bool requireResult = true) {
        // we'll use an associative array for the translation
        // it would also be possible to load from a file, at
        // compile time
        enum string[string] translations =
                [
                        "Hello!" : "¡Hola!"
                ];

        if(auto value = key in translations)
          return TranslatedString(*value);
        if(requireResult)
            throw new Exception(key ~ " is not translated");
       return TranslatedString(null);
}

template T(string key, string file = __FILE__, size_t line = __LINE__) {
    version(gettext) {
            // prints messages at compile time with all strings
            // in the program, if requested
            import std.conv;
            pragma(msg, "#: " ~ file ~ ":" ~ to!string(line));
            pragma(msg, "msgid "" ~ key ~ """);
            pragma(msg, "msgstr ""~localize(key, false)~""");
            pragma(msg, "");
        }

     enum T = localize(key);
}

void main() {
    auto p = T!"Hello!"; // translated string literal
    import std.stdio;
    writeln(p);
}

How it works…

Here, we utilize the type system to help us catch untranslated strings that are to be printed to the user and the compile-time features to help us translate them.

The TranslatedString type, if used throughout the library, would provide a degree of type safety to printing. Using alias this subtyping, it will easily convert back to string for integration with other functions without allowing implicit conversions from string to TranslatedString, which would defeat the purpose.

The T template provides concise syntax to access the localization function (without runtime cost; this pattern is how we can do user-defined literals in D, a topic we'll cover in greater depth in a later chapter) as well as a place to track all translatable strings in the application.

Using the version statement, we provide an optional code path that prints the locations, strings, and current translations of all the instantiations of this template. The version statement takes a single identifier. Unless that identifier is specialized on the compiler's command line or is predefined for the given target platform (for example, Windows or OS X), the code inside is not compiled. Here, we used version(gettext), so the code inside is only compiled if dmd is passed the –version=gettext argument.

If this code is compiled, it will run the pragma(msg) instructions. This will print out the given string at compile time. It is meant to give a compilation message to the user. The argument need not be a literal because it will be evaluated using compile-time function evaluation; ordinary D code will be run and the result printed.

The arguments to T, (string key, string file = __FILE__, size_t line = __LINE__), include another bit of D magic: the default arguments to file and line. Just like we saw with runtime function arguments back in Chapter 1, Core Tasks, when we looked at creating a custom exception type, the special tokens __FILE__, __LINE__, and a few others are automatically inserted at the call site. It works the same way with compile-time arguments. Using these, we can print out the list of strings (including the location of their appearance) for the translator to examine, without writing an external tool to parse the code to find the strings.

Once we have the strings, we put them into an associative array literal inside the localize function, which is wrapped in the TranslatedString type, and use that to get the translation to print to the user.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.166.131