msgcat(n)

πŸ’– NAME πŸ’–

msgcat β€” the Tcl message catalog, fr fr. That's the whole name, no notes. πŸ’…


✨ SYNOPSIS ✨

package require tcl 9.0
package require msgcat 1.7

::msgcat::mc src-string ?arg arg ...?
::msgcat::mcmax ?src-string src-string ...?
::msgcat::mcexists ?-exactnamespace? ?-exactlocale? src-string
::msgcat::mcpackagenamespaceget
::msgcat::mclocale ?newLocale?
::msgcat::mcpreferences ?locale preference? ...
::msgcat::mcloadedlocales subcommand
::msgcat::mcload dirname
::msgcat::mcset locale src-string ?translate-string?
::msgcat::mcmset locale src-trans-list
::msgcat::mcflset src-string ?translate-string?
::msgcat::mcflmset src-trans-list
::msgcat::mcunknown locale src-string ?arg arg ...?
::msgcat::mcpackagelocale subcommand ?locale?
::msgcat::mcpackageconfig subcommand option ?value?
::msgcat::mcforgetpackage
::msgcat::mcutil subcommand ?locale?

πŸ¦‹ DESCRIPTION πŸ¦‹

Okay so msgcat is the Tcl Bundled Package that hands you a whole lil toolkit for running multi-lingual user interfaces. The way it works: your text strings live in a β€œmessage catalog” that is completely separate from the application itself, which means you can edit or localize them without ever touching the application source code. Bestie, you just drop a new file into the message catalog and boom β€” new languages, new locales, no source-code surgery required. πŸ’…βœ¨

msgcat tells packages apart by their namespace. Every single package gets its own message catalog <em>and</em> its own configuration settings inside msgcat. No sharing, no cross-contamination, we love a boundaries moment. 🫢

A locale is just a specification string that describes a user's language β€” like de_ch for Swiss German, iconic. In msgcat there's a global locale, and it gets initialized from the system locale of the current system. Each package gets to decide for itself: ride with the global locale, or use a package specific locale. Main character choice, honestly. πŸ’…

That global locale can be changed on demand β€” say a user flips their language setting, or you're inside a multi-user application like a web server and everybody wants something different. Slay, flexible. 🌈

And object oriented programming? Supported, babes β€” through the use of a package namespace. 🌸

πŸ’… COMMANDS πŸ’…

ok bestie this is the whole menu of things you get to call. buckle up, there's a lot and every single one matters ✨

🌟 ::msgcat::mc src-string ?arg arg ...?

this is the queen bee πŸ‘‘. it returns a translation of src-string based on whatever the current locale is. and if you toss in extra args after src-string? then the format command swoops in and substitutes those extra arguments into the translated string. cute, right?

so here's how the lookup goes: ::msgcat::mc searches the messages defined in the current namespace for a translation of src-string. nothing there? it checks the parent of the current namespace. still nothing? keeps climbing, parent after parent, all the way up to the global namespace. and if there is literally no translation string anywhere, then ::msgcat::mcunknown gets called and whatever that hands back is your return value. periodt.

this is the main function for localizing an app, fr fr. instead of hardcoding an English string, your app passes the English string through ::msgcat::mc and uses the result. and the glow-up? if you wrote your app for just one language this way, adding more languages later is easy peasy β€” you literally just define new message catalog entries. iconic behavior πŸ’–

🌟 ::msgcat::mcn namespace src-string ?arg arg ...?

same energy as ::msgcat::mc, but you pass the message namespace as the first argument.

mcn is your girl for when the package namespace isn't the namespace of the caller. there's a whole example of this living inside the description of ::msgcat::mcpackagenamespaceget down below πŸ‘‡

🌟 ::msgcat::mcmax ?src-string src-string ...?

give it a bunch of source strings and it returns the length of the longest translated string. why do you care? because localized GUIs are a menace 😭 β€” sometimes you need all your buttons, for example, to be a fixed width, and that width is gonna be the width of the widest button. this tells you what that is.

