// Fenrir console screen reader. // Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . // Package i18n combines installed and bundled Fenrir gettext catalogs. Screen // contents and internal command/cue IDs must never pass through this package. package i18n import ( "embed" "os" "strconv" "strings" "sync/atomic" "github.com/leonelquinteros/gotext" ) const Domain = "fenrir" const LocaleDir = "/usr/share/locale" //go:embed locale/*/LC_MESSAGES/fenrir.po var bundled embed.FS type Catalog struct{ locales []*gotext.Locale } var current atomic.Pointer[Catalog] // Language follows Python gettext's environment precedence. LANGUAGE may list // several languages separated by colons; C/POSIX stop catalog fallback. func Language() string { for _, name := range []string{"LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"} { if value := os.Getenv(name); value != "" { return value } } return "C" } func New(root, languages string) *Catalog { catalog := &Catalog{} for _, language := range strings.Split(languages, ":") { language = strings.TrimSpace(language) if index := strings.IndexByte(language, '.'); index >= 0 { modifier := "" if at := strings.IndexByte(language[index:], '@'); at >= 0 { modifier = language[index+at:] } language = language[:index] + modifier } if language == "C" || language == "POSIX" { break } if language == "" || strings.ContainsAny(language, "/\\") { continue } locale := gotext.NewLocale(root, language) locale.AddDomain(Domain) catalog.locales = append(catalog.locales, locale) // Keep each language's additions ahead of the next LANGUAGE fallback. // Installed contributor translations retain priority within a language. local := gotext.NewLocaleFSWithPath(language, bundled, "locale") local.AddDomain(Domain) catalog.locales = append(catalog.locales, local) } return catalog } // Use installs an immutable catalog and returns the previous one. Configure // once at startup; no catalog loading belongs on the input/event path. func Use(catalog *Catalog) *Catalog { return current.Swap(catalog) } func T(message string) string { if catalog := current.Load(); catalog != nil { for _, locale := range catalog.locales { if locale.IsTranslated(message) { return locale.Get(message) } } } return message } func N(singular, plural string, count int) string { if catalog := current.Load(); catalog != nil { for _, locale := range catalog.locales { if locale.IsTranslatedN(singular, count) { return locale.GetN(singular, plural, count) } } } if count == 1 { return singular } return plural } // Format substitutes Python-style positional fields after lookup. Keeping // {0}/{1} intact lets the Go reader reuse existing catalog message IDs. func Format(message string, fields ...string) string { replacements := []string{"{{", "{", "}}", "}"} for index, value := range fields { replacements = append(replacements, "{"+strconv.Itoa(index)+"}", value) } return strings.NewReplacer(replacements...).Replace(T(message)) }