Reads the game's process memory read-only (AOB signature scanning, re-resolved every tick — never a cached pointer) and serves a live death counter as a browser-source overlay plus a status panel. Supports co-op: each player runs the program locally, one acts as hub. The peer link is authenticated (HMAC challenge/response, no token on the wire, constant-time compare) but not yet encrypted — see CLAUDE.md's TODO section for the planned TLS+pinning split. Zero external dependencies — including a hand-rolled WebSocket implementation — so the whole thing ships as one .exe.
123 lines
3.0 KiB
Go
123 lines
3.0 KiB
Go
//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
|
|
}
|