Hyprland icon indicating copy to clipboard operation
Hyprland copied to clipboard

I18n support for dialogs / notifications / etc.

Open vaxerski opened this issue 7 months ago • 10 comments

Discussion

https://github.com/hyprwm/Hyprland/discussions/9948

Description

As the Hyprland gui stuff is expanding, and Hyprland starts printing more and more text in general, it might be a good idea to add some i18n class and translation files, where volunteers could contribute as well for their languages.

vaxerski avatar Apr 06 '25 21:04 vaxerski

Don't you mean i18n?

fufexan avatar Apr 06 '25 21:04 fufexan

yes, I always forget the numba

vaxerski avatar Apr 06 '25 21:04 vaxerski

depending on how much you want to keep leaning into Qt, could just build on their stuff? https://doc.qt.io/qt-6/i18n-source-translation.html

mhydock avatar Apr 07 '25 23:04 mhydock

nope, we'd have I18n inside hyprland itself

vaxerski avatar Apr 08 '25 13:04 vaxerski

Have you got an idea of the API you're looking for?

internationalization covers a lot

  1. Bidirectional text support - required for Arabic or Hebrew etc.
  2. Locale aware formatting for dates, times, numbers, currency.
  3. text segmentation support for tokenizing or wrapping text in languages like Chinese or Japanese as they don't use spaces between words.
  4. Basic Key = string translation, potentially with placeholders for formatting values (that might also need localization).
  5. Loading and management of a translation catalog of some kind in JSON or YAML or some other format that will be easy for contributors to edit.
  6. detection and proper handling of LANG, LC_ALL, etc to select the initial locale.
  7. UTF-8 is used everywhere, obviously.

I think the most important thing to decide early is whether you ever want to support RTL ever as you will want to at least have the scaffolding in place to support that feature even if you don't implement it right away.

If it were me I'd start with something like

#pragma once

#include <string>
#include <string_view>
#include <unordered_map>

namespace i18n {

using ArgMap = std::unordered_map<std::string_view, std::string>;
using TranslationMap = std::unordered_map<std::string_view, std::string_view>;

struct LocaleInfo {
    std::string_view code;
    bool is_rtl;
};

// Sets the current active locale (e.g., "en", "de")
void set_locale(std::string_view locale_code);

// Gets the currently active locale's metadata
[[nodiscard]] const LocaleInfo& current_locale() noexcept;

// Returns true if the current locale is a right-to-left language
[[nodiscard]] bool is_rtl() noexcept;

// Gets a translation for the given key (no interpolation)
[[nodiscard]] std::string t(std::string_view key);

// Gets a translation with fmt-style placeholder substitution
[[nodiscard]] std::string t(std::string_view key, const ArgMap& args);

// Sets the fallback locale to use when a key is missing
void set_fallback_locale(std::string_view fallback_code);

// Manually loads translations into a locale (used for testing or static config)
void load_translations(std::string_view locale_code, const TranslationMap& translations);

} // namespace i18n
#include "i18n.h"

#include <print>
#include <string>
#include <unordered_map>

int main() {
    // Step 1: Load translations
    i18n::load_translations("en", {
        {"startup_complete", "Hyprland started in {ms}ms."},
        {"monitor_connected", "Monitor {name} connected at {resolution}."},
        {"workspace_switched", "Switched to workspace {number} on monitor {monitor}."},
        {"window_moved", "Window {title} moved to workspace {workspace}."}
    });

    i18n::load_translations("de", {
        {"startup_complete", "Hyprland in {ms}ms gestartet."},
        {"monitor_connected", "Monitor {name} mit Auflösung {resolution} verbunden."},
        {"workspace_switched", "Zu Arbeitsfläche {number} auf Monitor {monitor} gewechselt."},
        {"window_moved", "Fenster {title} wurde zu Arbeitsfläche {workspace} verschoben."}
    });

    // Step 2: Set locale
    i18n::set_locale("de"); // change to "en" if you prefer

    // Step 3: Emit event messages
    std::print("{}\n", i18n::t("startup_complete", {
        {"ms", "148"}
    }));

    std::print("{}\n", i18n::t("monitor_connected", {
        {"name", "DP-1"},
        {"resolution", "2560x1440@144Hz"}
    }));

    std::print("{}\n", i18n::t("workspace_switched", {
        {"number", "2"},
        {"monitor", "eDP-1"}
    }));

    std::print("{}\n", i18n::t("window_moved", {
        {"title", "Alacritty"},
        {"workspace", "3"}
    }));

    return 0;
}

Hey this AI generated slop isn't too bad for a intital stab. Anyway, this is the kind of thing I can probably do, if you're keen.

Or not, I don't care, I have a day job.

matjam avatar Apr 10 '25 03:04 matjam

what does your comment add? To me it reads off as "oh if you don't know how to code I'll show you some ai generated code"

vaxerski avatar Apr 10 '25 12:04 vaxerski

I am asking what API you're looking for and if you need help writing it. Its just a starting point mate. I would like to help if I can?

matjam avatar Apr 10 '25 20:04 matjam

nope, no need. This is quite a big thing, I'll write it myself once I get time. I prefer writing bigger things myself

vaxerski avatar Apr 10 '25 22:04 vaxerski

AI slop aside, this is also why i recommended using something already built. i18n is a total headache when you want to do it right. (also, the fact that the plagiarism machine has an opinion at all shows that this is a problem that's been solved numerous times)

mhydock avatar Apr 11 '25 23:04 mhydock

suggest something then instead of theorizing :)

vaxerski avatar Apr 12 '25 14:04 vaxerski

Probably a good idea to base the string translation and placeholders on the Unicode MessageFormat spec, because then you get quite a bit of the work already done for you in the ICU4C C++ library. It allows for things like different languages needing different words for plurals than the source language, e.g. in English there's just "1 day" or "2 days" but in Czech the plural of "den" (day) could be "dny" or "dne" or "dni" depending on how many there are. So in MessageFormat you'd write something like this for the English-language original string:

.input {$numDays :number}
.match $numDays
one {{{$numDays} day}}
*   {{{$numDays} days}}

Then the Czech translator can write this:

.input {$numDays :number}
.match $numDays
one  {{{$numDays} den}}
few  {{{$numDays} dny}}
many {{{$numDays} dne}}
*    {{{$numDays} dní}}

And now your "Remind me in # days" message will look grammatically correct when it's translated into Czech.

MessageFormat looks like it's more complex than it needs to be and a first look makes you think "Surely it could be simpler??!?". But it turns out that simpler solutions don't allow translators to sound grammatically correct in all languages.

P.S. If you search the Web for info about MessageFormat, a lot of it will be about MessageFormat version 1 (e.g. this docs page from ICU). MessageFormat version 2 fixes several of v1's pain points so since you're setting up a brand-new system, v2 will definitely be easier in the long run. (And probably in the short run too).

rmunn avatar Jun 30 '25 07:06 rmunn

Please see https://github.com/hyprwm/hyprutils/pull/83

vaxerski avatar Nov 08 '25 19:11 vaxerski