tkmsgcat

Create multilingual interfaces for your tkinter applications.

tkinter-msgcat leverages Tk's msgcat to provide a per-instance message catalog which holds all the translations, while allowing them to be kept in separate files away from code.

Example use:

>>> from tkmsgcat import load, locale, translate
>>> load("msgs")
>>> locale = "hi"
>>> translate("Hello")
"नमस्ते"

Complete docs available on https://tkmsgcat.rtfd.io.

MessageCatalog

Override this to create custom msgcat functionality.

Most applications should suffice with the package-level functions.

Scope

Tkinter's message catalog is scoped to a tkinter.Tk instance and not the Python interpreter! In practice, this means that you need to have a single tkinter.Tk instance to share the loaded translations and locales.

Source code in tkmsgcat/__init__.py
class MessageCatalog:
    """Override this to create custom msgcat functionality.

    Most applications should suffice with the package-level functions.

    Caution: Scope
        Tkinter's message catalog is scoped to a `tkinter.Tk` instance and
        not the Python interpreter! In practice, this means that you need to
        have a single `tkinter.Tk` instance to share the loaded translations
        and locales.
    """

    @property
    def root(self) -> tk.Tk:
        return cast(tk.Tk, _get_default_root(what="use msgcat"))

    def eval_(self, cmd: str) -> str:
        log.debug("Evaluating %s", cmd)
        return self.root.eval(cmd)

    def _splitlist(self, __s: str) -> tuple[str]:
        # pylint: disable=deprecated-typing-alias
        return cast(Tuple[str], self.root.splitlist(__s))

    @staticmethod
    def _join(*__l: str) -> str:
        new_l = []
        for i in __l:
            stripped = i.strip('"')
            new_l.append(f'"{stripped}"')
        return " ".join(new_l)

    @staticmethod
    def _dict2str(__d: dict[str, str]) -> str:
        lst = []
        for key, val in __d.items():
            # Append quotes to support strings with spaces
            s_key = key.strip('"')
            s_val = val.strip('"')
            key = f'"{s_key}"'
            val = f'"{s_val}"'
            lst.extend([key, val])
        return " ".join(lst)

    @contextlib.contextmanager
    def _locale_ctx(self, newlocale: str) -> Iterator[None]:
        oldlc = self.locale
        self.locale = newlocale
        try:
            yield None
        finally:
            self.locale = oldlc

    def is_init(self) -> bool:
        tkbool = self.eval_("::msgcat::mcpackageconfig isset mcfolder")
        return cast(bool, self.root.getboolean(tkbool))

    def is_loaded(self, locale_: str) -> bool:
        tklist = self.eval_("::msgcat::mcloadedlocales loaded")
        return locale_ in self._splitlist(tklist)

    @overload
    def load(self, dir_: str) -> None:
        ...  # pragma: no cover

    @overload
    def load(self, dir_: pathlib.Path) -> None:
        ...  # pragma: no cover

    def load(self, dir_: str | pathlib.Path) -> None:
        _path = dir_ if isinstance(dir_, pathlib.Path) else pathlib.Path(dir_)
        _resolvedpath = _path.resolve()
        # ! Tk bug: All backslashes need to be replaced by formward slashes
        msgsdir = str(_resolvedpath).replace("\\", "/")
        log.debug("Loading translations from %s", msgsdir)
        self.eval_(
            f'::msgcat::mcload [file join [file dirname [info script]] "{msgsdir}"]'
        )

    @property
    def locale(self) -> str:
        return self.eval_("::msgcat::mclocale")

    @locale.setter
    def locale(self, newlocale: str) -> None:
        self.eval_(f"::msgcat::mclocale {newlocale}")

    @property
    def loaded_locales(self) -> tuple[str]:
        tklist = self.eval_("::msgcat::mcloadedlocales loaded")
        return self._splitlist(tklist)

    loaded_from = _PackageOption("mcfolder")

    def longest(self, strings: tuple[str]) -> int:
        return int(self.eval_(f"::msgcat::mcmax {self._join(*strings)}"))

    def longest_in(self, locale_: str, strings: tuple[str]) -> int:
        with self._locale_ctx(locale_):
            return self.longest(strings)

    @property
    def preferences(self) -> tuple[str]:
        tklist = self.eval_("::msgcat::mcpreferences")
        return self._splitlist(tklist)

    def has(self, what: str, search_all: bool = True) -> bool:
        command = "::msgcat::mcexists"
        if not search_all:
            command += " -exactlocale"
        command += f" {what}"
        return cast(bool, self.root.getboolean(self.eval_(command)))

    def add(self, what: str, translation: str) -> None:
        self.eval_(f'::msgcat::mcset {self.locale} "{what}" "{translation}"')

    def add_to(self, locale_: str, what: str, translation: str) -> None:
        self.eval_(f'::msgcat::mcset {locale_} "{what}" "{translation}"')

    def update(self, translations: dict[str, str]) -> None:
        self.eval_(
            f"::msgcat::mcmset {self.locale} {{{self._dict2str(translations)}}}",
        )

    def update_to(self, locale_: str, translations: dict[str, str]) -> None:
        self.eval_(f"::msgcat::mcmset {locale_} {{{self._dict2str(translations)}}}")

    def get(self, what: str, *fmtargs: str) -> str:
        command = f'::msgcat::mc "{what}"'
        if fmtargs:
            command = command + " " + self._join(*fmtargs)
        return self.eval_(command)

    def get_from(self, locale_: str, what: str, *fmtargs: str) -> str:
        with self._locale_ctx(locale_):
            return self.get(what, *fmtargs)

    # TODO Doesn't work
    # locale_handler = _Handler("changecmd")

    missing_handler = _PackageOption("unknowncmd")

    # TODO Doesn't work
    # preload_handler = _Handler("loadcmd")

    def unload(self) -> None:
        self.eval_("::msgcat::mcforgetpackage")

