Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
package i18n
// Stable machine codes that must never be translated when used as the JSON
// "error" / "code" field value. Clients branch on these exact strings.
var stableCodes = map[string]struct{}{
"password_not_set": {},
"email_mismatch": {},
"maintenance": {},
"read_only": {},
"already_claimed": {},
"not_claimable": {},
"invalid_credentials": {},
"user_already_exists": {},
"password_too_short": {},
"register_fields_required": {},
}
// IsStableCode reports whether msg is a machine-stable error token.
func IsStableCode(msg string) bool {
_, ok := stableCodes[msg]
return ok
}
// T returns msg translated for locale. Missing entries fall back to English msg.
// Stable machine codes are returned unchanged.
func T(locale, msg string) string {
if msg == "" || IsStableCode(msg) {
return msg
}
lang := Normalize(locale)
if lang == Default {
return msg
}
if pack, ok := catalogs[lang]; ok {
if translated, ok := pack[msg]; ok && translated != "" {
return translated
}
}
return msg
}
// catalogs maps locale → (English source message → translation).
// English is the identity key; add entries here when introducing new public copy.
var catalogs = map[string]map[string]string{
"es": esMessages,
"fr": frMessages,
"de": deMessages,
"it": itMessages,
"pt": ptMessages,
"nl": nlMessages,
"pl": plMessages,
"ja": jaMessages,
}
+125
View File
@@ -0,0 +1,125 @@
package i18n
import (
"context"
"strconv"
"strings"
)
// Default is the fallback UI/API locale when Accept-Language is missing or unsupported.
const Default = "en"
// Supported UI/API locales for public error/validation copy.
// Keep aligned with apps/web/src/lib/i18n/locales.ts (UI_LOCALES).
var Supported = []string{"en", "es", "fr", "de", "it", "pt", "nl", "pl", "ja"}
var supportedSet map[string]struct{}
func init() {
supportedSet = make(map[string]struct{}, len(Supported))
for _, code := range Supported {
supportedSet[code] = struct{}{}
}
}
type ctxKey struct{}
// WithLocale stores a resolved locale on ctx.
func WithLocale(ctx context.Context, locale string) context.Context {
return context.WithValue(ctx, ctxKey{}, Normalize(locale))
}
// FromContext returns the locale stored by middleware, or Default.
func FromContext(ctx context.Context) string {
if ctx == nil {
return Default
}
if v, ok := ctx.Value(ctxKey{}).(string); ok && v != "" {
return v
}
return Default
}
// Normalize lowercases/trims and maps to a supported primary language tag, or Default.
func Normalize(raw string) string {
code := strings.ToLower(strings.TrimSpace(raw))
if code == "" || code == "*" {
return Default
}
if i := strings.IndexByte(code, '-'); i > 0 {
code = code[:i]
}
if i := strings.IndexByte(code, '_'); i > 0 {
code = code[:i]
}
if _, ok := supportedSet[code]; ok {
return code
}
return Default
}
// IsSupported reports whether the primary language tag is in Supported.
func IsSupported(raw string) bool {
code := strings.ToLower(strings.TrimSpace(raw))
if code == "" {
return false
}
if i := strings.IndexByte(code, '-'); i > 0 {
code = code[:i]
}
if i := strings.IndexByte(code, '_'); i > 0 {
code = code[:i]
}
_, ok := supportedSet[code]
return ok
}
// Resolve picks the best supported locale from an Accept-Language header value.
// Quality values are respected; unsupported tags are skipped; empty → Default.
func Resolve(acceptLanguage string) string {
header := strings.TrimSpace(acceptLanguage)
if header == "" {
return Default
}
bestTag := ""
bestQ := -1.0
for _, part := range strings.Split(header, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
tag := part
q := 1.0
if i := strings.IndexByte(part, ';'); i >= 0 {
tag = strings.TrimSpace(part[:i])
for _, p := range strings.Split(part[i+1:], ";") {
p = strings.TrimSpace(p)
if len(p) >= 2 && (p[0] == 'q' || p[0] == 'Q') && p[1] == '=' {
if parsed, err := strconv.ParseFloat(strings.TrimSpace(p[2:]), 64); err == nil {
q = parsed
}
}
}
}
primary := strings.ToLower(strings.TrimSpace(tag))
if primary == "*" {
if q > bestQ {
bestQ = q
bestTag = Default
}
continue
}
if !IsSupported(primary) {
continue
}
norm := Normalize(primary)
if q > bestQ {
bestQ = q
bestTag = norm
}
}
if bestTag == "" {
return Default
}
return bestTag
}
+59
View File
@@ -0,0 +1,59 @@
package i18n
import "testing"
func TestResolveAcceptLanguage(t *testing.T) {
t.Parallel()
cases := []struct {
in string
want string
}{
{"", Default},
{"en", "en"},
{"nl-NL,nl;q=0.9,en;q=0.8", "nl"},
{"fr-CA,en;q=0.5", "fr"},
{"xx-YY,en;q=0.1", "en"},
{"de;q=0.2,pl;q=0.9", "pl"},
{"*;q=0.1", "en"},
}
for _, tc := range cases {
if got := Resolve(tc.in); got != tc.want {
t.Fatalf("Resolve(%q)=%q want %q", tc.in, got, tc.want)
}
}
}
func TestTFallsBackAndSkipsStableCodes(t *testing.T) {
t.Parallel()
if got := T("nl", "unauthorized"); got != "niet geautoriseerd" {
t.Fatalf("nl unauthorized=%q", got)
}
if got := T("nl", "password_not_set"); got != "password_not_set" {
t.Fatalf("stable code translated: %q", got)
}
if got := T("nl", "some unknown message"); got != "some unknown message" {
t.Fatalf("missing key should stay English: %q", got)
}
if got := T("en", "unauthorized"); got != "unauthorized" {
t.Fatalf("en identity=%q", got)
}
}
func TestSupportedMatchesUILocales(t *testing.T) {
t.Parallel()
want := []string{"en", "es", "fr", "de", "it", "pt", "nl", "pl", "ja"}
if len(Supported) != len(want) {
t.Fatalf("Supported len=%d want %d", len(Supported), len(want))
}
for i, code := range want {
if Supported[i] != code {
t.Fatalf("Supported[%d]=%q want %q", i, Supported[i], code)
}
if !IsSupported(code) {
t.Fatalf("IsSupported(%q)=false", code)
}
}
if IsSupported("xx") {
t.Fatal("xx must not be supported")
}
}
+238
View File
@@ -0,0 +1,238 @@
package i18n
// Locale message packs (English source string → translation).
// Missing entries fall back to the English source via T. Keep keys identical
// to the English public Error()/CodedError message text. Stable machine codes
// (password_not_set, …) must not appear here — IsStableCode leaves them alone.
var esMessages = map[string]string{
"unauthorized": "no autorizado",
"Unauthorized": "No autorizado",
"forbidden": "prohibido",
"not found": "no encontrado",
"invalid api key": "clave API no válida",
"invalid credentials": "credenciales no válidas",
"invalid json": "JSON no válido",
"invalid email": "correo no válido",
"user not found": "usuario no encontrado",
"company not found": "empresa no encontrada",
"company required": "se requiere empresa",
"method not allowed": "método no permitido",
"rate limit exceeded": "límite de velocidad superado",
"csrf token mismatch": "token CSRF no coincide",
"login failed": "error al iniciar sesión",
"logout failed": "error al cerrar sesión",
"Authentication failed": "Error de autenticación",
"admin required": "se requiere administrador",
"platform admin required": "se requiere administrador de plataforma",
"database unavailable": "base de datos no disponible",
"auth unavailable": "autenticación no disponible",
"body too large": "cuerpo demasiado grande",
"user already exists": "el usuario ya existe",
"password must be at least 8 characters": "la contraseña debe tener al menos 8 caracteres",
"invite invalid or expired": "invitación no válida o caducada",
"unsupported language": "idioma no admitido",
}
var frMessages = map[string]string{
"unauthorized": "non autorisé",
"Unauthorized": "Non autorisé",
"forbidden": "interdit",
"not found": "introuvable",
"invalid api key": "clé API invalide",
"invalid credentials": "identifiants invalides",
"invalid json": "JSON invalide",
"invalid email": "e-mail invalide",
"user not found": "utilisateur introuvable",
"company not found": "entreprise introuvable",
"company required": "entreprise requise",
"method not allowed": "méthode non autorisée",
"rate limit exceeded": "limite de débit dépassée",
"csrf token mismatch": "jeton CSRF non concordant",
"login failed": "échec de la connexion",
"logout failed": "échec de la déconnexion",
"Authentication failed": "Échec de l'authentification",
"admin required": "administrateur requis",
"platform admin required": "administrateur de plateforme requis",
"database unavailable": "base de données indisponible",
"auth unavailable": "authentification indisponible",
"body too large": "corps trop volumineux",
"user already exists": "l'utilisateur existe déjà",
"password must be at least 8 characters": "le mot de passe doit contenir au moins 8 caractères",
"invite invalid or expired": "invitation invalide ou expirée",
"unsupported language": "langue non prise en charge",
}
var deMessages = map[string]string{
"unauthorized": "nicht autorisiert",
"Unauthorized": "Nicht autorisiert",
"forbidden": "verboten",
"not found": "nicht gefunden",
"invalid api key": "ungültiger API-Schlüssel",
"invalid credentials": "ungültige Anmeldedaten",
"invalid json": "ungültiges JSON",
"invalid email": "ungültige E-Mail",
"user not found": "Benutzer nicht gefunden",
"company not found": "Unternehmen nicht gefunden",
"company required": "Unternehmen erforderlich",
"method not allowed": "Methode nicht erlaubt",
"rate limit exceeded": "Ratenlimit überschritten",
"csrf token mismatch": "CSRF-Token stimmt nicht überein",
"login failed": "Anmeldung fehlgeschlagen",
"logout failed": "Abmeldung fehlgeschlagen",
"Authentication failed": "Authentifizierung fehlgeschlagen",
"admin required": "Admin erforderlich",
"platform admin required": "Plattform-Admin erforderlich",
"database unavailable": "Datenbank nicht verfügbar",
"auth unavailable": "Authentifizierung nicht verfügbar",
"body too large": "Anfragetext zu groß",
"user already exists": "Benutzer existiert bereits",
"password must be at least 8 characters": "Passwort muss mindestens 8 Zeichen haben",
"invite invalid or expired": "Einladung ungültig oder abgelaufen",
"unsupported language": "nicht unterstützte Sprache",
}
var itMessages = map[string]string{
"unauthorized": "non autorizzato",
"Unauthorized": "Non autorizzato",
"forbidden": "vietato",
"not found": "non trovato",
"invalid api key": "chiave API non valida",
"invalid credentials": "credenziali non valide",
"invalid json": "JSON non valido",
"invalid email": "email non valida",
"user not found": "utente non trovato",
"company not found": "azienda non trovata",
"company required": "azienda richiesta",
"method not allowed": "metodo non consentito",
"rate limit exceeded": "limite di frequenza superato",
"csrf token mismatch": "token CSRF non corrispondente",
"login failed": "accesso non riuscito",
"logout failed": "disconnessione non riuscita",
"Authentication failed": "Autenticazione non riuscita",
"admin required": "amministratore richiesto",
"platform admin required": "amministratore della piattaforma richiesto",
"database unavailable": "database non disponibile",
"auth unavailable": "autenticazione non disponibile",
"body too large": "corpo troppo grande",
"user already exists": "l'utente esiste già",
"password must be at least 8 characters": "la password deve avere almeno 8 caratteri",
"invite invalid or expired": "invito non valido o scaduto",
"unsupported language": "lingua non supportata",
}
var ptMessages = map[string]string{
"unauthorized": "não autorizado",
"Unauthorized": "Não autorizado",
"forbidden": "proibido",
"not found": "não encontrado",
"invalid api key": "chave API inválida",
"invalid credentials": "credenciais inválidas",
"invalid json": "JSON inválido",
"invalid email": "e-mail inválido",
"user not found": "utilizador não encontrado",
"company not found": "empresa não encontrada",
"company required": "empresa obrigatória",
"method not allowed": "método não permitido",
"rate limit exceeded": "limite de taxa excedido",
"csrf token mismatch": "token CSRF não coincide",
"login failed": "falha no início de sessão",
"logout failed": "falha ao terminar sessão",
"Authentication failed": "Falha de autenticação",
"admin required": "administrador obrigatório",
"platform admin required": "administrador da plataforma obrigatório",
"database unavailable": "base de dados indisponível",
"auth unavailable": "autenticação indisponível",
"body too large": "corpo demasiado grande",
"user already exists": "o utilizador já existe",
"password must be at least 8 characters": "a palavra-passe deve ter pelo menos 8 caracteres",
"invite invalid or expired": "convite inválido ou expirado",
"unsupported language": "idioma não suportado",
}
var nlMessages = map[string]string{
"unauthorized": "niet geautoriseerd",
"Unauthorized": "Niet geautoriseerd",
"forbidden": "verboden",
"not found": "niet gevonden",
"invalid api key": "ongeldige API-sleutel",
"invalid credentials": "ongeldige inloggegevens",
"invalid json": "ongeldige JSON",
"invalid email": "ongeldig e-mailadres",
"user not found": "gebruiker niet gevonden",
"company not found": "bedrijf niet gevonden",
"company required": "bedrijf verplicht",
"method not allowed": "methode niet toegestaan",
"rate limit exceeded": "limiet overschreden",
"csrf token mismatch": "CSRF-token komt niet overeen",
"login failed": "inloggen mislukt",
"logout failed": "uitloggen mislukt",
"Authentication failed": "Authenticatie mislukt",
"admin required": "beheerder vereist",
"platform admin required": "platformbeheerder vereist",
"database unavailable": "database niet beschikbaar",
"auth unavailable": "authenticatie niet beschikbaar",
"body too large": "body te groot",
"user already exists": "gebruiker bestaat al",
"password must be at least 8 characters": "wachtwoord moet minstens 8 tekens hebben",
"invite invalid or expired": "uitnodiging ongeldig of verlopen",
"unsupported language": "niet-ondersteunde taal",
}
var plMessages = map[string]string{
"unauthorized": "nieautoryzowany",
"Unauthorized": "Nieautoryzowany",
"forbidden": "zabronione",
"not found": "nie znaleziono",
"invalid api key": "nieprawidłowy klucz API",
"invalid credentials": "nieprawidłowe dane logowania",
"invalid json": "nieprawidłowy JSON",
"invalid email": "nieprawidłowy e-mail",
"user not found": "nie znaleziono użytkownika",
"company not found": "nie znaleziono firmy",
"company required": "wymagana firma",
"method not allowed": "metoda niedozwolona",
"rate limit exceeded": "przekroczono limit żądań",
"csrf token mismatch": "token CSRF nie pasuje",
"login failed": "logowanie nie powiodło się",
"logout failed": "wylogowanie nie powiodło się",
"Authentication failed": "Uwierzytelnianie nie powiodło się",
"admin required": "wymagany administrator",
"platform admin required": "wymagany administrator platformy",
"database unavailable": "baza danych niedostępna",
"auth unavailable": "uwierzytelnianie niedostępne",
"body too large": "ciało żądania zbyt duże",
"user already exists": "użytkownik już istnieje",
"password must be at least 8 characters": "hasło musi mieć co najmniej 8 znaków",
"invite invalid or expired": "zaproszenie nieprawidłowe lub wygasłe",
"unsupported language": "nieobsługiwany język",
}
var jaMessages = map[string]string{
"unauthorized": "認証されていません",
"Unauthorized": "認証されていません",
"forbidden": "禁止されています",
"not found": "見つかりません",
"invalid api key": "無効なAPIキー",
"invalid credentials": "無効な認証情報",
"invalid json": "無効なJSON",
"invalid email": "無効なメールアドレス",
"user not found": "ユーザーが見つかりません",
"company not found": "会社が見つかりません",
"company required": "会社が必要です",
"method not allowed": "許可されていないメソッド",
"rate limit exceeded": "レート制限を超えました",
"csrf token mismatch": "CSRFトークンが一致しません",
"login failed": "ログインに失敗しました",
"logout failed": "ログアウトに失敗しました",
"Authentication failed": "認証に失敗しました",
"admin required": "管理者が必要です",
"platform admin required": "プラットフォーム管理者が必要です",
"database unavailable": "データベースを利用できません",
"auth unavailable": "認証を利用できません",
"body too large": "リクエスト本文が大きすぎます",
"user already exists": "ユーザーは既に存在します",
"password must be at least 8 characters": "パスワードは8文字以上である必要があります",
"invite invalid or expired": "招待が無効または期限切れです",
"unsupported language": "サポートされていない言語",
}