Files

120 lines
3.7 KiB
Go

// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
// 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))
}