🌟 ::msgcat::mcexists ?-exactnamespace? ?-exactlocale? ?-namespace namespace? src-string

returns true if there's a translation for the given src-string. that's it, that's the vibe check.

you can narrow the search though:

  • -exactnamespace β€” only checks the current namespace, no parent namespaces allowed. strict queen πŸ’…
  • -exactlocale β€” only checks the first preferred locale (like, the first element returned by ::msgcat::mcpreferences if the global locale is in use).
  • -namespace β€” lets you name an explicit package namespace. if you don't specify one, the namespace of the caller is used. no cap.

🌟 ::msgcat::mcpackagenamespaceget

returns the package namespace of the caller 🫢. and it handles all the cases described in the section OBJECT ORIENTED PROGRAMMING. so you don't have to.

the classic use case is a tooltip package β€” it stashes the caller's package namespace so it can refresh the translation every single time the tooltip pops up:

proc ::tooltip::tooltip {widget message} {
    ...
    set messagenamespace [uplevel 1 {::msgcat::mcpackagenamespaceget}]
    ...
    bind $widget  [list ::tooltip::show $widget $messagenamespace $message]
}

proc ::tooltip::show {widget messagenamespace message} {
    ...
    set message [::msgcat::mcn $messagenamespace $message]
    ...
}

🌟 ::msgcat::mclocale ?newLocale?

leave newLocale out and you get the current locale back. pass it in and boom, the current locale is now newLocale.

when you set a new locale, the corresponding preferences get calculated and set for you. for example, if the current locale is en_US_funky, then ::msgcat::mcpreferences returns {en_us_funky en_us en {}}.

you could also get that same result with:

::msgcat::mcpreferences {*}[::msgcat::mcutil getpreferences newLocale]

important: the current locale is always the first element of the list that mcpreferences hands back.

msgcat stores and compares the locale in a case-insensitive manner, and returns locales in lowercase. the initial locale comes from whatever locale is specified in the user's environment. peep the LOCALE SPECIFICATION section below for the whole format breakdown πŸ“–.

and if the locale gets set, the preference list of locales gets evaluated β€” and any locales in that list get loaded now, if they weren't loaded already.

🌟 ::msgcat::mcpreferences ?locale preference? ...

no arguments? you get an ordered list of the locales the user prefers, sorted from most specific to least preference 🌈.

you can also hand it a set of locale preferences to set the list of locale preferences. doing that also sets the current locale β€” which is the first element of the locale preferences list.

locale preferences get loaded right now, if they weren't loaded already.

for example, maybe the user prefers French or English text. configure it like this:

::msgcat::mcpreferences fr en {}

🌟 ::msgcat::mcloadedlocales subcommand

this little group of commands manages the list of loaded locales, specifically for packages that aren't setting a package locale.

  • loaded β€” returns the list of currently loaded locales.
  • clear β€” removes all locales and their data that are not in the current preference list. goodbye bestie 🧹

🌟 ::msgcat::mcload dirname

this one goes hunting πŸ”. it searches the specified directory for files matching the language specifications returned by ::msgcat::mcloadedlocales loaded β€” or msgcat::mcpackagelocale preferences if a package locale is set. (heads up, these are all lowercase!) then it tacks on the file extension β€œ.msg”.

each matching file gets read in order, assuming a UTF-8 encoding, and the file contents are then evaluated as a Tcl script. what does that mean for you? it means Unicode characters can show up in your message file either directly in their UTF-8 encoded form, or via the backslash-u quoting that Tcl evaluation recognizes. either way works ✨. and the number of message files that matched the spec and got loaded is what you get back.

bonus: the given folder is also stored in the msgcat package configuration option mcfolder, so that message catalog files required by a locale change can eventually be loaded. future-proofing, we love to see it πŸ’–

🌟 ::msgcat::mcset locale src-string ?translate-string?

sets the translation for src-string to translate-string in the specified locale and the current namespace. skip translate-string and src-string gets used for both. returns translate-string.