add(what: str, translation: str) -> None

Set/update a translation for the current locale.

Parameters:
  • what (str) – The string to be translated.

  • translation (str) – The translated string.

Source code in tkmsgcat/__init__.py
def add(what: str, translation: str) -> None:
    """Set/update a translation for the current locale.

    Args:
        what (str): The string to be translated.
        translation (str): The translated string.
    """
    _default_msgcat.add(what, translation)

add_to(locale_: str, what: str, translation: str) -> None

Set/update a translation in a specific locale.

Parameters:
  • locale_ (str) – The locale in which this operation will take place.

  • what (str) – The string to be translated.

  • translation (str) – The translated string.

Source code in tkmsgcat/__init__.py
def add_to(locale_: str, what: str, translation: str) -> None:
    """Set/update a translation in a specific locale.

    Args:
        locale_ (str): The locale in which this operation will take place.
        what (str): The string to be translated.
        translation (str): The translated string.
    """
    _default_msgcat.add_to(locale_, what, translation)

get(what: str, *fmtargs: str) -> str

Translate a string according to a user's current locale.

Parameters:
  • what (str) – The string to be translated. It should generally be in English as that is the language used by code itself.

  • *fmtargs (tuple[str]) – Extra arguments passed internally to the format package.

Returns:
  • str – The translated string.

Source code in tkmsgcat/__init__.py
def get(what: str, *fmtargs: str) -> str:
    """Translate a string according to a user's current locale.

    Args:
        what (str): The string to be translated. It should generally be in
            English as that is the language used by code itself.
        *fmtargs (tuple[str], optional): Extra arguments passed internally
            to the [format](https://www.tcl.tk/man/tcl8.6/TclCmd/format.html)
            package.

    Returns:
        str: The translated string.
    """
    return _default_msgcat.get(what, *fmtargs)

get_from(locale_: str, what: str, *fmtargs: str) -> str

Get the translation of a string from a specific locale.

Parameters:
  • locale_ (str) – The locale to be used for looking up __what.

  • what (str) – The string to be translated. It should generally be in English as that is the language used by code itself.

  • *fmtargs (tuple[str]) – Extra arguments passed internally to the format package.

Returns:
  • str – The translated string.

