// 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. // // systemLang(), which this file calls to pick the default language, is // the one OS-specific piece — implemented in process_windows.go/ // process_linux.go, not here, since this file's own job (embedding and // picking a dictionary) doesn't depend on the OS at all. package main import ( "embed" "encoding/json" "log" "path" "sort" "strings" ) //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 } // resolveLang decides the final language. "auto" (or empty) uses the // system'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 }