🌟 ::msgcat::mcmset locale src-trans-list

the multitasker πŸ’…. sets the translation for multiple source strings in src-trans-list, in the specified locale and the current namespace. src-trans-list must have an even number of elements and looks like {src-string translate-string ?src-string translate-string ...?}.

and honestly? ::msgcat::mcmset can be significantly faster than calling ::msgcat::mcset over and over. it returns the number of translations set. efficiency slay πŸ”₯

🌟 ::msgcat::mcflset src-string ?translate-string?

sets the translation for src-string to translate-string in the current namespace, for the locale implied by the name of the message catalog being loaded via ::msgcat::mcload. skip translate-string and src-string is used for both. returns translate-string.

🌟 ::msgcat::mcflmset src-trans-list

the batch version of mcflset πŸ¦‹. sets the translation for multiple source strings in src-trans-list, in the current namespace, for the locale implied by the name of the message catalog being loaded via ::msgcat::mcload. src-trans-list must have an even number of elements and looks like {src-string translate-string ?src-string translate-string ...?}.

::msgcat::mcflmset can be significantly faster than multiple invocations of ::msgcat::mcflset, and it returns the number of translations set.

🌟 ::msgcat::mcunknown locale src-string ?arg arg ...?

this is the fallback bestie πŸ₯². ::msgcat::mc calls this routine whenever a translation for src-string isn't defined in the current locale.

the default action is to return src-string β€” passed through format if there are any arguments. but you're allowed to redefine this procedure in your app, for example to log error messages for every unknown string you bump into.

::msgcat::mcunknown is invoked at the same stack context as the call to ::msgcat::mc, and whatever it returns becomes the return value for that call to ::msgcat::mc.

heads up: this routine is only called if the concerned package did not set a package locale unknown command name. iykyk πŸ‘€

🌟 ::msgcat::mcforgetpackage

the calling package wipes all its state inside the msgcat package β€” all settings and all translations. full factory reset, no leftovers 🧼

🌟 ::msgcat::mcutil getpreferences locale

returns the preferences list of the given locale, exactly as described in the LOCALE SPECIFICATION section.

here's a cute example β€” composing a preference list for the bilingual region "Biel/Bienne" by concatenating swiss german and swiss french:

% concat [lrange [msgcat::mcutil getpreferences fr_CH] 0 end-1] [msgcat::mcutil getpreferences de_CH]
fr_ch fr de_ch de {}

🌟 ::msgcat::mcutil getsystemlocale

returns the system locale, as described by the LOCALE SPECIFICATION section. that's the whole job πŸŽ€

✨ LOCALE SPECIFICATION ✨

Okay bestie, msgcat gets its locale from a locale string that you pass to ::msgcat::mclocale. That string is built from a language code, an optional country code, and an optional system-specific code, each one separated by _. The country and language codes are defined by ISO-639 and ISO-3166. So for example, locale en is English and en_US is U.S. English. πŸ’–

When the msgcat package first loads, the locale is initialized from the user's environment. It looks at env(LC_ALL), env(LC_MESSAGES), and env(LANG) in that order. The first one that has a non-empty value is used to determine the initial locale. That value gets parsed according to the XPG4 pattern:

language[_country][.codeset][@modifier]

to pull out its parts. Then the initial locale is set by calling ::msgcat::mclocale with the argument:

language[_country][_modifier]

On Windows and Cygwin, if none of those environment variables is set, msgcat will try to extract locale info from the registry. The RFC4747 locale name "lang-script-country-options" is transformed to the locale "lang_country_script" (example: sr-Latn-CS -> sr_cs_latin). If all these attempts to discover an initial locale from the user's environment fail, msgcat defaults to an initial locale of "C". 😭

When a locale is specified by the user, a "best match" search is performed during string translation. For example, if a user specifies en_GB_Funky, the locales en_gb_funky, en_gb, en and "" (the empty string) are searched in order until a matching translation string is found. If no translation string is available, then the unknown handler is called. πŸ”₯


