Documentation
¶
Overview ¶
Package i18n implements message catalog translation for every user-facing string in routercli: login prompts, command descriptions, and runtime messages. A catalog is a flat YAML file, one per language, keyed by a short language code taken from its filename, so var/lang/en.yaml becomes "en".
This does not use golang.org/x/text/message. That package is built around ICU-style plural and gender rules and compiled message catalogs, more machinery than a CLI's short, mostly static strings need. A flat map of key to string, with printf-style substitution, covers everything a routercli message actually requires, stays trivial to read and hand-edit, matching var/tree/tree_structure.yaml's own philosophy, and keeps the dependency footprint down.
Two Ways A String Can Reach The User ¶
Every user-facing string in this project takes one of two forms. A literal string, written directly in Go or in a tree YAML file's plain desc field, is never translated. What is written is exactly what a user sees, in every language. A catalog lookup, through Translator.T in Go, or through a tree YAML file's desc_key, help_key, or arghelp_key field instead of desc, help, or arghelp, resolves against whichever catalog is currently active, see Catalog Lookup below.
A tree file entry can set both a literal field and its key counterpart, see Command.ResolvedDesc in package command. When both are set, the key wins whenever a Translator is available. The plain literal is used only as a fallback if translation was never wired up at all, a nil Translator, see T's own doc comment. Every command file this project ships uses the key form throughout, to demonstrate the full translation path end to end.
Catalog Lookup ¶
T looks up a key in the current language, falls back to the default language, and beyond that falls back to the literal key itself, printed in double brackets, for example "[[show.desc]]". Seeing that bracketed form in a running CLI means exactly one thing: no catalog, in any loaded language, has an entry for that key. Check var/lang/<code>.yaml for a typo, or a line that was simply never added.
The Cost Of A Translation Key ¶
Using a key such as desc_key means a translated string exists in two places at once: the key name itself, invented in the tree YAML file, and the actual English text in var/lang/en.yaml, keyed by that same name. Every other language's catalog then needs its own entry, keyed the same way, with its own translated text. This is real overhead for the default language specifically, since English gains nothing from being looked up through a key rather than written literally.
The alternative, skipping the key and letting a catalog key its own entries by the literal English text itself, avoids inventing a key name, but does not remove the requirement that some catalog contain that exact string for T to find. Without a key at all, ResolvedDesc never calls T in the first place, see command.Command.ResolvedDesc, so a command with only a plain desc field is never translatable no matter what another catalog contains. There is no way to make English free and still have that string be translatable later. The choice is between naming a key now, or committing to the literal English text as the permanent key later. This project uses named keys, show.desc rather than the literal sentence itself, because a short, meaningful key survives an English wording change without invalidating every other language's catalog entry. Reword var/lang/en.yaml's show.desc line, and every other language's show.desc entry is still correctly linked to the updated English meaning. Keying by literal text would break that link the moment the English wording changed at all.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func LoadCatalogs ¶
LoadCatalogs - This function reads every *.yaml file in dir and returns them as a map keyed by language code, taken from each file's base name, so var/lang/en.yaml becomes "en" and var/lang/fr.yaml becomes "fr". Each file is a flat map of key to text, with no nested structure and no wrapper key repeating the language code the filename already states.
A missing directory is not an error. It returns an empty catalog set, the same reasoning as the missing file handling in config.LoadToolConfig. i18n is opt-in, and a project that has not set up var/lang/ yet should still run, with every T() call falling back to its raw key, which is visibly obvious rather than a startup crash over a feature nobody is using yet. A file that exists but fails to parse is a hard error, though, since a malformed YAML catalog is a real mistake worth surfacing immediately rather than silently dropping that language.
Types ¶
type Catalog ¶
Catalog - This type holds one language's worth of translated strings, mapping each key to its text.
type Translator ¶
type Translator struct {
// contains filtered or unexported fields
}
Translator - This type holds every loaded catalog, plus which language is currently active and which one to fall back to. defaultLang is set explicitly by whoever constructs the Translator, main.go, from the config's language directive, rather than guessed from convention, such as the first loaded catalog, so there is never ambiguity about which language text should fall back to.
func New ¶
func New(catalogs map[string]Catalog, lang, defaultLang string) *Translator
New - This function constructs a Translator from a set of already loaded catalogs, see LoadCatalogs, plus which language should be active to start. An unrecognized language code in a config file makes for a much better startup experience if it falls back to defaultLang and starts in English than if it fails outright, so if lang is not among the loaded catalogs, New falls back to defaultLang instead of erroring. A caller that wants to know whether that happened should check CurrentLanguage() against what it asked for.
func (*Translator) AvailableLanguages ¶
func (t *Translator) AvailableLanguages() []string
AvailableLanguages - This method returns every loaded language code, sorted for stable output since Go's map iteration order is randomized, for the "language" command to list and for the error message in SetLanguage.
func (*Translator) CurrentLanguage ¶
func (t *Translator) CurrentLanguage() string
CurrentLanguage - This method returns the active language code.
func (*Translator) SetLanguage ¶
func (t *Translator) SetLanguage(lang string) error
SetLanguage - This method switches the active language, used by the runtime "language set" command, cmd/cmd_language.go. Falling back silently to a default is fine as a constructor default, see New's own doc comment, but it is wrong for an explicit runtime request, where the user needs to be told either that they got what they asked for or why not. So SetLanguage returns an error naming the requested language if it was not loaded, rather than falling back, letting the command handler report a useful message.
func (*Translator) T ¶
func (t *Translator) T(key string, args ...any) string
T - This method looks up key in the current language, falls back to the default language, and falls back to the literal key wrapped in double brackets, "[[key]]", if neither has it, so a missing translation is something a person notices immediately in the CLI's output rather than something they have to go hunting for in a log file. args are applied with fmt.Sprintf if any are given, so a catalog entry can contain a verb such as %s or %d the same way any other Go format string would.
T is safe to call on a nil *Translator, returning the bracketed key as if no catalogs were loaded at all. This lets a caller throughout the project use ctx.Translator.T(...) unconditionally, with no call site needing its own nil check for the valid case where i18n was never wired up at all.