//go:build windows // i18n.go: idiomas de la interfaz. // // Las traducciones viven en locales/*.json y se embeben en el ejecutable. // La razon de elegir archivos sueltos en vez de un mapa en el codigo es // que agregar un idioma sea "copiar en.json, traducirlo, y listo": quien // traduce no necesita saber Go ni tocar una linea de codigo. // // Los mensajes de la consola NO pasan por aca a proposito: son // diagnostico, y conviene que esten siempre en el mismo idioma para que // un log pegado en un issue se pueda leer sin importar de donde venga. 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("no pude leer los idiomas embebidos: %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("no pude leer locales/%s: %v", name, err) continue } var m map[string]string if err := json.Unmarshal(data, &m); err != nil { log.Printf("locales/%s tiene JSON invalido: %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 devuelve el idioma de Windows ("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 decide el idioma final. "auto" (o vacio) usa el de // Windows; si ese idioma no esta traducido, cae a ingles. 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" o "es_AR" igual tienen que encontrar "es". if base, _, ok := strings.Cut(strings.NewReplacer("_", "-").Replace(want), "-"); ok { if _, ok := locales[base]; ok { return base } } return fallbackLang } // stringsFor devuelve el diccionario del idioma pedido, completado con // el ingles: si una traduccion quedo a medias, se ve la clave que falta // en ingles y no la clave cruda. 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 }