πŸ¦‹ NAMESPACES AND MESSAGE CATALOGS πŸ¦‹

Strings stored in the message catalog are stored relative to the namespace from which they were added. This lets multiple packages use the same strings without fear of collisions with other packages. It also lets the source string be shorter and less prone to typographical error. Periodt.

For example, executing the code:

::msgcat::mcset en hello "hello from ::"
namespace eval foo {
    ::msgcat::mcset en hello "hello from ::foo"
}
puts [::msgcat::mc hello]
namespace eval foo {puts [::msgcat::mc hello]}

will print:

hello from ::
hello from ::foo

When searching for a translation of a message, the message catalog will search first the current namespace, then the parent of the current namespace, and so on until the global namespace is reached. This allows child namespaces to "inherit" messages from their parent namespace. πŸ‘‘

For example, executing (in the "en" locale) the code:

::msgcat::mcset en m1 ":: message1"
::msgcat::mcset en m2 ":: message2"
::msgcat::mcset en m3 ":: message3"
namespace eval ::foo {
    ::msgcat::mcset en m2 "::foo message2"
    ::msgcat::mcset en m3 "::foo message3"
}
namespace eval ::foo::bar {
    ::msgcat::mcset en m3 "::foo::bar message3"
}
namespace import ::msgcat::mc
puts "[mc m1]; [mc m2]; [mc m3]"
namespace eval ::foo {puts "[mc m1]; [mc m2]; [mc m3]"}
namespace eval ::foo::bar {puts "[mc m1]; [mc m2]; [mc m3]"}

will print:

:: message1; :: message2; :: message3
:: message1; ::foo message2; ::foo message3
:: message1; ::foo message2; ::foo::bar message3

🌸 LOCATION AND FORMAT OF MESSAGE FILES 🌸

Message files can be located in any directory, subject to the following conditions:

  1. All message files for a package are in the same directory.
  2. The message file name is a msgcat locale specifier (all lowercase) followed by .msg. For example:
es.msg    β€” spanish
en_gb.msg β€” United Kingdom English

Exception: The message file for the root locale "" is called ROOT.msg. This exception is made so as not to cause peculiar behavior, such as marking the message file as "hidden" on Unix file systems. 🫢

  1. The file contains a series of calls to mcflset and mcflmset, setting the necessary translation strings for the language, likely enclosed in a namespace eval so that all source strings are tied to the namespace of the package. For example, a short es.msg might contain:
namespace eval ::mypackage {
    ::msgcat::mcflset "Free Beer" "Cerveza Gratis"
}

If a package is installed into a subdirectory of the tcl_pkgPath and loaded via package require, the following procedure is recommended.

  1. During package installation, create a subdirectory msgs under your package directory.
  2. Copy your *.msg files into that directory.
  3. Add the following command to your package initialization script:
# load language files, stored in msgs subdirectory
::msgcat::mcload [file join [file dirname [info script]] msgs]

πŸ’… POSITIONAL CODES FOR FORMAT AND SCAN COMMANDS πŸ’…

It is possible that a message string used as an argument to format might have positionally dependent parameters that might need to be repositioned. For example, it might be syntactically desirable to rearrange the sentence structure while translating.

format "We produced %d units in location %s" $num $city
format "In location %s we produced %d units" $city $num

This can be handled by using the positional parameters:

format "We produced %1\$d units in location %2\$s" $num $city
format "In location %2\$s we produced %1\$d units" $num $city

Similarly, positional parameters can be used with scan to extract values from internationalized strings. Note that it is not necessary to pass the output of ::msgcat::mc to format directly; by passing the values to substitute in as arguments, the formatting substitution is done directly.

msgcat::mc {Produced %1$d at %2$s} $num $city
# ... where that key is mapped to one of the
# human-oriented versions by msgcat::mcset

πŸ‘‘ PACKAGE PRIVATE LOCALE πŸ‘‘

A package using msgcat may choose to use its own package private locale and its own set of loaded locales, independent to the global locale set by ::msgcat::mclocale.

