June 9, 2023 by Olivier Goffart

Introducing Translation Infrastructure in Slint

We're excited to announce a new feature of the upcoming release of Slint version 1.1: translation infrastructure. In this blog post, we'll provide an overview of Slint's translation capabilities and explain why we chose Gettext as the underlying technology. We also value your feedback and suggestions to help us improve this feature further.


Empowering Multilingual User Interfaces

Screenshot of Printer demo translated to French
The Printer Demo translated to French

How It Works

The translation infrastructure in Slint is powered by the @tr(...) macro, allowing you to mark strings for translation. This macro performs two important tasks:

  1. Looks up the translation for the specified string.
  2. Replaces variables by interpolating the string.

Let's look at an example of using the @tr macro in Slint code:

Button {
    text: @tr("Next");
}

In the code above, the @tr macro wraps the string "Next," indicating the need for translation. This mechanism provides a simple way to incorporate translations into your Slint user interface.

Behind the scenes, Slint uses the popular Gettext library for looking up translated strings. We've developed a tool that extracts all strings from a .slint file to create a template file in Gettext POT format (Portable Object Template). Gettext and associated tools can then be used for translate the source text - here english - to another language.

The syntax used within the @tr macro is inspired by the tr crate in Rust. It supports interpolation using {} or {0}, handles plurals with {n} and ".." | ".." % n notation, and allows adding context using "..." =>.

The {} syntax for interpolation is preferred as it aligns with the conventions in the standard libraries of Rust and C++.

Let's explore a more complex example that demonstrates formatting and plural handling:

component MessageNotificationText {
    in property <int> message-count;
    in property <string> folder;
    Text {
        text: @tr("notification" =>
            "You have one new message in folder {}"
            | "You have {n} new messages in folder {}" % message-count,
            folder);
    }
}

In the above code, the @tr macro is used to translate a notification message. Depending on the value of message-count, the appropriate plural form is selected. The variable {n} represents the count, and {} is used for string interpolation to include the folder name.

While we understand that the initial version of this feature in Slint may have limitations, it provides a solid foundation for using translations and opens up possibilities for localization.

Why Gettext?

We've received valuable feedback from users who questioned our choice of Gettext over more modern approaches like the Fluent project. We like to share our reasons behind this decision:

  • Industry Standard: Gettext is widely adopted and used by various projects, including popular applications like GNU tools (e.g., gcc), the GNOME applications, and the KDE applications. Gettext is included in the glibc and is also available as a cross-platform library, making it a reliable choice for Slint.
  • Mature Ecosystem: Gettext benefits from a wide range of available translation tools, such as poedit, OmegaT, Lokalize, or Transifex (a web service). Translators familiar with Gettext can leverage these existing resources to work effectively and efficiently on translating Slint applications.

Gettext primarily focuses on translation lookup rather than extensive formatting. While it does offer some support for plural handling, it is basically handling static strings, and formatting has to be implemented separately. On the other hand, Fluent provides a robust formatting syntax and a domain-specific language (DSL).

In Slint, we've currently implemented only basic formatting capabilities, which may not be as comprehensive as Fluent's but are sufficient for the majority of use cases. In .slint files, more than 99% of the strings don't require formatting. These strings primarily consist of labels and interface elements. The few strings that do require formatting are usually very simple. Complex strings needing intricate formatting typically come from the business logic in C++ or Rust, where different translation systems can be utilized.

However, the main reason we chose Gettext over Fluent is explained in the following paragraph:

Message Identifiers vs. English in the Source Code

One significant difference between Gettext and Fluent is how strings are referenced in the source code. With Gettext, the original language (usually English) strings appear verbatim in the source code with minimal additional syntax. The original English text serves as the key for translation, sometimes with additional context.

Let's consider an example using Gettext:

component PrintDetails inherits GridLayout {
    in property <PrinterQueueItem> queue-item;
    spacing: 3px;

    Row {
        PrintQueueDetailsLabelTitle {
            text: @tr("Job Owner");
        }
        PrintQueueDetailsLabel {
            text: root.queue-item.owner;
        }
    }
    // ...
}

In the above code snippet, the string "Job Owner" is used directly as the key for translation. When reading this code, it's immediately clear which string is being used, without any context switching. Tools like grep can easily locate the position in the code where this UI element is used, simplifying development.

In contrast, Fluent uses message identifiers to reference strings. The source code must come up with a unique message identifier for each string, and the translation lookup is performed using these identifiers.

Here's an example of how the same code would look using Fluent:

component PrintDetails inherits GridLayout {
    in property <PrinterQueueItem> queue-item;
    spacing: 3px;

    Row {
        PrintQueueDetailsLabelTitle {
            text: @tr("print-details-owner");
        }
        PrintQueueDetailsLabel {
            text: root.queue-item.owner;
        }
    }
    // ...
}

In the Fluent approach, the string "Job Owner" is now referenced by a message identifier (print-details-owner), which is then mapped to its translation in a separate file. As a result, the original strings are no longer directly visible in the code, adding an additional layer of indirection when searching or understanding the context of a string.

The choice of Gettext in Slint allows for a more straightforward and self-contained approach, where the original language strings are visible in the source code, making it easier to work with and maintain.

Future Plans

While the initial version of the translation infrastructure in Slint provides a solid foundation, we plan to enhance its capabilities in the future. Here are some of the features we're considering:

  • Alternative Gettext implementation: Currently, we use the gettext-rs crate to wrap the system gettext library or compile it from the C source. We're exploring the possibility of using the gettext crate that re-implements Gettext in Rust. However, the re-implementation does not yet provide certain features, such as finding the translation file. We're also considering supporting translation systems other than .mo files and providing a custom translation system.
  • no_std: Slint supports no_std runtimes, which cannot work with gettext. To address this, we're considering making the Slint compiler perform translations at compile time or embedding all translations in the binary.
  • Language change at runtime: We plan to enable applications to change the language from the UI and automatically refresh all its strings.
  • Localized formatting: Our future plans include providing options for proper formatting of numbers, dates, and other localized content based on the current language.

Conclusion

The upcoming release of Slint version 1.1 will introduce translations as a core feature, enabling developers to localize their user interfaces effectively. While the initial version may lack certain functionalities, it serves as a solid starting point for localization in Slint.

We invite you to try the current version on the master branch or wait for the next release. Your feedback is crucial to help us refine and enhance this feature further. Please share your thoughts and suggestions by participating in the GitHub issue dedicated to translations.


Comments