Files
deathwatch/i18n.go
T
emmatherock 5a2c3272e9 Translate remaining Go source and overlay.html comments to English
Identifiers, comments, and log/console messages in auth.go, i18n.go,
counter.go, totals.go, and overlay.html's script/CSS comments — the
files the TLS commit didn't already touch. No functional changes.

locales/*.json and overlay.html's own FALLBACK/data-i18n strings stay
bilingual on purpose: that's end-user text for the overlay/panel, a
different audience than the source code, served by the existing i18n
system rather than this convention.
2026-09-17 21:51:33 -03:00

123 lines
3.0 KiB
Go

//go:build windows
// i18n.go: interface languages.
//
// Translations live in locales/*.json and get embedded into the binary.
// The reason for separate files instead of a map in the code is so
// adding a language is "copy en.json, translate it, done": whoever
// translates doesn't need to know Go or touch a line of code.
//
// Console messages deliberately do NOT go through here: they're
// diagnostics, and it helps if they're always in the same language so a
// log pasted into an issue reads the same no matter where it came from.
package main
import (
"embed"
"encoding/json"
"log"
"path"
"sort"
"strings"
"syscall"
"unsafe"
)
//go:embed locales/*.json
var localeFS embed.FS
const fallbackLang = "en"
var locales = map[string]map[string]string{}
func loadLocales() {
entries, err := localeFS.ReadDir("locales")
if err != nil {
log.Printf("couldn't read the embedded languages: %v", err)
return
}
for _, e := range entries {
name := e.Name()
if !strings.HasSuffix(name, ".json") {
continue
}
data, err := localeFS.ReadFile(path.Join("locales", name))
if err != nil {
log.Printf("couldn't read locales/%s: %v", name, err)
continue
}
var m map[string]string
if err := json.Unmarshal(data, &m); err != nil {
log.Printf("locales/%s has invalid JSON: %v", name, err)
continue
}
locales[strings.TrimSuffix(name, ".json")] = m
}
}
func availableLangs() []string {
out := make([]string, 0, len(locales))
for k := range locales {
out = append(out, k)
}
sort.Strings(out)
return out
}
var procGetUserDefaultLocaleName = kernel32.NewProc("GetUserDefaultLocaleName")
// systemLang returns Windows's language ("es-AR" -> "es").
func systemLang() string {
buf := make([]uint16, 85) // LOCALE_NAME_MAX_LENGTH
r, _, _ := procGetUserDefaultLocaleName.Call(uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
if r == 0 {
return ""
}
name := syscall.UTF16ToString(buf[:r])
if base, _, ok := strings.Cut(name, "-"); ok {
return strings.ToLower(base)
}
return strings.ToLower(name)
}
// resolveLang decides the final language. "auto" (or empty) uses
// Windows's; if that language isn't translated, it falls back to English.
func resolveLang(want string) string {
want = strings.ToLower(strings.TrimSpace(want))
if want == "" || want == "auto" {
want = systemLang()
}
if want == "" {
return fallbackLang
}
if _, ok := locales[want]; ok {
return want
}
// "es-AR" or "es_AR" should still resolve to "es".
if base, _, ok := strings.Cut(strings.NewReplacer("_", "-").Replace(want), "-"); ok {
if _, ok := locales[base]; ok {
return base
}
}
return fallbackLang
}
// stringsFor returns the requested language's dictionary, filled in with
// English: if a translation is incomplete, the missing key shows up in
// English instead of as a raw key.
func stringsFor(lang string) map[string]string {
out := map[string]string{}
for k, v := range locales[fallbackLang] {
out[k] = v
}
if lang != fallbackLang {
for k, v := range locales[lang] {
if v != "" {
out[k] = v
}
}
}
out["_lang"] = lang
return out
}