This allows a package to change its locale without causing any locales load or removal in other packages and not to invoke the global locale change callback (see below).

This action is controled by the following ensemble:

  • ::msgcat::mcpackagelocale set ?locale? β€” Set or change a package private locale. The package private locale is set to the given locale if the locale is given. If the option locale is not given, the package is set to package private locale mode, but no locale is changed (e.g. if the global locale was valid for the package before, it is copied to the package private locale).

This command may cause the load of locales. ✨

  • ::msgcat::mcpackagelocale get β€” Return the package private locale or the global locale, if no package private locale is set.
  • ::msgcat::mcpackagelocale preferences ?locale preference? ... β€” With no parameters, return the package private preferences or the global preferences, if no package private locale is set. The package locale state (set or not) is not changed (in contrast to the command ::msgcat::mcpackagelocale set).

If a set of locale preferences is given, it is set as package 2 locale preference list. The package locale is set to the first 2 element of the preference list. A package locale is activated, 2 if it was not set so far. 2

Locale preferences are loaded now for the package, if not yet 2 loaded.

  • ::msgcat::mcpackagelocale loaded β€” Return the list of locales loaded for this package.
  • ::msgcat::mcpackagelocale isset β€” Returns true, if a package private locale is set.
  • ::msgcat::mcpackagelocale unset β€” Unset the package private locale and use the global locale. Load and remove locales to adjust the list of loaded locales for the package to the global loaded locales list.
  • ::msgcat::mcpackagelocale present locale β€” Returns true, if the given locale is loaded for the package.
  • ::msgcat::mcpackagelocale clear β€” Clear any loaded locales of the package not present in the package preferences.

πŸŽ€ CHANGING PACKAGE OPTIONS πŸŽ€

Okay so every package that uses msgcat has its own lil set of options living inside msgcat, bestie. Those options are described in the next section, Package options. And you can set or unset each one individually with this ensemble:

::msgcat::mcpackageconfig get option

Returns the current value of the given option. Heads up though β€” this call throws an error if the option isn't set for the package. πŸ’€

::msgcat::mcpackageconfig isset option

Returns 1 if the given option is set for the package, 0 otherwise.

::msgcat::mcpackageconfig set option value

Sets the given option to the given value. Depending on the option, this may kick off extra actions. The return value is 0, or β€” for the mcfolder option β€” the number of loaded packages. ✨

::msgcat::mcpackageconfig unset option

Unsets the given option for the package. If the option wasn't set for the package, literally nothing happens, no drama. Returns the empty string.


🧺 Package options

These are the package options available for each package:

  • mcfolder β€” this is the message folder of the package. It gets set by mcload and by the set subcommand. The two are identical, and both return the number of loaded message catalog files.

Setting or changing this value loads all the locales contained in the preferences that are valid for the package. That also means it invokes any loadcmd you've set (more on that below πŸ‘‡).

Unsetting this value disables message file loading for the package.

  • loadcmd β€” this callback is invoked before a set of message catalog files gets loaded for the package that has this property set.

You can use this callback to do any prep work for message file loading, or to pull your message data from a whole other source, like a database. In that case, no message files are used at all (mcfolder is unset).

See the callback invocation section below. The parameter list appended to this callback is the list of locales to load.

If this callback is changed, it is called with the preferences valid for the package.

  • changecmd β€” this callback fires when a default locale change was performed. Its whole purpose is to let a package update anything that depends on the default locale β€” like, say, showing the GUI in another language. 🌍

See the callback invocation section below. The parameter list appended to this callback is mcpreferences. The registered callbacks are invoked in no particular order.

  • unknowncmd β€” use a package locale mcunknown procedure instead of the standard version the msgcat package supplies (msgcat::mcunknown).

The procedure that gets called must return the formatted message that will finally be returned by msgcat::mc.

A generic unknown handler is used if you set this to the empty string. That handler returns the key if no arguments are given. If arguments are given, the format command is used to process them.