Source code in tkmsgcat/__init__.py
def get_from(locale_: str, what: str, *fmtargs: str) -> str:
    """Get the translation of a string from a specific locale.

    Args:
        locale_ (str): The locale to be used for looking up `__what`.
        what (str): The string to be translated. It should generally be in
            English as that is the language used by code itself.
        *fmtargs (tuple[str], optional): Extra arguments passed internally
            to the [format](https://www.tcl.tk/man/tcl8.6/TclCmd/format.html)
            package.

    Returns:
        str: The translated string.
    """
    return _default_msgcat.get_from(locale_, what, *fmtargs)

has(what: str, search_all: bool = True) -> bool

Check if a string has a translation in the current/all locale(s).

Parameters:
  • what (str) – The string to lookup for a translation.

  • search_all (bool) – Whether to search in all of the loaded locales or just the current locale. If a locale is not set, the value returned by preferences is used. Defaults to True.

Returns:
  • bool – Whether the given string has a translation.

Source code in tkmsgcat/__init__.py
def has(what: str, search_all: bool = True) -> bool:
    """Check if a string has a translation in the current/all locale(s).

    Args:
        what (str): The string to lookup for a translation.
        search_all (bool, optional): Whether to search in all of the loaded
            locales or just the current locale. If a locale is not set, the
            value returned by `preferences` is used. Defaults to True.

    Returns:
        bool: Whether the given string has a translation.
    """
    return _default_msgcat.has(what, search_all)

is_init() -> bool

Whether any translation file has been loaded.

Source code in tkmsgcat/__init__.py
def is_init() -> bool:
    """Whether any translation file has been loaded."""
    return _default_msgcat.is_init()

is_loaded(locale_: str) -> bool

Whether a translation file for a particular locale is loaded.

Parameters:
  • locale_ (str) – The locale to be checked if it is loaded.

Source code in tkmsgcat/__init__.py
def is_loaded(locale_: str) -> bool:
    """Whether a translation file for a particular locale is loaded.

    Args:
        locale_ (str): The locale to be checked if it is loaded.
    """
    return _default_msgcat.is_loaded(locale_)

load(dir_: str | pathlib.Path) -> None

Loads all translation files from the specified directory.

Parameters:
  • dir_ (str | pathlib.Path) – The path/name of the directory where all the translation files (.msg extension) are stored. Tk recommends you to store them all in a separate msgs directory at the package level and name them according to their locale.

Source code in tkmsgcat/__init__.py
def load(dir_: str | pathlib.Path) -> None:
    """Loads all translation files from the specified directory.

    Args:
        dir_ (str | pathlib.Path): The path/name of the directory where
            all the translation files (.msg extension) are stored. Tk
            recommends you to store them all in a separate `msgs` directory at
            the package level and name them according to their locale.
    """
    _default_msgcat.load(dir_)

loaded_from() -> str

Returns the path of the directory from which translations were loaded.

Exceptions:
  • AttributeError – When the directory is not set.

Source code in tkmsgcat/__init__.py
def loaded_from() -> str:
    """Returns the path of the directory from which translations were loaded.

    Raises:
        AttributeError: When the directory is not set.
    """
    return _default_msgcat.loaded_from

loaded_locales() -> tuple[str]

Returns a list of all the currently loaded locales.

A locale is loaded only when it is requested i.e. set via locale.

Source code in tkmsgcat/__init__.py
def loaded_locales() -> tuple[str]:
    """Returns a list of all the currently loaded locales.

    A locale is loaded only when it is requested i.e. set via `locale`.
    """
    return _default_msgcat.loaded_locales

locale(newlocale: str = '') -> str

The locale used to translate strings.

Tip

See msgcat manual for details on how to specify a locale.

Parameters:
  • newlocale (str) – Use this to change the locale.

Returns:
  • str – The currently used locale.

Source code in tkmsgcat/__init__.py
def locale(newlocale: str = "") -> str:
    """The locale used to translate strings.

    Tip:
        See [msgcat manual](https://www.tcl-lang.org/man/tcl/TclCmd/msgcat.htm#M19)
        for details on how to specify a locale.

    Args:
        newlocale (str): Use this to change the locale.

    Returns:
        str: The currently used locale.
    """
    if newlocale:
        _default_msgcat.locale = newlocale
    return _default_msgcat.locale

