The built-in strings of Adw.AboutDialog do not get localized when changing the language at runtime

I am trying to implement dynamic language switching in my app at runtime and I have managed to do so but for some reason Adw.AboutDialog is the only thing left that is not fully affected by the language change, specifically the built-in strings of Adw.AboutDialog (“Support Questions”, “Report an Issue” etc…) stay in English while switching to RTL even though the whole UI is rebuilt on a language switch.

For example, selecting the language at start up via LANG=ar_IQ.UTF-8 python3 -m myappdoes the trick, the built-in strings of Adw.AboutDialog (“Support Questions”, “Report an Issue” etc…) get properly localized but doing the same from the Adw.ComboRowthat I have added in the Adw.PreferencesPage() does not.

Am I doing something wrong or is there a solution that does not involve restarting the app or is this a current limitation of libadwaita?

Here is a code snippet:

def on_language_changed(self, row, _name):
    current_lang = row.get_selected_item().get_string()
    current_page = self.stack.get_visible_child_name()

    direction = (
        Gtk.TextDirection.RTL
        if self.languages[current_lang] in i18n.RTL_LANGUAGES
        else Gtk.TextDirection.LTR
    )
    Gtk.Widget.set_default_direction(direction)
    os.environ["LANG"] = self.languages[current_lang]

    self.build_widgets()
    self.win.set_content(self.view)

    self.language_row.disconnect_by_func(self.on_language_changed)

    assert current_page is not None
    self.stack.set_visible_child_name(current_page)
    index = self.lang_names.index(current_lang)
    self.language_row.set_selected(index)

    self.language_row.connect("notify::selected-item", self.on_language_changed)

def build_widgets(self):
    header = Adw.HeaderBar()
    self.view = Adw.ToolbarView()
    self.stack = Adw.ViewStack(vexpand=True)

    main_page = self.build_main_page()
    trusted_page = self.build_trusted_page()
    settings_page = self.build_settings_page()
    about_dialog = self.build_about_dialog()

Nothing in GTK or libadwaita is designed around switching language in runtime. I’d suggest to just not do that, there’s already a system language selector, why do you need a second one?

What you could do is change LANG variable with your desired language, and make the program reset completely by calling any of execve family of functions with the new language set.

You would perform this by passing to execve the original argc and argv values you would normally get from main().

On Python I think you would use sys.args to get them.

Note this would only work on UNIX systems (Linux, macOS, etc.), it will not work on Windows, AFAIK.

Please note that the program will start completely from the beginning, so if you wish to show an specific window/widget when the program restarts, you would have to implement this yourself.

It looks like the python execve functions works on Windows as well:

I sometimes find myself using someone else’s computer, but the software I have to use is either difficult or frustrating to use because of its language so having a language selecter is just nice to have in my book.

Sure, it would be. The issue is that the localisation infrastructure available on Linux (and, by extension, on every platform supported by GTK and libadwaita) does not allow for that to happen.

The localisation API is based on gettext, which depends on environment variables only read at the beginning of a process. GTK cannot change language at run time, because it would require discarding all constructed UI, and rebuilding it from scratch, since all the localisable text is either static, or it’s recomputed at the point of construction.

I guess there is no way around it, thanks.

No unfortunately, based on what ebassi said, you would need to restart the process completely with the new language set, which is what execve family of functions do.

You may have seen this behaviour on various apps, where the app ask the user to restart the application so the changes have effect. Well they are most probably doing this.

I noticed this while I was working on implementing process restart for a full language change. Basically, I managed to make language switching at runtime work flawlessly, no restart needed!

  • Unless your app involves date formats, number formats, collation order, currency symbols, etc… that you want to be localized, then do not manipulate the LANG environment variable; you should use the LANGUAGE variable only. This change alone would make the built-in strings switch language, and it also would save you from many headaches, trust me.
  • Now if you only do the above change, then after switching the language for the 1st time then you try to switch back to the original language the app launched with (for example, en –> ar –> en) you will notice that the built-in strings will still be in Arabic. To fix this, you will have to call the C-level bindtextdomain() and textdomain() directly via ctypes for libadwaita domain after changing LANGUAGE, that will trigger a re-lookup that picks up the new LANGUAGE value which in turn will update the built-in strings.
import ctypes
import ctypes.util

libc = ctypes.CDLL(ctypes.util.find_library("c"))

libc.bindtextdomain.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
libc.bindtextdomain.restype = ctypes.c_char_p

libc.textdomain.argtypes = [ctypes.c_char_p]
libc.textdomain.restype = ctypes.c_char_p


def invalidate_gettext_cache(domain: str, locale_dir: str) -> None:
    if not libc.bindtextdomain(domain.encode(), locale_dir.encode()):
        raise RuntimeError("bindtextdomain failed")

    libc.textdomain(domain.encode())

invalidate_gettext_cache("libadwaita", "/usr/share/locale")

needless to say that this relies on an implementation detail of glibc’s caching behavior and is not a guaranteed public API.

It’s worth noting that this will only invalidate libadwaita’s textdomain, you would need to do the same for all the loaded textdomains. e.g:

GETTEXT_PACKAGE = "your-app-name"

invalidate_gettext_cache("libadwaita", "/usr/share/locale")
invalidate_gettext_cache("gtk", "/usr/share/locale")
# Others...
invalidate_gettext_cache(GETTEXT_PACKAGE, "/usr/share/locale") # Your app textdomain

Otherwise you would have strings from gtk and of yours app translated on the previous cached language.

you would need to do the same for all the loaded textdomains. Otherwise you would have strings from gtk and of yours app translated on the previous cached language.

That does not seem true at all based on my real-world testing with my app. My app’s textdomain did not need to be invalidated at the C library level, all my app’s strings were being translated with no issue. In other words, I did not and do not need to do:

invalidate_gettext_cache(GETTEXT_PACKAGE, "/usr/share/locale") # Your app textdomain

TLDR, you only need to invalidate the textdomain of the library you have problems with its built-in strings (in my case that was only libadwaita), your app’s strings are handled pretty well by Python’s gettext library.