See the callback invocation section below. The appended arguments are identical to msgcat::mcunknown.

πŸ“’ Callback invocation

A package can register one or multiple callbacks, as described above. πŸ‘†

Callbacks get invoked if:

  1. the callback command is set,
  2. the command is not the empty string,
  3. the registering namespace exists.

If a called routine fails with an error, the bgerror routine for the interpreter is invoked after command completion. The one exception is the unknowncmd callback β€” there, an error makes the invoking mc-command fail with that error instead.


πŸ¦‹ OBJECT ORIENTED PROGRAMMING πŸ¦‹

msgcat supports packages implemented with object oriented programming. Objects and classes should be defined within a package namespace. Periodt. πŸ’…

There are 3 supported cases where package-namespace-sensitive commands of msgcat (mc, mcexists, mcpackagelocale, mcforgetpackage, mcpackagenamespaceget, mcpackageconfig, mcset and mcmset) may be called:

  1. In class definition script β€” the msgcat command is called within a class definition script.
namespace eval ::N2 {
    mcload $dir/msgs
    oo::class create C1 {puts [mc Hi!]}
}
  1. method defined in a class β€” the msgcat command is called from a method in an object, and the method is defined in a class.
namespace eval ::N3Class {
    mcload $dir/msgs
    oo::class create C1
    oo::define C1 method m1 {
        puts [mc Hi!]
    }
}
  1. method defined in a classless object β€” the msgcat command is called from a method of a classless object.
namespace eval ::N4 {
    mcload $dir/msgs
    oo::object create O1
    oo::objdefine O1 method m1 {} {
        puts [mc Hi!]
    }
}

✨ EXAMPLES ✨

Packages that display a GUI might want to update their widgets when the global locale changes. To register to a callback, use:

namespace eval gui {
    msgcat::mcpackageconfig changecmd updateGUI

    proc updateGUI args {
        puts "New locale is '[lindex $args 0]'."
    }
}
% msgcat::mclocale fr
fr
% New locale is 'fr'.

If locales (or additional locales) live in another source, like a database, a package can use the load callback and skip mcload entirely:

namespace eval db {
    msgcat::mcpackageconfig loadcmd loadMessages

    proc loadMessages args {
        foreach locale $args {
            if {[LocaleInDB $locale]} {
                msgcat::mcmset $locale [GetLocaleList $locale]
            }
        }
    }
}

The clock command implementation uses msgcat with a package locale to implement the command line parameter -locale. Here are some sketches of the implementation:

First, a package locale is initialized and the generic unknown function is deactivated:

msgcat::mcpackagelocale set
msgcat::mcpackageconfig unknowncmd ""

As an example, the user requires the week day in a certain locale as follows:

clock format [clock seconds] -format %A -locale fr

clock sets the package locale to fr and looks for the day name as follows:

msgcat::mcpackagelocale set $locale
return [lindex [msgcat::mc DAYS_OF_WEEK_FULL] $day]
### Returns "mercredi"

Within clock, some message-catalog items are heavy in computation and thus are dynamically cached using:

proc ::tcl::clock::LocalizeFormat { locale format } {
    set key FORMAT_$format
    if { [::msgcat::mcexists -exactlocale -exactnamespace $key] } {
        return [mc $key]
    }
    #...expensive computation of format clipped...
    mcset $locale $key $format
    return $format
}

πŸ’– CREDITS πŸ’–

The message catalog code was developed by Mark Harrison. Iconic behaviour, honestly. 🫢


πŸ‘€ SEE ALSO πŸ‘€

format(n), scan(n), namespace(n), package(n), oo::class(n), oo::object


πŸ”‘ KEYWORDS πŸ”‘

internationalization, i18n, localization, l10n, message, text, translation, class, object


And that's the whole page, bestie!! πŸŽ€ This one is msgcat version 1.5, straight from the msgcat(n) manual, dated as it says up top. Go forth and internationalize, you absolute main character. πŸ’…βœ¨πŸŒˆ

πŸ“¦ where this page comes from