longest(what: tuple[str]) -> int

Find the length of the longest translated string in the current locale.

This is useful in deciding the maximum size of a label or a button when using the place geometry manager, for exmaple.

Parameters:
  • what (str) – The strings whose translations are to be compared.

Returns:
  • int – Length of the longest translated string with respect to all the strings passed .

Source code in tkmsgcat/__init__.py
def longest(what: tuple[str]) -> int:
    """Find the length of the longest translated string in the current locale.

    This is useful in deciding the maximum size of a label or a button when
    using the `place` geometry manager, for exmaple.

    Args:
        what (str): The strings whose translations are to be compared.

    Returns:
        int: Length of the longest translated string with respect to all the
            strings passed .
    """
    return _default_msgcat.longest(what)

longest_in(locale_: str, what: tuple[str]) -> int

Find the length of the longest translated string in a specific locale.

This is useful in deciding the maximum size of a label or a button when using the place geometry manager, for exmaple.

Parameters:
  • locale_ (str) – The locale to use for finding the length.

  • what (str) – The strings whose translations are to be compared.

Returns:
  • int – Length of the longest translated string with respect to all the strings passed .

Source code in tkmsgcat/__init__.py
def longest_in(locale_: str, what: tuple[str]) -> int:
    """Find the length of the longest translated string in a specific locale.

    This is useful in deciding the maximum size of a label or a button when
    using the `place` geometry manager, for exmaple.

    Args:
        locale_ (str): The locale to use for finding the length.
        what (str): The strings whose translations are to be compared.

    Returns:
        int: Length of the longest translated string with respect to all the
            strings passed .
    """
    return _default_msgcat.longest_in(locale_, what)

missing_handler(func: Callable[..., str] | None = None) -> None

Register the callback invoked when a translation is not found.

It is invoked with the same arguments passed to translate. It must return a formatted message as translate would do normally.

Parameters:
  • func (Callable) – The handler is set when this has a value and unset when it is None. Defaults to None.

Source code in tkmsgcat/__init__.py
def missing_handler(func: Callable[..., str] | None = None) -> None:
    """Register the callback invoked when a translation is not found.

    It is invoked with the same arguments passed to `translate`. It must
    return a formatted message as `translate` would do normally.

    Args:
        func (Callable, optional): The handler is set when this has a value
            and unset when it is None. Defaults to None.
    """
    if func:
        _default_msgcat.missing_handler = func
    else:
        del _default_msgcat.missing_handler

preferences() -> tuple[str]

Returns a list of the preferred locales based on the current locale.

Source code in tkmsgcat/__init__.py
def preferences() -> tuple[str]:
    """Returns a list of the preferred locales based on the current locale."""
    return _default_msgcat.preferences

unload() -> None

Unloads all translations and forgets all callbacks and settings.

You can reinitialise the message catalog by a calling load with the appropriate arguments.

Source code in tkmsgcat/__init__.py
def unload() -> None:
    """Unloads all translations and forgets all callbacks and settings.

    You can reinitialise the message catalog by a calling `load` with the
    appropriate arguments.
    """
    _default_msgcat.unload()

update(translations: dict[str, str]) -> None

Set/update translations of the current locale.

Parameters:
  • translations (dict[str, str]) – A mapping of source strings to translated strings.

Source code in tkmsgcat/__init__.py
def update(translations: dict[str, str]) -> None:
    """Set/update translations of the current locale.

    Args:
        translations (dict[str, str]): A mapping of source strings to
            translated strings.
    """
    _default_msgcat.update(translations)

update_to(locale_: str, translations: dict[str, str]) -> None

Set/update translations in a specific locale.

Parameters:
  • locale_ (str) – The locale in which this operation will take place.

  • translations (dict[str, str]) – A mapping of source strings to translated strings.

Source code in tkmsgcat/__init__.py
def update_to(locale_: str, translations: dict[str, str]) -> None:
    """Set/update translations in a specific locale.

    Args:
        locale_ (str): The locale in which this operation will take place.
        translations (dict[str, str]): A mapping of source strings to
            translated strings.
    """
    _default_msgcat.update_to(locale_, translations)