Weighs the soul of incoming HTTP requests to stop AI crawlers

feat: implement localization system (#716)

* lib/localization: implement localization system

Locale files are placed in lib/localization/locales/. If you add a
locale, update manifest.json with available locales.

* Exclude locales from check spelling

* tests(lib/localization): add comprehensive translations test

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(challenge/metarefresh): enable localization

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix: use simple syntax for localization in templ

Also localize CELPHASE into French according to the wishes of the
artist.

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore:(js): fix forbidden patterns

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: add goi18n to tools

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test(lib/localization): dynamically determine the list of supported languages

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>

authored by Laurent Laffont Xe Iaso and committed by GitHub ad543061 c2423d06

+2
.github/actions/spelling/excludes.txt
··· 89 89 ^lib/policy/config/testdata/bad/unparseable\.json$ 90 90 ignore$ 91 91 robots.txt 92 + ^lib/localization/locales/.*\.json$ 93 + ^lib/localization/.*_test.go$
+3 -1
.github/actions/spelling/expect.txt
··· 33 33 caninetools 34 34 Cardyb 35 35 celchecker 36 - CELPHASE 36 + celphase 37 37 cerr 38 38 certresolver 39 39 cespare ··· 42 42 chainguard 43 43 chall 44 44 challengemozilla 45 + Chargement 45 46 checkpath 46 47 checkresult 47 48 chibi ··· 183 184 nbf 184 185 netsurf 185 186 nginx 187 + nicksnyder 186 188 nobots 187 189 NONINFRINGEMENT 188 190 nosleep
+2
.gitignore
··· 20 20 21 21 # how does this get here 22 22 doc/VERSION 23 + 24 + web/static/locales/*.json
+2
docs/docs/CHANGELOG.md
··· 11 11 12 12 ## [Unreleased] 13 13 14 + - Implement localization system. Find locale files in lib/localization/locales/. 15 + 14 16 ## v1.20.0: Thancred Waters 15 17 16 18 The big ticket items are as follows:
+3 -1
go.mod
··· 29 29 cel.dev/expr v0.23.1 // indirect 30 30 dario.cat/mergo v1.0.2 // indirect 31 31 github.com/AlekSi/pointer v1.2.0 // indirect 32 - github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 32 + github.com/BurntSushi/toml v1.5.0 // indirect 33 33 github.com/Masterminds/goutils v1.1.1 // indirect 34 34 github.com/Masterminds/semver/v3 v3.3.1 // indirect 35 35 github.com/Masterminds/sprig/v3 v3.3.0 // indirect ··· 87 87 github.com/mitchellh/reflectwalk v1.0.2 // indirect 88 88 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 89 89 github.com/natefinch/atomic v1.0.1 // indirect 90 + github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect 90 91 github.com/pjbgf/sha1cd v0.3.2 // indirect 91 92 github.com/pkg/errors v0.9.1 // indirect 92 93 github.com/prometheus/client_model v0.6.1 // indirect ··· 133 134 tool ( 134 135 github.com/TecharoHQ/yeet/cmd/yeet 135 136 github.com/a-h/templ/cmd/templ 137 + github.com/nicksnyder/go-i18n/v2/goi18n 136 138 github.com/suzuki-shunsuke/pinact/cmd/pinact 137 139 golang.org/x/tools/cmd/deadcode 138 140 golang.org/x/tools/cmd/goimports
+4
go.sum
··· 10 10 github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= 11 11 github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 12 12 github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 13 + github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 14 + github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 13 15 github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= 14 16 github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= 15 17 github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= ··· 237 239 github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= 238 240 github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= 239 241 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 242 + github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= 243 + github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= 240 244 github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 241 245 github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 242 246 github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
+27 -14
lib/anubis.go
··· 26 26 "github.com/TecharoHQ/anubis/internal/dnsbl" 27 27 "github.com/TecharoHQ/anubis/internal/ogtags" 28 28 "github.com/TecharoHQ/anubis/lib/challenge" 29 + "github.com/TecharoHQ/anubis/lib/localization" 29 30 "github.com/TecharoHQ/anubis/lib/policy" 30 31 "github.com/TecharoHQ/anubis/lib/policy/checker" 31 32 "github.com/TecharoHQ/anubis/lib/policy/config" ··· 87 88 } 88 89 } 89 90 91 + 92 + 90 93 func (s *Server) challengeFor(r *http.Request, difficulty int) string { 91 94 var fp [32]byte 92 95 if len(s.hs512Secret) == 0 { ··· 126 129 cr, rule, err := s.check(r) 127 130 if err != nil { 128 131 lg.Error("check failed", "err", err) 129 - s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy\"") 132 + localizer := localization.GetLocalizer(r) 133 + s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy\"", localizer.T("internal_server_error"))) 130 134 return 131 135 } 132 136 ··· 210 214 cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/" 211 215 } 212 216 217 + localizer := localization.GetLocalizer(r) 218 + 213 219 switch cr.Rule { 214 220 case config.RuleAllow: 215 221 lg.Debug("allowing traffic to origin (explicit)") ··· 220 226 lg.Info("explicit deny") 221 227 if rule == nil { 222 228 lg.Error("rule is nil, cannot calculate checksum") 223 - s.respondWithError(w, r, "Internal Server Error: Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.RuleDeny\"") 229 + s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.RuleDeny\"", localizer.T("internal_server_error"))) 224 230 return true 225 231 } 226 232 hash := rule.Hash() 227 233 228 234 lg.Debug("rule hash", "hash", hash) 229 - s.respondWithStatus(w, r, fmt.Sprintf("Access Denied: error code %s", hash), s.policy.StatusCodes.Deny) 235 + s.respondWithStatus(w, r, fmt.Sprintf("%s %s", localizer.T("access_denied"), hash), s.policy.StatusCodes.Deny) 230 236 return true 231 237 case config.RuleChallenge: 232 238 lg.Debug("challenge requested") ··· 237 243 default: 238 244 s.ClearCookie(w, s.cookieName, cookiePath, r.Host) 239 245 slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule) 240 - s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.Rules\"") 246 + s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.Rules\"", localizer.T("internal_server_error"))) 241 247 return true 242 248 } 243 249 return false ··· 258 264 259 265 if resp != dnsbl.AllGood { 260 266 lg.Info("DNSBL hit", "status", resp.String()) 261 - s.respondWithStatus(w, r, fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip), s.policy.StatusCodes.Deny) 267 + localizer := localization.GetLocalizer(r) 268 + s.respondWithStatus(w, r, fmt.Sprintf("%s: %s, %s https://dronebl.org/lookup?ip=%s", 269 + localizer.T("dronebl_entry"), 270 + resp.String(), 271 + localizer.T("see_dronebl_lookup"), 272 + ip), s.policy.StatusCodes.Deny) 262 273 return true 263 274 } 264 275 } ··· 267 278 268 279 func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { 269 280 lg := internal.GetRequestLogger(r) 281 + localizer := localization.GetLocalizer(r) 270 282 271 283 redir := r.FormValue("redir") 272 284 if redir == "" { ··· 276 288 encoder.Encode(struct { 277 289 Error string `json:"error"` 278 290 }{ 279 - Error: "Invalid invocation of MakeChallenge", 291 + Error: localizer.T("invalid_invocation"), 280 292 }) 281 293 return 282 294 } ··· 291 303 err := encoder.Encode(struct { 292 304 Error string `json:"error"` 293 305 }{ 294 - Error: "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"makeChallenge\"", 306 + Error: fmt.Sprintf("%s \"makeChallenge\"", localizer.T("internal_server_error")), 295 307 }) 296 308 if err != nil { 297 309 lg.Error("failed to encode error response", "err", err) ··· 322 334 323 335 func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { 324 336 lg := internal.GetRequestLogger(r) 337 + localizer := localization.GetLocalizer(r) 325 338 326 339 // Adjust cookie path if base prefix is not empty 327 340 cookiePath := "/" ··· 333 346 s.ClearCookie(w, s.cookieName, cookiePath, r.Host) 334 347 s.ClearCookie(w, anubis.TestCookieName, "/", r.Host) 335 348 lg.Warn("user has cookies disabled, this is not an anubis bug") 336 - s.respondWithError(w, r, "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain") 349 + s.respondWithError(w, r, localizer.T("cookies_disabled")) 337 350 return 338 351 } 339 352 ··· 343 356 redirURL, err := url.ParseRequestURI(redir) 344 357 if err != nil { 345 358 lg.Error("invalid redirect", "err", err) 346 - s.respondWithError(w, r, "Invalid redirect") 359 + s.respondWithError(w, r, localizer.T("invalid_redirect")) 347 360 return 348 361 } 349 362 // used by the path checker rule ··· 351 364 352 365 urlParsed, err := r.URL.Parse(redir) 353 366 if err != nil { 354 - s.respondWithError(w, r, "Redirect URL not parseable") 367 + s.respondWithError(w, r, localizer.T("redirect_not_parseable")) 355 368 return 356 369 } 357 370 if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host { 358 - s.respondWithError(w, r, "Redirect domain not allowed") 371 + s.respondWithError(w, r, localizer.T("redirect_domain_not_allowed")) 359 372 return 360 373 } 361 374 362 375 cr, rule, err := s.check(r) 363 376 if err != nil { 364 377 lg.Error("check failed", "err", err) 365 - s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\"") 378 + s.respondWithError(w, r, fmt.Sprintf("%s \"passChallenge\"", localizer.T("internal_server_error"))) 366 379 return 367 380 } 368 381 lg = lg.With("check_result", cr) ··· 370 383 impl, ok := challenge.Get(rule.Challenge.Algorithm) 371 384 if !ok { 372 385 lg.Error("check failed", "err", err) 373 - s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm)) 386 + s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm)) 374 387 return 375 388 } 376 389 ··· 403 416 if err != nil { 404 417 lg.Error("failed to sign JWT", "err", err) 405 418 s.ClearCookie(w, s.cookieName, cookiePath, r.Host) 406 - s.respondWithError(w, r, "failed to sign JWT") 419 + s.respondWithError(w, r, localizer.T("failed_to_sign_jwt")) 407 420 return 408 421 } 409 422
+4 -1
lib/challenge/metarefresh/metarefresh.go
··· 8 8 9 9 "github.com/TecharoHQ/anubis" 10 10 "github.com/TecharoHQ/anubis/lib/challenge" 11 + "github.com/TecharoHQ/anubis/lib/localization" 11 12 "github.com/TecharoHQ/anubis/lib/policy" 12 13 "github.com/TecharoHQ/anubis/web" 13 14 "github.com/a-h/templ" ··· 34 35 q.Set("challenge", in.Challenge) 35 36 u.RawQuery = q.Encode() 36 37 37 - component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", page(in.Challenge, u.String(), in.Rule.Challenge.Difficulty), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags) 38 + loc := localization.GetLocalizer(r) 39 + component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), page(in.Challenge, u.String(), in.Rule.Challenge.Difficulty, loc), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags, loc) 40 + 38 41 if err != nil { 39 42 return nil, fmt.Errorf("can't render page: %w", err) 40 43 }
+4 -3
lib/challenge/metarefresh/metarefresh.templ
··· 4 4 "fmt" 5 5 6 6 "github.com/TecharoHQ/anubis" 7 + "github.com/TecharoHQ/anubis/lib/localization" 7 8 ) 8 9 9 - templ page(challenge, redir string, difficulty int) { 10 + templ page(challenge, redir string, difficulty int, loc *localization.SimpleLocalizer) { 10 11 <div class="centered-div"> 11 12 <img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> 12 13 <img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/> 13 - <p id="status">Loading...</p> 14 - <p>Please wait a moment while we ensure the security of your connection.</p> 14 + <p id="status">{ loc.T("loading") }</p> 15 + <p>{ loc.T("connection_security") }</p> 15 16 <meta http-equiv="refresh" content={ fmt.Sprintf("%d; url=%s", difficulty, redir) }/> 16 17 </div> 17 18 }
+34 -7
lib/challenge/metarefresh/metarefresh_templ.go
··· 12 12 "fmt" 13 13 14 14 "github.com/TecharoHQ/anubis" 15 + "github.com/TecharoHQ/anubis/lib/localization" 15 16 ) 16 17 17 - func page(challenge, redir string, difficulty int) templ.Component { 18 + func page(challenge, redir string, difficulty int, loc *localization.SimpleLocalizer) templ.Component { 18 19 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 19 20 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 20 21 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { ··· 42 43 var templ_7745c5c3_Var2 string 43 44 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) 44 45 if templ_7745c5c3_Err != nil { 45 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 11, Col: 165} 46 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 165} 46 47 } 47 48 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 48 49 if templ_7745c5c3_Err != nil { ··· 55 56 var templ_7745c5c3_Var3 string 56 57 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version) 57 58 if templ_7745c5c3_Err != nil { 58 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 174} 59 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 13, Col: 174} 59 60 } 60 61 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 61 62 if templ_7745c5c3_Err != nil { 62 63 return templ_7745c5c3_Err 63 64 } 64 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><p id=\"status\">Loading...</p><p>Please wait a moment while we ensure the security of your connection.</p><meta http-equiv=\"refresh\" content=\"") 65 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><p id=\"status\">") 65 66 if templ_7745c5c3_Err != nil { 66 67 return templ_7745c5c3_Err 67 68 } 68 69 var templ_7745c5c3_Var4 string 69 - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir)) 70 + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(loc.T("loading")) 70 71 if templ_7745c5c3_Err != nil { 71 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 15, Col: 83} 72 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 14, Col: 35} 72 73 } 73 74 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 74 75 if templ_7745c5c3_Err != nil { 75 76 return templ_7745c5c3_Err 76 77 } 77 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"></div>") 78 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p><p>") 79 + if templ_7745c5c3_Err != nil { 80 + return templ_7745c5c3_Err 81 + } 82 + var templ_7745c5c3_Var5 string 83 + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(loc.T("connection_security")) 84 + if templ_7745c5c3_Err != nil { 85 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 15, Col: 35} 86 + } 87 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 88 + if templ_7745c5c3_Err != nil { 89 + return templ_7745c5c3_Err 90 + } 91 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p><meta http-equiv=\"refresh\" content=\"") 92 + if templ_7745c5c3_Err != nil { 93 + return templ_7745c5c3_Err 94 + } 95 + var templ_7745c5c3_Var6 string 96 + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir)) 97 + if templ_7745c5c3_Err != nil { 98 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 83} 99 + } 100 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 101 + if templ_7745c5c3_Err != nil { 102 + return templ_7745c5c3_Err 103 + } 104 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></div>") 78 105 if templ_7745c5c3_Err != nil { 79 106 return templ_7745c5c3_Err 80 107 }
+3 -1
lib/challenge/proofofwork/proofofwork.go
··· 10 10 11 11 "github.com/TecharoHQ/anubis/internal" 12 12 chall "github.com/TecharoHQ/anubis/lib/challenge" 13 + "github.com/TecharoHQ/anubis/lib/localization" 13 14 "github.com/TecharoHQ/anubis/lib/policy" 14 15 "github.com/TecharoHQ/anubis/web" 15 16 "github.com/a-h/templ" ··· 29 30 } 30 31 31 32 func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) { 32 - component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags) 33 + loc := localization.GetLocalizer(r) 34 + component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), web.Index(loc), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags, loc) 33 35 if err != nil { 34 36 return nil, fmt.Errorf("can't render page: %w", err) 35 37 }
+2 -1
lib/config.go
··· 20 20 "github.com/TecharoHQ/anubis/internal/dnsbl" 21 21 "github.com/TecharoHQ/anubis/internal/ogtags" 22 22 "github.com/TecharoHQ/anubis/lib/challenge" 23 + "github.com/TecharoHQ/anubis/lib/localization" 23 24 "github.com/TecharoHQ/anubis/lib/policy" 24 25 "github.com/TecharoHQ/anubis/lib/policy/config" 25 26 "github.com/TecharoHQ/anubis/web" ··· 155 156 if opts.Policy.Impressum != nil { 156 157 registerWithPrefix(anubis.APIPrefix+"imprint", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 157 158 templ.Handler( 158 - web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum), 159 + web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)), 159 160 ).ServeHTTP(w, r) 160 161 }), "GET") 161 162 }
+18 -9
lib/http.go
··· 12 12 "github.com/TecharoHQ/anubis" 13 13 "github.com/TecharoHQ/anubis/internal" 14 14 "github.com/TecharoHQ/anubis/lib/challenge" 15 + "github.com/TecharoHQ/anubis/lib/localization" 15 16 "github.com/TecharoHQ/anubis/lib/policy" 16 17 "github.com/TecharoHQ/anubis/web" 17 18 "github.com/a-h/templ" ··· 83 84 } 84 85 85 86 func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) { 87 + localizer := localization.GetLocalizer(r) 88 + 86 89 if returnHTTPStatusOnly { 87 90 w.WriteHeader(http.StatusUnauthorized) 88 - w.Write([]byte("Authorization required")) 91 + w.Write([]byte(localizer.T("authorization_required"))) 89 92 return 90 93 } 91 94 ··· 93 96 94 97 if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) { 95 98 lg.Error("client was given a challenge but does not in fact support gzip compression") 96 - s.respondWithError(w, r, "Client Error: Please ensure your browser is up to date and try again later.") 99 + s.respondWithError(w, r, localizer.T("client_error_browser")) 97 100 } 98 101 99 102 challengesIssued.WithLabelValues("embedded").Add(1) ··· 118 121 impl, ok := challenge.Get(rule.Challenge.Algorithm) 119 122 if !ok { 120 123 lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm) 121 - s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm)) 124 + s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm)) 122 125 return 123 126 } 124 127 ··· 132 135 component, err := impl.Issue(r, lg, in) 133 136 if err != nil { 134 137 lg.Error("[unexpected] render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this. 135 - s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"") 138 + s.respondWithError(w, r, fmt.Sprintf("%s \"RenderIndex\"", localizer.T("internal_server_error"))) 136 139 return 137 140 } 138 141 ··· 144 147 } 145 148 146 149 func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) { 150 + localizer := localization.GetLocalizer(r) 151 + 147 152 templ.Handler( 148 - web.Base("Benchmarking Anubis!", web.Bench(), s.policy.Impressum), 153 + web.Base(localizer.T("benchmarking_anubis"), web.Bench(localizer), s.policy.Impressum, localizer), 149 154 ).ServeHTTP(w, r) 150 155 } 151 156 ··· 154 159 } 155 160 156 161 func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg string, status int) { 157 - templ.Handler(web.Base("Oh noes!", web.ErrorPage(msg, s.opts.WebmasterEmail), s.policy.Impressum), templ.WithStatus(status)).ServeHTTP(w, r) 162 + localizer := localization.GetLocalizer(r) 163 + 164 + templ.Handler(web.Base(localizer.T("oh_noes"), web.ErrorPage(msg, s.opts.WebmasterEmail, localizer), s.policy.Impressum, localizer), templ.WithStatus(status)).ServeHTTP(w, r) 158 165 } 159 166 160 167 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 189 196 190 197 func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) { 191 198 if s.next == nil { 199 + localizer := localization.GetLocalizer(r) 200 + 192 201 redir := r.FormValue("redir") 193 202 urlParsed, err := r.URL.Parse(redir) 194 203 if err != nil { 195 - s.respondWithStatus(w, r, "Redirect URL not parseable", http.StatusBadRequest) 204 + s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), http.StatusBadRequest) 196 205 return 197 206 } 198 207 199 208 if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host { 200 - s.respondWithStatus(w, r, "Redirect domain not allowed", http.StatusBadRequest) 209 + s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), http.StatusBadRequest) 201 210 return 202 211 } 203 212 ··· 207 216 } 208 217 209 218 templ.Handler( 210 - web.Base("You are not a bot!", web.StaticHappy(), s.policy.Impressum), 219 + web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer), 211 220 ).ServeHTTP(w, r) 212 221 } else { 213 222 requestsProxied.WithLabelValues(r.Host).Inc()
+63
lib/localization/locales/en.json
··· 1 + { 2 + "loading": "Loading...", 3 + "why_am_i_seeing": "Why am I seeing this?", 4 + "protected_by": "Protected by", 5 + "made_with": "Made with ❤️ in 🇨🇦", 6 + "mascot_design": "Mascot design by", 7 + "ai_companies_explanation": "You are seeing this because the administrator of this website has set up Anubis to protect the server against the scourge of AI companies aggressively scraping websites. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.", 8 + "anubis_compromise": "Anubis is a compromise. Anubis uses a Proof-of-Work scheme in the vein of Hashcash, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.", 9 + "hack_purpose": "Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.", 10 + "jshelter_note": "Please note that Anubis requires the use of modern JavaScript features that plugins like JShelter will disable. Please disable JShelter or other such plugins for this domain.", 11 + "version_info": "This website is running Anubis version", 12 + "try_again": "Try again", 13 + "go_home": "Go home", 14 + "contact_webmaster": "or if you believe you should not be blocked, please contact the webmaster at", 15 + "connection_security": "Please wait a moment while we ensure the security of your connection.", 16 + "javascript_required": "Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works. A no-JS solution is a work-in-progress.", 17 + "benchmark_requires_js": "Running the benchmark tool requires JavaScript to be enabled.", 18 + "difficulty": "Difficulty:", 19 + "algorithm": "Algorithm:", 20 + "compare": "Compare:", 21 + "time": "Time", 22 + "iters": "Iters", 23 + "time_a": "Time A", 24 + "iters_a": "Iters A", 25 + "time_b": "Time B", 26 + "iters_b": "Iters B", 27 + "static_check_endpoint": "This is just a check endpoint for your reverse proxy to use.", 28 + "authorization_required": "Authorization required", 29 + "cookies_disabled": "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain", 30 + "access_denied": "Access Denied: error code", 31 + "dronebl_entry": "DroneBL reported an entry", 32 + "see_dronebl_lookup": "see", 33 + "internal_server_error": "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around", 34 + "invalid_redirect": "Invalid redirect", 35 + "redirect_not_parseable": "Redirect URL not parseable", 36 + "redirect_domain_not_allowed": "Redirect domain not allowed", 37 + "failed_to_sign_jwt": "failed to sign JWT", 38 + "invalid_invocation": "Invalid invocation of MakeChallenge", 39 + "client_error_browser": "Client Error: Please ensure your browser is up to date and try again later.", 40 + "oh_noes": "Oh noes!", 41 + "benchmarking_anubis": "Benchmarking Anubis!", 42 + "you_are_not_a_bot": "You are not a bot!", 43 + "making_sure_not_bot": "Making sure you're not a bot!", 44 + "celphase": "CELPHASE", 45 + "js_web_crypto_error": "Your browser doesn't have a functioning web.crypto element. Are you viewing this over a secure context?", 46 + "js_web_workers_error": "Your browser doesn't support web workers (Anubis uses this to avoid freezing your browser). Do you have a plugin like JShelter installed?", 47 + "js_cookies_error": "Your browser doesn't store cookies. Anubis uses cookies to determine which clients have passed challenges by storing a signed token in a cookie. Please enable storing cookies for this domain. The names of the cookies Anubis stores may vary without notice. Cookie names and values are not part of the public API.", 48 + "js_context_not_secure": "Your context is not secure!", 49 + "js_context_not_secure_msg": "Try connecting over HTTPS or let the admin know to set up HTTPS. For more information, see <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.", 50 + "js_calculating": "Calculating...", 51 + "js_missing_feature": "Missing feature", 52 + "js_challenge_error": "Challenge error!", 53 + "js_challenge_error_msg": "Failed to resolve check algorithm. You may want to reload the page.", 54 + "js_calculating_difficulty": "Calculating...<br/>Difficulty:", 55 + "js_speed": "Speed:", 56 + "js_verification_longer": "Verification is taking longer than expected. Please do not refresh the page.", 57 + "js_success": "Success!", 58 + "js_done_took": "Done! Took", 59 + "js_iterations": "iterations", 60 + "js_finished_reading": "I've finished reading, continue →", 61 + "js_calculation_error": "Calculation error!", 62 + "js_calculation_error_msg": "Failed to calculate challenge:" 63 + }
+63
lib/localization/locales/es.json
··· 1 + { 2 + "loading": "Cargando...", 3 + "why_am_i_seeing": "¿Por qué veo esto?", 4 + "protected_by": "Protegido por", 5 + "made_with": "Hecho con ❤️ en 🇨🇦", 6 + "mascot_design": "Diseño de la mascota por", 7 + "ai_companies_explanation": "Ves esto porque el administrador de este sitio web ha configurado Anubis para proteger el servidor contra la plaga de empresas de IA que rastrean agresivamente los sitios web. Esto puede y causa tiempo de inactividad para los sitios web, haciendo que sus recursos sean inaccesibles para todos.", 8 + "anubis_compromise": "Anubis es un compromiso. Anubis utiliza un esquema de Prueba de Trabajo en la línea de Hashcash, un esquema de prueba de trabajo propuesto para reducir el spam por correo electrónico. La idea es que a escala individual, la carga adicional es insignificante, pero a escala de raspadores masivos, se acumula y hace que el raspado sea mucho más costoso.", 9 + "hack_purpose": "En última instancia, esto es un hack cuyo verdadero propósito es dar una solución alternativa \"suficientemente buena\" para que se pueda dedicar más tiempo a la huella digital e identificación de navegadores sin cabeza (por ejemplo: a través de cómo renderizan las fuentes) para que la página de desafío de prueba de trabajo no necesite ser presentada a usuarios que son mucho más propensos a ser legítimos.", 10 + "jshelter_note": "Ten en cuenta que Anubis requiere el uso de características modernas de JavaScript que plugins como JShelter deshabilitarán. Por favor, deshabilita JShelter u otros plugins similares para este dominio.", 11 + "version_info": "Este sitio web utiliza Anubis versión", 12 + "try_again": "Intentar de nuevo", 13 + "go_home": "Inicio", 14 + "contact_webmaster": "o si crees que no deberías estar bloqueado, por favor contacta al webmaster en", 15 + "connection_security": "Espere un momento mientras garantizamos la seguridad de su conexión.", 16 + "javascript_required": "Desafortunadamente, necesitas habilitar JavaScript para pasar este desafío. Esto es requerido porque las empresas de IA han cambiado el contrato social sobre cómo funciona el alojamiento de sitios web. Una solución sin JS está en desarrollo.", 17 + "benchmark_requires_js": "Ejecutar la herramienta de benchmark requiere que JavaScript esté habilitado.", 18 + "difficulty": "Dificultad:", 19 + "algorithm": "Algoritmo:", 20 + "compare": "Comparar:", 21 + "time": "Tiempo", 22 + "iters": "Iteraciones", 23 + "time_a": "Tiempo A", 24 + "iters_a": "Iter. A", 25 + "time_b": "Tiempo B", 26 + "iters_b": "Iter. B", 27 + "static_check_endpoint": "Este es solo un endpoint de verificación para que tu proxy inverso lo use.", 28 + "authorization_required": "Autorización requerida", 29 + "cookies_disabled": "Tu navegador está configurado para deshabilitar las cookies. Anubis requiere cookies para el interés legítimo de asegurar que eres un cliente válido. Por favor habilita las cookies para este dominio", 30 + "access_denied": "Acceso denegado: código de error", 31 + "dronebl_entry": "DroneBL reportó una entrada", 32 + "see_dronebl_lookup": "ver", 33 + "internal_server_error": "Error interno del servidor: el administrador ha configurado mal Anubis. Por favor contacta al administrador y pídele que revise los logs alrededor de", 34 + "invalid_redirect": "Redirección inválida", 35 + "redirect_not_parseable": "URL de redirección no analizable", 36 + "redirect_domain_not_allowed": "Dominio de redirección no permitido", 37 + "failed_to_sign_jwt": "falló al firmar JWT", 38 + "invalid_invocation": "Invocación inválida de MakeChallenge", 39 + "client_error_browser": "Error del cliente: Por favor asegúrate de que tu navegador esté actualizado e inténtalo de nuevo más tarde.", 40 + "oh_noes": "¡Oh no!", 41 + "benchmarking_anubis": "¡Benchmarking de Anubis!", 42 + "you_are_not_a_bot": "¡No eres un robot!", 43 + "making_sure_not_bot": "¡Asegurándonos de que no eres un robot!", 44 + "celphase": "CELPHASE", 45 + "js_web_crypto_error": "Tu navegador no tiene un elemento web.crypto funcional. ¿Estás viendo esta página en un contexto seguro?", 46 + "js_web_workers_error": "Tu navegador no soporta web workers (Anubis los usa para evitar bloquear tu navegador). ¿Tienes un plugin como JShelter instalado?", 47 + "js_cookies_error": "Tu navegador no almacena cookies. Anubis usa cookies para determinar qué clientes han pasado los desafíos almacenando un token firmado en una cookie. Por favor habilita el almacenamiento de cookies para este dominio. Los nombres de las cookies que Anubis almacena pueden variar sin previo aviso. Los nombres y valores de las cookies no son parte de la API pública.", 48 + "js_context_not_secure": "¡Tu contexto no es seguro!", 49 + "js_context_not_secure_msg": "Intenta conectarte a través de HTTPS o informa al administrador para configurar HTTPS. Para más información, consulta <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.", 50 + "js_calculating": "Calculando...", 51 + "js_missing_feature": "Característica faltante", 52 + "js_challenge_error": "¡Error de desafío!", 53 + "js_challenge_error_msg": "Falló al resolver el algoritmo de verificación. Puedes intentar recargar la página.", 54 + "js_calculating_difficulty": "Calculando...<br/>Dificultad:", 55 + "js_speed": "Velocidad:", 56 + "js_verification_longer": "La verificación está tomando más tiempo del esperado. Por favor no actualices la página.", 57 + "js_success": "¡Éxito!", 58 + "js_done_took": "¡Terminado! Tomó", 59 + "js_iterations": "iteraciones", 60 + "js_finished_reading": "He terminado de leer, continuar →", 61 + "js_calculation_error": "¡Error de cálculo!", 62 + "js_calculation_error_msg": "Falló al calcular el desafío:" 63 + }
+63
lib/localization/locales/fr.json
··· 1 + { 2 + "loading": "Chargement...", 3 + "why_am_i_seeing": "Pourquoi je vois ceci ?", 4 + "protected_by": "Protégé par", 5 + "made_with": "Fait avec ❤️ au 🇨🇦", 6 + "mascot_design": "Design de la mascotte par", 7 + "ai_companies_explanation": "Vous voyez ceci car l'administrateur de ce site web a configuré Anubis pour protéger le serveur contre le fléau des entreprises d'IA qui scrapent agressivement les sites web. Cela peut et cause des temps d'arrêt pour les sites web, ce qui rend leurs ressources inaccessibles pour tout le monde.", 8 + "anubis_compromise": "Anubis est un compromis. Anubis utilise un schéma de Preuve de Travail dans la veine de Hashcash, un schéma de preuve de travail proposé pour réduire le spam par email. L'idée est qu'à l'échelle individuelle, la charge supplémentaire est négligeable, mais à l'échelle des scrapers de masse, cela s'accumule et rend le scraping beaucoup plus coûteux.", 9 + "hack_purpose": "En fin de compte, c'est un hack dont le véritable objectif est de donner une solution de substitution \"assez bonne\" pour que plus de temps puisse être consacré à l'empreinte digitale et à l'identification des navigateurs sans tête (par exemple : via la façon dont ils font le rendu des polices) afin que la page de défi de preuve de travail n'ait pas besoin d'être présentée aux utilisateurs qui sont beaucoup plus susceptibles d'être légitimes.", 10 + "jshelter_note": "Veuillez noter qu'Anubis nécessite l'utilisation de fonctionnalités JavaScript modernes que des plugins comme JShelter désactiveront. Veuillez désactiver JShelter ou d'autres plugins similaires pour ce domaine.", 11 + "version_info": "Ce site web utilise Anubis version", 12 + "try_again": "Réessayer", 13 + "go_home": "Accueil", 14 + "contact_webmaster": "ou si vous pensez que vous ne devriez pas être bloqué, veuillez contacter le webmaster à", 15 + "connection_security": "Veuillez patienter un instant pendant que nous assurons la sécurité de votre connexion.", 16 + "javascript_required": "Malheureusement, vous devez activer JavaScript pour passer ce défi. Ceci est requis car les entreprises d'IA ont changé le contrat social autour du fonctionnement de l'hébergement de sites web. Une solution sans JS est en cours de développement.", 17 + "benchmark_requires_js": "L'exécution de l'outil de benchmark nécessite l'activation de JavaScript.", 18 + "difficulty": "Difficulté :", 19 + "algorithm": "Algorithme :", 20 + "compare": "Comparer :", 21 + "time": "Temps", 22 + "iters": "Itérations", 23 + "time_a": "Temps A", 24 + "iters_a": "Itér. A", 25 + "time_b": "Temps B", 26 + "iters_b": "Itér. B", 27 + "static_check_endpoint": "Ceci est juste un point de terminaison de vérification pour votre proxy inverse à utiliser.", 28 + "authorization_required": "Autorisation requise", 29 + "cookies_disabled": "Votre navigateur est configuré pour désactiver les cookies. Anubis nécessite des cookies pour l'intérêt légitime de s'assurer que vous êtes un client valide. Veuillez activer les cookies pour ce domaine", 30 + "access_denied": "Accès refusé : code d'erreur", 31 + "dronebl_entry": "DroneBL a signalé une entrée", 32 + "see_dronebl_lookup": "voir", 33 + "internal_server_error": "Erreur interne du serveur : l'administrateur a mal configuré Anubis. Veuillez contacter l'administrateur et lui demander de consulter les logs autour de", 34 + "invalid_redirect": "Redirection invalide", 35 + "redirect_not_parseable": "URL de redirection non analysable", 36 + "redirect_domain_not_allowed": "Domaine de redirection non autorisé", 37 + "failed_to_sign_jwt": "échec de la signature JWT", 38 + "invalid_invocation": "Invocation invalide de MakeChallenge", 39 + "client_error_browser": "Erreur client : Veuillez vous assurer que votre navigateur est à jour et réessayez plus tard.", 40 + "oh_noes": "Oh non !", 41 + "benchmarking_anubis": "Test de performance d'Anubis !", 42 + "you_are_not_a_bot": "Vous n'êtes pas un robot !", 43 + "making_sure_not_bot": "Vérification que vous n'êtes pas un robot !", 44 + "celphase": "PHASE de CEL", 45 + "js_web_crypto_error": "Votre navigateur n'a pas d'élément web.crypto fonctionnel. Consultez-vous cette page dans un contexte sécurisé ?", 46 + "js_web_workers_error": "Votre navigateur ne prend pas en charge les web workers (Anubis les utilise pour éviter de bloquer votre navigateur). Avez-vous un plugin comme JShelter installé ?", 47 + "js_cookies_error": "Votre navigateur ne stocke pas les cookies. Anubis utilise des cookies pour déterminer quels clients ont réussi les défis en stockant un jeton signé dans un cookie. Veuillez activer le stockage des cookies pour ce domaine. Les noms des cookies qu'Anubis stocke peuvent varier sans préavis. Les noms et valeurs des cookies ne font pas partie de l'API publique.", 48 + "js_context_not_secure": "Votre contexte n'est pas sécurisé !", 49 + "js_context_not_secure_msg": "Essayez de vous connecter via HTTPS ou informez l'administrateur de configurer HTTPS. Pour plus d'informations, voir <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.", 50 + "js_calculating": "Calcul en cours...", 51 + "js_missing_feature": "Fonctionnalité manquante", 52 + "js_challenge_error": "Erreur de défi !", 53 + "js_challenge_error_msg": "Échec de la résolution de l'algorithme de vérification. Vous pouvez essayer de recharger la page.", 54 + "js_calculating_difficulty": "Calcul en cours...<br/>Difficulté :", 55 + "js_speed": "Vitesse :", 56 + "js_verification_longer": "La vérification prend plus de temps que prévu. Veuillez ne pas actualiser la page.", 57 + "js_success": "Succès !", 58 + "js_done_took": "Terminé ! A pris", 59 + "js_iterations": "itérations", 60 + "js_finished_reading": "J'ai fini de lire, continuer →", 61 + "js_calculation_error": "Erreur de calcul !", 62 + "js_calculation_error_msg": "Échec du calcul du défi :" 63 + }
+3
lib/localization/locales/manifest.json
··· 1 + { 2 + "supportedLanguages": ["en", "fr", "es"] 3 + }
+100
lib/localization/localization.go
··· 1 + package localization 2 + 3 + import ( 4 + "embed" 5 + "encoding/json" 6 + "net/http" 7 + "strings" 8 + "sync" 9 + 10 + "github.com/nicksnyder/go-i18n/v2/i18n" 11 + "golang.org/x/text/language" 12 + ) 13 + 14 + //go:embed locales/*.json 15 + var localeFS embed.FS 16 + 17 + type LocalizationService struct { 18 + bundle *i18n.Bundle 19 + } 20 + 21 + var ( 22 + globalService *LocalizationService 23 + once sync.Once 24 + ) 25 + 26 + func NewLocalizationService() *LocalizationService { 27 + once.Do(func() { 28 + bundle := i18n.NewBundle(language.English) 29 + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) 30 + 31 + // Read all JSON files from the locales directory 32 + entries, err := localeFS.ReadDir("locales") 33 + if err != nil { 34 + // Try fallback - create a minimal service with default messages 35 + globalService = &LocalizationService{bundle: bundle} 36 + return 37 + } 38 + 39 + loadedAny := false 40 + for _, entry := range entries { 41 + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") { 42 + filePath := "locales/" + entry.Name() 43 + _, err := bundle.LoadMessageFileFS(localeFS, filePath) 44 + if err != nil { 45 + // Log error but continue with other files 46 + continue 47 + } 48 + loadedAny = true 49 + } 50 + } 51 + 52 + if !loadedAny { 53 + // If no files were loaded successfully, create minimal service 54 + globalService = &LocalizationService{bundle: bundle} 55 + return 56 + } 57 + 58 + globalService = &LocalizationService{bundle: bundle} 59 + }) 60 + 61 + // Safety check - if globalService is still nil, create a minimal one 62 + if globalService == nil { 63 + bundle := i18n.NewBundle(language.English) 64 + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) 65 + globalService = &LocalizationService{bundle: bundle} 66 + } 67 + 68 + return globalService 69 + } 70 + 71 + func (ls *LocalizationService) GetLocalizer(lang string) *i18n.Localizer { 72 + return i18n.NewLocalizer(ls.bundle, lang) 73 + } 74 + 75 + func (ls *LocalizationService) GetLocalizerFromRequest(r *http.Request) *i18n.Localizer { 76 + if ls == nil || ls.bundle == nil { 77 + // Fallback to a basic bundle if service is not properly initialized 78 + bundle := i18n.NewBundle(language.English) 79 + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) 80 + return i18n.NewLocalizer(bundle, "en") 81 + } 82 + acceptLanguage := r.Header.Get("Accept-Language") 83 + return i18n.NewLocalizer(ls.bundle, acceptLanguage, "en") 84 + } 85 + 86 + // SimpleLocalizer wraps i18n.Localizer with a more convenient API 87 + type SimpleLocalizer struct { 88 + Localizer *i18n.Localizer 89 + } 90 + 91 + // T provides a concise way to localize messages 92 + func (sl *SimpleLocalizer) T(messageID string) string { 93 + return sl.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID}) 94 + } 95 + 96 + // GetLocalizer creates a localizer based on the request's Accept-Language header 97 + func GetLocalizer(r *http.Request) *SimpleLocalizer { 98 + localizer := NewLocalizationService().GetLocalizerFromRequest(r) 99 + return &SimpleLocalizer{Localizer: localizer} 100 + }
+116
lib/localization/localization_test.go
··· 1 + package localization 2 + 3 + import ( 4 + "encoding/json" 5 + "sort" 6 + "testing" 7 + 8 + "github.com/nicksnyder/go-i18n/v2/i18n" 9 + ) 10 + 11 + func TestLocalizationService(t *testing.T) { 12 + service := NewLocalizationService() 13 + 14 + t.Run("English localization", func(t *testing.T) { 15 + localizer := service.GetLocalizer("en") 16 + result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "loading"}) 17 + if result != "Loading..." { 18 + t.Errorf("Expected 'Loading...', got '%s'", result) 19 + } 20 + }) 21 + 22 + t.Run("French localization", func(t *testing.T) { 23 + localizer := service.GetLocalizer("fr") 24 + result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "loading"}) 25 + if result != "Chargement..." { 26 + t.Errorf("Expected 'Chargement...', got '%s'", result) 27 + } 28 + }) 29 + 30 + t.Run("All required keys exist in English", func(t *testing.T) { 31 + localizer := service.GetLocalizer("en") 32 + requiredKeys := []string{ 33 + "loading", "why_am_i_seeing", "protected_by", "made_with", 34 + "mascot_design", "try_again", "go_home", "javascript_required", 35 + } 36 + 37 + for _, key := range requiredKeys { 38 + result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: key}) 39 + if result == "" { 40 + t.Errorf("Key '%s' returned empty string", key) 41 + } 42 + } 43 + }) 44 + 45 + t.Run("All required keys exist in French", func(t *testing.T) { 46 + localizer := service.GetLocalizer("fr") 47 + requiredKeys := []string{ 48 + "loading", "why_am_i_seeing", "protected_by", "made_with", 49 + "mascot_design", "try_again", "go_home", "javascript_required", 50 + } 51 + 52 + for _, key := range requiredKeys { 53 + result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: key}) 54 + if result == "" { 55 + t.Errorf("Key '%s' returned empty string", key) 56 + } 57 + } 58 + }) 59 + } 60 + 61 + type manifest struct { 62 + SupportedLanguages []string `json:"supported_languages"` 63 + } 64 + 65 + func loadManifest(t *testing.T) manifest { 66 + t.Helper() 67 + 68 + fin, err := localeFS.Open("locales/manifest.json") 69 + if err != nil { 70 + t.Fatal(err) 71 + } 72 + defer fin.Close() 73 + 74 + var result manifest 75 + if err := json.NewDecoder(fin).Decode(&result); err != nil { 76 + t.Fatal(err) 77 + } 78 + 79 + return result 80 + } 81 + 82 + func TestComprehensiveTranslations(t *testing.T) { 83 + service := NewLocalizationService() 84 + 85 + var translations = map[string]any{} 86 + fin, err := localeFS.Open("locales/en.json") 87 + if err != nil { 88 + t.Fatal(err) 89 + } 90 + defer fin.Close() 91 + 92 + if err := json.NewDecoder(fin).Decode(&translations); err != nil { 93 + t.Fatal(err) 94 + } 95 + 96 + var keys []string 97 + for k := range translations { 98 + keys = append(keys, k) 99 + } 100 + 101 + sort.Strings(keys) 102 + 103 + for _, lang := range loadManifest(t).SupportedLanguages { 104 + t.Run(lang, func(t *testing.T) { 105 + loc := service.GetLocalizer(lang) 106 + sl := SimpleLocalizer{Localizer: loc} 107 + for _, key := range keys { 108 + t.Run(key, func(t *testing.T) { 109 + if result := sl.T(key); result == "" { 110 + t.Error("key not defined") 111 + } 112 + }) 113 + } 114 + }) 115 + } 116 + }
+5 -1
web/build.sh
··· 32 32 for the JavaScript code in this page. 33 33 */' 34 34 35 + # Copy localization files to static directory 36 + mkdir -p static/locales 37 + cp ../lib/localization/locales/*.json static/locales/ 38 + 35 39 esbuild js/main.mjs --sourcemap --bundle --minify --outfile=static/js/main.mjs "--banner:js=${LICENSE}" 36 40 gzip -f -k -n static/js/main.mjs 37 41 zstd -f -k --ultra -22 static/js/main.mjs 38 42 brotli -fZk static/js/main.mjs 39 43 40 - esbuild js/bench.mjs --sourcemap --bundle --minify --outfile=static/js/bench.mjs 44 + esbuild js/bench.mjs --sourcemap --bundle --minify --outfile=static/js/bench.mjs
+11 -10
web/index.go
··· 3 3 import ( 4 4 "github.com/a-h/templ" 5 5 6 + "github.com/TecharoHQ/anubis/lib/localization" 6 7 "github.com/TecharoHQ/anubis/lib/policy/config" 7 8 ) 8 9 9 - func Base(title string, body templ.Component, impressum *config.Impressum) templ.Component { 10 - return base(title, body, impressum, nil, nil) 10 + func Base(title string, body templ.Component, impressum *config.Impressum, localizer *localization.SimpleLocalizer) templ.Component { 11 + return base(title, body, impressum, nil, nil, localizer) 11 12 } 12 13 13 - func BaseWithChallengeAndOGTags(title string, body templ.Component, impressum *config.Impressum, challenge string, rules *config.ChallengeRules, ogTags map[string]string) (templ.Component, error) { 14 + func BaseWithChallengeAndOGTags(title string, body templ.Component, impressum *config.Impressum, challenge string, rules *config.ChallengeRules, ogTags map[string]string, localizer *localization.SimpleLocalizer) (templ.Component, error) { 14 15 return base(title, body, impressum, struct { 15 16 Rules *config.ChallengeRules `json:"rules"` 16 17 Challenge string `json:"challenge"` 17 18 }{ 18 19 Challenge: challenge, 19 20 Rules: rules, 20 - }, ogTags), nil 21 + }, ogTags, localizer), nil 21 22 } 22 23 23 - func Index() templ.Component { 24 - return index() 24 + func Index(localizer *localization.SimpleLocalizer) templ.Component { 25 + return index(localizer) 25 26 } 26 27 27 - func ErrorPage(msg string, mail string) templ.Component { 28 - return errorPage(msg, mail) 28 + func ErrorPage(msg string, mail string, localizer *localization.SimpleLocalizer) templ.Component { 29 + return errorPage(msg, mail, localizer) 29 30 } 30 31 31 - func Bench() templ.Component { 32 - return bench() 32 + func Bench(localizer *localization.SimpleLocalizer) templ.Component { 33 + return bench(localizer) 33 34 }
+36 -53
web/index.templ
··· 3 3 import ( 4 4 "fmt" 5 5 "github.com/TecharoHQ/anubis" 6 + "github.com/TecharoHQ/anubis/lib/localization" 6 7 "github.com/TecharoHQ/anubis/lib/policy/config" 7 8 "github.com/TecharoHQ/anubis/xess" 8 9 ) 9 10 10 - templ base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string) { 11 + templ base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) { 11 12 <!DOCTYPE html> 12 13 <html lang="en"> 13 14 <head> ··· 71 72 <footer> 72 73 <center> 73 74 <p> 74 - Protected by <a href="https://github.com/TecharoHQ/anubis">Anubis</a> from <a 75 + { localizer.T("protected_by") } <a href="https://github.com/TecharoHQ/anubis">Anubis</a> from <a 75 76 href="https://techaro.lol" 76 - >Techaro</a>. Made with ❤️ in 🇨🇦. 77 + >Techaro</a>. { localizer.T("made_with") }. 77 78 </p> 78 - <p>Mascot design by <a href="https://bsky.app/profile/celphase.bsky.social">CELPHASE</a>.</p> 79 + <p>{ localizer.T("mascot_design") } <a href="https://bsky.app/profile/celphase.bsky.social">{ localizer.T("celphase") }</a>.</p> 79 80 if impressum != nil { 80 - <p>@templ.Raw(impressum.Footer) 81 - -- <a href={ templ.SafeURL(fmt.Sprintf("%simprint", anubis.APIPrefix)) }>Imprint</a></p> 81 + <p> 82 + @templ.Raw(impressum.Footer) 83 + -- <a href={ templ.SafeURL(fmt.Sprintf("%simprint", anubis.APIPrefix)) }>Imprint</a> 84 + </p> 82 85 } 83 86 </center> 84 87 </footer> ··· 87 90 </html> 88 91 } 89 92 90 - templ index() { 93 + templ index(localizer *localization.SimpleLocalizer) { 91 94 <div class="centered-div"> 92 95 <img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> 93 96 <img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/> 94 - <p id="status">Loading...</p> 97 + <p id="status">{ localizer.T("loading") }</p> 95 98 <script async type="module" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script> 96 99 <div id="progress" role="progressbar" aria-labelledby="status"> 97 100 <div class="bar-inner"></div> 98 101 </div> 99 102 <details> 100 - <summary>Why am I seeing this?</summary> 103 + <summary>{ localizer.T("why_am_i_seeing") }</summary> 101 104 <p> 102 - You are seeing this because the administrator of this website has set up <a 103 - href="https://github.com/TecharoHQ/anubis" 104 - >Anubis</a> to protect the server against the scourge of 105 - <a href="https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/"> 106 - AI companies 107 - aggressively scraping websites 108 - </a>. This can and does cause downtime for the websites, which makes their 109 - resources inaccessible for everyone. 105 + { localizer.T("ai_companies_explanation") } 110 106 </p> 111 107 <p> 112 - Anubis is a compromise. Anubis uses a <a 113 - href="https://anubis.techaro.lol/docs/design/why-proof-of-work" 114 - >Proof-of-Work</a> 115 - scheme in the vein of <a href="https://en.wikipedia.org/wiki/Hashcash">Hashcash</a>, a proposed 116 - proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is 117 - ignorable, but at mass scraper levels it adds up and makes scraping much more expensive. 108 + { localizer.T("anubis_compromise") } 118 109 </p> 119 110 <p> 120 - Ultimately, this is a hack whose real purpose is to give a "good enough" placeholder solution so that more 121 - time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) 122 - so that the challenge proof of work page doesn't need to be presented to users that are much more likely to 123 - be legitimate. 111 + { localizer.T("hack_purpose") } 124 112 </p> 125 113 <p> 126 - Please note that Anubis requires the use of modern JavaScript features that plugins like <a 127 - href="https://jshelter.org/" 128 - >JShelter</a> will disable. Please disable JShelter or other such 129 - plugins for this domain. 114 + { localizer.T("jshelter_note") } 130 115 </p> 131 - <p>This website is running Anubis version <code>{ anubis.Version }</code>.</p> 116 + <p>{ localizer.T("version_info") } <code>{ anubis.Version }</code>.</p> 132 117 </details> 133 118 <noscript> 134 119 <p> 135 - Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have 136 - changed 137 - the social contract around how website hosting works. A no-JS solution is a work-in-progress. 120 + { localizer.T("javascript_required") } 138 121 </p> 139 122 </noscript> 140 123 <div id="testarea"></div> 141 124 </div> 142 125 } 143 126 144 - templ errorPage(message string, mail string) { 127 + templ errorPage(message string, mail string, localizer *localization.SimpleLocalizer) { 145 128 <div class="centered-div"> 146 129 <img id="image" alt="Sad Anubis" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version }/> 147 130 <p>{ message }.</p> 148 - <button onClick="window.location.reload();">Try again</button> 131 + <button onClick="window.location.reload();">{ localizer.T("try_again") }</button> 149 132 if mail != "" { 150 133 <p> 151 - <a href="/">Go home</a> or if you believe you should not be blocked, please contact the webmaster at 134 + <a href="/">{ localizer.T("go_home") }</a> { localizer.T("contact_webmaster") } 152 135 <a href={ "mailto:" + templ.SafeURL(mail) }> 153 136 { mail } 154 137 </a> 155 138 </p> 156 139 } else { 157 - <p><a href="/">Go home</a></p> 140 + <p><a href="/">{ localizer.T("go_home") }</a></p> 158 141 } 159 142 </div> 160 143 } 161 144 162 - templ StaticHappy() { 145 + templ StaticHappy(localizer *localization.SimpleLocalizer) { 163 146 <div class="centered-div"> 164 147 <img 165 148 style="display:none;" ··· 167 150 src={ "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + 168 151 anubis.Version } 169 152 /> 170 - <p>This is just a check endpoint for your reverse proxy to use.</p> 153 + <p>{ localizer.T("static_check_endpoint") }</p> 171 154 </div> 172 155 } 173 156 174 - templ bench() { 157 + templ bench(localizer *localization.SimpleLocalizer) { 175 158 <div style="height:20rem;display:flex"> 176 159 <table style="margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem"> 177 160 <thead 178 161 style="border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1" 179 162 > 180 163 <tr id="table-header" style="display:contents"> 181 - <th style="width:4.5rem">Time</th> 182 - <th style="width:4rem">Iters</th> 164 + <th style="width:4.5rem">{ localizer.T("time") }</th> 165 + <th style="width:4rem">{ localizer.T("iters") }</th> 183 166 </tr> 184 167 <tr id="table-header-compare" style="display:none"> 185 - <th style="width:4.5rem">Time A</th> 186 - <th style="width:4rem">Iters A</th> 187 - <th style="width:4.5rem">Time B</th> 188 - <th style="width:4rem">Iters B</th> 168 + <th style="width:4.5rem">{ localizer.T("time_a") }</th> 169 + <th style="width:4rem">{ localizer.T("iters_a") }</th> 170 + <th style="width:4.5rem">{ localizer.T("time_b") }</th> 171 + <th style="width:4rem">{ localizer.T("iters_b") }</th> 189 172 </tr> 190 173 </thead> 191 174 <tbody ··· 195 178 </table> 196 179 <div class="centered-div"> 197 180 <img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> 198 - <p id="status" style="max-width:256px">Loading...</p> 181 + <p id="status" style="max-width:256px">{ localizer.T("loading") }</p> 199 182 <script async type="module" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version }></script> 200 183 <div id="sparkline"></div> 201 184 <noscript> 202 - <p>Running the benchmark tool requires JavaScript to be enabled.</p> 185 + <p>{ localizer.T("benchmark_requires_js") }</p> 203 186 </noscript> 204 187 </div> 205 188 </div> 206 189 <form id="controls" style="position:fixed;top:0.5rem;right:0.5rem"> 207 190 <div style="display:flex;justify-content:end"> 208 - <label for="difficulty-input" style="margin-right:0.5rem">Difficulty:</label> 191 + <label for="difficulty-input" style="margin-right:0.5rem">{ localizer.T("difficulty") }</label> 209 192 <input id="difficulty-input" type="number" name="difficulty" style="width:3rem"/> 210 193 </div> 211 194 <div style="margin-top:0.25rem;display:flex;justify-content:end"> 212 - <label for="algorithm-select" style="margin-right:0.5rem">Algorithm:</label> 195 + <label for="algorithm-select" style="margin-right:0.5rem">{ localizer.T("algorithm") }</label> 213 196 <select id="algorithm-select" name="algorithm"></select> 214 197 </div> 215 198 <div style="margin-top:0.25rem;display:flex;justify-content:end"> 216 - <label for="compare-select" style="margin-right:0.5rem">Compare:</label> 199 + <label for="compare-select" style="margin-right:0.5rem">{ localizer.T("compare") }</label> 217 200 <select id="compare-select" name="compare"> 218 201 <option value="NONE">-</option> 219 202 </select>
+458 -93
web/index_templ.go
··· 11 11 import ( 12 12 "fmt" 13 13 "github.com/TecharoHQ/anubis" 14 + "github.com/TecharoHQ/anubis/lib/localization" 14 15 "github.com/TecharoHQ/anubis/lib/policy/config" 15 16 "github.com/TecharoHQ/anubis/xess" 16 17 ) 17 18 18 - func base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string) templ.Component { 19 + func base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) templ.Component { 19 20 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 20 21 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 21 22 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { ··· 43 44 var templ_7745c5c3_Var2 string 44 45 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) 45 46 if templ_7745c5c3_Err != nil { 46 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 14, Col: 17} 47 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 15, Col: 17} 47 48 } 48 49 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 49 50 if templ_7745c5c3_Err != nil { ··· 56 57 var templ_7745c5c3_Var3 string 57 58 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + xess.URL) 58 59 if templ_7745c5c3_Err != nil { 59 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 15, Col: 61} 60 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 16, Col: 61} 60 61 } 61 62 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 62 63 if templ_7745c5c3_Err != nil { ··· 74 75 var templ_7745c5c3_Var4 string 75 76 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(key) 76 77 if templ_7745c5c3_Err != nil { 77 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 19, Col: 24} 78 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 20, Col: 24} 78 79 } 79 80 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 80 81 if templ_7745c5c3_Err != nil { ··· 87 88 var templ_7745c5c3_Var5 string 88 89 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(value) 89 90 if templ_7745c5c3_Err != nil { 90 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 19, Col: 42} 91 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 20, Col: 42} 91 92 } 92 93 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 93 94 if templ_7745c5c3_Err != nil { ··· 123 124 var templ_7745c5c3_Var6 string 124 125 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(title) 125 126 if templ_7745c5c3_Err != nil { 126 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 68, Col: 49} 127 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 69, Col: 49} 127 128 } 128 129 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 129 130 if templ_7745c5c3_Err != nil { ··· 137 138 if templ_7745c5c3_Err != nil { 138 139 return templ_7745c5c3_Err 139 140 } 140 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<footer><center><p>Protected by <a href=\"https://github.com/TecharoHQ/anubis\">Anubis</a> from <a href=\"https://techaro.lol\">Techaro</a>. Made with ❤️ in 🇨🇦.</p><p>Mascot design by <a href=\"https://bsky.app/profile/celphase.bsky.social\">CELPHASE</a>.</p>") 141 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<footer><center><p>") 142 + if templ_7745c5c3_Err != nil { 143 + return templ_7745c5c3_Err 144 + } 145 + var templ_7745c5c3_Var7 string 146 + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("protected_by")) 147 + if templ_7745c5c3_Err != nil { 148 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 75, Col: 36} 149 + } 150 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 151 + if templ_7745c5c3_Err != nil { 152 + return templ_7745c5c3_Err 153 + } 154 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " <a href=\"https://github.com/TecharoHQ/anubis\">Anubis</a> from <a href=\"https://techaro.lol\">Techaro</a>. ") 155 + if templ_7745c5c3_Err != nil { 156 + return templ_7745c5c3_Err 157 + } 158 + var templ_7745c5c3_Var8 string 159 + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("made_with")) 160 + if templ_7745c5c3_Err != nil { 161 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 77, Col: 40} 162 + } 163 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) 164 + if templ_7745c5c3_Err != nil { 165 + return templ_7745c5c3_Err 166 + } 167 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ".</p><p>") 168 + if templ_7745c5c3_Err != nil { 169 + return templ_7745c5c3_Err 170 + } 171 + var templ_7745c5c3_Var9 string 172 + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("mascot_design")) 173 + if templ_7745c5c3_Err != nil { 174 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 79, Col: 39} 175 + } 176 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 177 + if templ_7745c5c3_Err != nil { 178 + return templ_7745c5c3_Err 179 + } 180 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " <a href=\"https://bsky.app/profile/celphase.bsky.social\">") 181 + if templ_7745c5c3_Err != nil { 182 + return templ_7745c5c3_Err 183 + } 184 + var templ_7745c5c3_Var10 string 185 + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("celphase")) 186 + if templ_7745c5c3_Err != nil { 187 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 79, Col: 123} 188 + } 189 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 190 + if templ_7745c5c3_Err != nil { 191 + return templ_7745c5c3_Err 192 + } 193 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</a>.</p>") 141 194 if templ_7745c5c3_Err != nil { 142 195 return templ_7745c5c3_Err 143 196 } 144 197 if impressum != nil { 145 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p>") 198 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<p>") 146 199 if templ_7745c5c3_Err != nil { 147 200 return templ_7745c5c3_Err 148 201 } ··· 150 203 if templ_7745c5c3_Err != nil { 151 204 return templ_7745c5c3_Err 152 205 } 153 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "-- <a href=\"") 206 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "-- <a href=\"") 154 207 if templ_7745c5c3_Err != nil { 155 208 return templ_7745c5c3_Err 156 209 } 157 - var templ_7745c5c3_Var7 templ.SafeURL 158 - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("%simprint", anubis.APIPrefix))) 210 + var templ_7745c5c3_Var11 templ.SafeURL 211 + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("%simprint", anubis.APIPrefix))) 159 212 if templ_7745c5c3_Err != nil { 160 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 81, Col: 70} 213 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 83, Col: 78} 161 214 } 162 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 215 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 163 216 if templ_7745c5c3_Err != nil { 164 217 return templ_7745c5c3_Err 165 218 } 166 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">Imprint</a></p>") 219 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">Imprint</a></p>") 167 220 if templ_7745c5c3_Err != nil { 168 221 return templ_7745c5c3_Err 169 222 } 170 223 } 171 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</center></footer></main></body></html>") 224 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</center></footer></main></body></html>") 172 225 if templ_7745c5c3_Err != nil { 173 226 return templ_7745c5c3_Err 174 227 } ··· 176 229 }) 177 230 } 178 231 179 - func index() templ.Component { 232 + func index(localizer *localization.SimpleLocalizer) templ.Component { 180 233 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 181 234 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 182 235 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { ··· 192 245 }() 193 246 } 194 247 ctx = templ.InitializeContext(ctx) 195 - templ_7745c5c3_Var8 := templ.GetChildren(ctx) 196 - if templ_7745c5c3_Var8 == nil { 197 - templ_7745c5c3_Var8 = templ.NopComponent 248 + templ_7745c5c3_Var12 := templ.GetChildren(ctx) 249 + if templ_7745c5c3_Var12 == nil { 250 + templ_7745c5c3_Var12 = templ.NopComponent 198 251 } 199 252 ctx = templ.ClearChildren(ctx) 200 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"") 253 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"") 201 254 if templ_7745c5c3_Err != nil { 202 255 return templ_7745c5c3_Err 203 256 } 204 - var templ_7745c5c3_Var9 string 205 - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) 257 + var templ_7745c5c3_Var13 string 258 + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) 206 259 if templ_7745c5c3_Err != nil { 207 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 92, Col: 165} 260 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 95, Col: 165} 208 261 } 209 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 262 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) 210 263 if templ_7745c5c3_Err != nil { 211 264 return templ_7745c5c3_Err 212 265 } 213 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"> <img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"") 266 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\"> <img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"") 214 267 if templ_7745c5c3_Err != nil { 215 268 return templ_7745c5c3_Err 216 269 } 217 - var templ_7745c5c3_Var10 string 218 - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version) 270 + var templ_7745c5c3_Var14 string 271 + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version) 219 272 if templ_7745c5c3_Err != nil { 220 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 93, Col: 174} 273 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 96, Col: 174} 221 274 } 222 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 275 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) 223 276 if templ_7745c5c3_Err != nil { 224 277 return templ_7745c5c3_Err 225 278 } 226 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"><p id=\"status\">Loading...</p><script async type=\"module\" src=\"") 279 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\"><p id=\"status\">") 227 280 if templ_7745c5c3_Err != nil { 228 281 return templ_7745c5c3_Err 229 282 } 230 - var templ_7745c5c3_Var11 string 231 - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version) 283 + var templ_7745c5c3_Var15 string 284 + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading")) 232 285 if templ_7745c5c3_Err != nil { 233 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 95, Col: 136} 286 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 97, Col: 41} 234 287 } 235 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 288 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) 236 289 if templ_7745c5c3_Err != nil { 237 290 return templ_7745c5c3_Err 238 291 } 239 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"></script><div id=\"progress\" role=\"progressbar\" aria-labelledby=\"status\"><div class=\"bar-inner\"></div></div><details><summary>Why am I seeing this?</summary><p>You are seeing this because the administrator of this website has set up <a href=\"https://github.com/TecharoHQ/anubis\">Anubis</a> to protect the server against the scourge of <a href=\"https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/\">AI companies aggressively scraping websites</a>. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.</p><p>Anubis is a compromise. Anubis uses a <a href=\"https://anubis.techaro.lol/docs/design/why-proof-of-work\">Proof-of-Work</a> scheme in the vein of <a href=\"https://en.wikipedia.org/wiki/Hashcash\">Hashcash</a>, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.</p><p>Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.</p><p>Please note that Anubis requires the use of modern JavaScript features that plugins like <a href=\"https://jshelter.org/\">JShelter</a> will disable. Please disable JShelter or other such plugins for this domain.</p><p>This website is running Anubis version <code>") 292 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p><script async type=\"module\" src=\"") 240 293 if templ_7745c5c3_Err != nil { 241 294 return templ_7745c5c3_Err 242 295 } 243 - var templ_7745c5c3_Var12 string 244 - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.Version) 296 + var templ_7745c5c3_Var16 string 297 + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version) 245 298 if templ_7745c5c3_Err != nil { 246 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 131, Col: 67} 299 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 98, Col: 136} 247 300 } 248 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) 301 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) 249 302 if templ_7745c5c3_Err != nil { 250 303 return templ_7745c5c3_Err 251 304 } 252 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</code>.</p></details><noscript><p>Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works. A no-JS solution is a work-in-progress.</p></noscript><div id=\"testarea\"></div></div>") 305 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"></script><div id=\"progress\" role=\"progressbar\" aria-labelledby=\"status\"><div class=\"bar-inner\"></div></div><details><summary>") 306 + if templ_7745c5c3_Err != nil { 307 + return templ_7745c5c3_Err 308 + } 309 + var templ_7745c5c3_Var17 string 310 + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("why_am_i_seeing")) 311 + if templ_7745c5c3_Err != nil { 312 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 103, Col: 44} 313 + } 314 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) 315 + if templ_7745c5c3_Err != nil { 316 + return templ_7745c5c3_Err 317 + } 318 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</summary><p>") 319 + if templ_7745c5c3_Err != nil { 320 + return templ_7745c5c3_Err 321 + } 322 + var templ_7745c5c3_Var18 string 323 + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("ai_companies_explanation")) 324 + if templ_7745c5c3_Err != nil { 325 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 105, Col: 45} 326 + } 327 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) 328 + if templ_7745c5c3_Err != nil { 329 + return templ_7745c5c3_Err 330 + } 331 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</p><p>") 332 + if templ_7745c5c3_Err != nil { 333 + return templ_7745c5c3_Err 334 + } 335 + var templ_7745c5c3_Var19 string 336 + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("anubis_compromise")) 337 + if templ_7745c5c3_Err != nil { 338 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 108, Col: 38} 339 + } 340 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) 341 + if templ_7745c5c3_Err != nil { 342 + return templ_7745c5c3_Err 343 + } 344 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</p><p>") 345 + if templ_7745c5c3_Err != nil { 346 + return templ_7745c5c3_Err 347 + } 348 + var templ_7745c5c3_Var20 string 349 + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("hack_purpose")) 350 + if templ_7745c5c3_Err != nil { 351 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 111, Col: 33} 352 + } 353 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) 354 + if templ_7745c5c3_Err != nil { 355 + return templ_7745c5c3_Err 356 + } 357 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p><p>") 358 + if templ_7745c5c3_Err != nil { 359 + return templ_7745c5c3_Err 360 + } 361 + var templ_7745c5c3_Var21 string 362 + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("jshelter_note")) 363 + if templ_7745c5c3_Err != nil { 364 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 114, Col: 34} 365 + } 366 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) 367 + if templ_7745c5c3_Err != nil { 368 + return templ_7745c5c3_Err 369 + } 370 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</p><p>") 371 + if templ_7745c5c3_Err != nil { 372 + return templ_7745c5c3_Err 373 + } 374 + var templ_7745c5c3_Var22 string 375 + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("version_info")) 376 + if templ_7745c5c3_Err != nil { 377 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 116, Col: 35} 378 + } 379 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) 380 + if templ_7745c5c3_Err != nil { 381 + return templ_7745c5c3_Err 382 + } 383 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " <code>") 384 + if templ_7745c5c3_Err != nil { 385 + return templ_7745c5c3_Err 386 + } 387 + var templ_7745c5c3_Var23 string 388 + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.Version) 389 + if templ_7745c5c3_Err != nil { 390 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 116, Col: 60} 391 + } 392 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) 393 + if templ_7745c5c3_Err != nil { 394 + return templ_7745c5c3_Err 395 + } 396 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</code>.</p></details><noscript><p>") 397 + if templ_7745c5c3_Err != nil { 398 + return templ_7745c5c3_Err 399 + } 400 + var templ_7745c5c3_Var24 string 401 + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("javascript_required")) 402 + if templ_7745c5c3_Err != nil { 403 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 120, Col: 40} 404 + } 405 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) 406 + if templ_7745c5c3_Err != nil { 407 + return templ_7745c5c3_Err 408 + } 409 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</p></noscript><div id=\"testarea\"></div></div>") 253 410 if templ_7745c5c3_Err != nil { 254 411 return templ_7745c5c3_Err 255 412 } ··· 257 414 }) 258 415 } 259 416 260 - func errorPage(message string, mail string) templ.Component { 417 + func errorPage(message string, mail string, localizer *localization.SimpleLocalizer) templ.Component { 261 418 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 262 419 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 263 420 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { ··· 273 430 }() 274 431 } 275 432 ctx = templ.InitializeContext(ctx) 276 - templ_7745c5c3_Var13 := templ.GetChildren(ctx) 277 - if templ_7745c5c3_Var13 == nil { 278 - templ_7745c5c3_Var13 = templ.NopComponent 433 + templ_7745c5c3_Var25 := templ.GetChildren(ctx) 434 + if templ_7745c5c3_Var25 == nil { 435 + templ_7745c5c3_Var25 = templ.NopComponent 279 436 } 280 437 ctx = templ.ClearChildren(ctx) 281 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"centered-div\"><img id=\"image\" alt=\"Sad Anubis\" style=\"width:100%;max-width:256px;\" src=\"") 438 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div class=\"centered-div\"><img id=\"image\" alt=\"Sad Anubis\" style=\"width:100%;max-width:256px;\" src=\"") 282 439 if templ_7745c5c3_Err != nil { 283 440 return templ_7745c5c3_Err 284 441 } 285 - var templ_7745c5c3_Var14 string 286 - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version) 442 + var templ_7745c5c3_Var26 string 443 + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version) 287 444 if templ_7745c5c3_Err != nil { 288 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 146, Col: 181} 445 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 129, Col: 181} 289 446 } 290 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) 447 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) 291 448 if templ_7745c5c3_Err != nil { 292 449 return templ_7745c5c3_Err 293 450 } 294 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\"><p>") 451 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"><p>") 295 452 if templ_7745c5c3_Err != nil { 296 453 return templ_7745c5c3_Err 297 454 } 298 - var templ_7745c5c3_Var15 string 299 - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(message) 455 + var templ_7745c5c3_Var27 string 456 + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(message) 300 457 if templ_7745c5c3_Err != nil { 301 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 147, Col: 14} 458 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 130, Col: 14} 459 + } 460 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) 461 + if templ_7745c5c3_Err != nil { 462 + return templ_7745c5c3_Err 463 + } 464 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ".</p><button onClick=\"window.location.reload();\">") 465 + if templ_7745c5c3_Err != nil { 466 + return templ_7745c5c3_Err 467 + } 468 + var templ_7745c5c3_Var28 string 469 + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("try_again")) 470 + if templ_7745c5c3_Err != nil { 471 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 131, Col: 72} 302 472 } 303 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) 473 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) 304 474 if templ_7745c5c3_Err != nil { 305 475 return templ_7745c5c3_Err 306 476 } 307 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ".</p><button onClick=\"window.location.reload();\">Try again</button> ") 477 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</button> ") 308 478 if templ_7745c5c3_Err != nil { 309 479 return templ_7745c5c3_Err 310 480 } 311 481 if mail != "" { 312 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p><a href=\"/\">Go home</a> or if you believe you should not be blocked, please contact the webmaster at <a href=\"") 482 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<p><a href=\"/\">") 483 + if templ_7745c5c3_Err != nil { 484 + return templ_7745c5c3_Err 485 + } 486 + var templ_7745c5c3_Var29 string 487 + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home")) 488 + if templ_7745c5c3_Err != nil { 489 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 134, Col: 40} 490 + } 491 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) 492 + if templ_7745c5c3_Err != nil { 493 + return templ_7745c5c3_Err 494 + } 495 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</a> ") 496 + if templ_7745c5c3_Err != nil { 497 + return templ_7745c5c3_Err 498 + } 499 + var templ_7745c5c3_Var30 string 500 + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("contact_webmaster")) 501 + if templ_7745c5c3_Err != nil { 502 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 134, Col: 81} 503 + } 504 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) 313 505 if templ_7745c5c3_Err != nil { 314 506 return templ_7745c5c3_Err 315 507 } 316 - var templ_7745c5c3_Var16 templ.SafeURL 317 - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs("mailto:" + templ.SafeURL(mail)) 508 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " <a href=\"") 509 + if templ_7745c5c3_Err != nil { 510 + return templ_7745c5c3_Err 511 + } 512 + var templ_7745c5c3_Var31 templ.SafeURL 513 + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs("mailto:" + templ.SafeURL(mail)) 318 514 if templ_7745c5c3_Err != nil { 319 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 152, Col: 45} 515 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 135, Col: 45} 320 516 } 321 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) 517 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) 322 518 if templ_7745c5c3_Err != nil { 323 519 return templ_7745c5c3_Err 324 520 } 325 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">") 521 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">") 326 522 if templ_7745c5c3_Err != nil { 327 523 return templ_7745c5c3_Err 328 524 } 329 - var templ_7745c5c3_Var17 string 330 - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(mail) 525 + var templ_7745c5c3_Var32 string 526 + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(mail) 331 527 if templ_7745c5c3_Err != nil { 332 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 153, Col: 11} 528 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 136, Col: 11} 333 529 } 334 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) 530 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) 335 531 if templ_7745c5c3_Err != nil { 336 532 return templ_7745c5c3_Err 337 533 } 338 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</a></p>") 534 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</a></p>") 339 535 if templ_7745c5c3_Err != nil { 340 536 return templ_7745c5c3_Err 341 537 } 342 538 } else { 343 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<p><a href=\"/\">Go home</a></p>") 539 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<p><a href=\"/\">") 540 + if templ_7745c5c3_Err != nil { 541 + return templ_7745c5c3_Err 542 + } 543 + var templ_7745c5c3_Var33 string 544 + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home")) 545 + if templ_7745c5c3_Err != nil { 546 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 140, Col: 42} 547 + } 548 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) 549 + if templ_7745c5c3_Err != nil { 550 + return templ_7745c5c3_Err 551 + } 552 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</a></p>") 344 553 if templ_7745c5c3_Err != nil { 345 554 return templ_7745c5c3_Err 346 555 } 347 556 } 348 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div>") 557 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div>") 349 558 if templ_7745c5c3_Err != nil { 350 559 return templ_7745c5c3_Err 351 560 } ··· 353 562 }) 354 563 } 355 564 356 - func StaticHappy() templ.Component { 565 + func StaticHappy(localizer *localization.SimpleLocalizer) templ.Component { 357 566 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 358 567 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 359 568 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { ··· 369 578 }() 370 579 } 371 580 ctx = templ.InitializeContext(ctx) 372 - templ_7745c5c3_Var18 := templ.GetChildren(ctx) 373 - if templ_7745c5c3_Var18 == nil { 374 - templ_7745c5c3_Var18 = templ.NopComponent 581 + templ_7745c5c3_Var34 := templ.GetChildren(ctx) 582 + if templ_7745c5c3_Var34 == nil { 583 + templ_7745c5c3_Var34 = templ.NopComponent 375 584 } 376 585 ctx = templ.ClearChildren(ctx) 377 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"centered-div\"><img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"") 586 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"centered-div\"><img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"") 378 587 if templ_7745c5c3_Err != nil { 379 588 return templ_7745c5c3_Err 380 589 } 381 - var templ_7745c5c3_Var19 string 382 - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + 590 + var templ_7745c5c3_Var35 string 591 + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + 383 592 anubis.Version) 384 593 if templ_7745c5c3_Err != nil { 385 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 168, Col: 18} 594 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 151, Col: 18} 595 + } 596 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) 597 + if templ_7745c5c3_Err != nil { 598 + return templ_7745c5c3_Err 599 + } 600 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\"><p>") 601 + if templ_7745c5c3_Err != nil { 602 + return templ_7745c5c3_Err 603 + } 604 + var templ_7745c5c3_Var36 string 605 + templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("static_check_endpoint")) 606 + if templ_7745c5c3_Err != nil { 607 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 153, Col: 43} 386 608 } 387 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) 609 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) 388 610 if templ_7745c5c3_Err != nil { 389 611 return templ_7745c5c3_Err 390 612 } 391 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\"><p>This is just a check endpoint for your reverse proxy to use.</p></div>") 613 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</p></div>") 392 614 if templ_7745c5c3_Err != nil { 393 615 return templ_7745c5c3_Err 394 616 } ··· 396 618 }) 397 619 } 398 620 399 - func bench() templ.Component { 621 + func bench(localizer *localization.SimpleLocalizer) templ.Component { 400 622 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 401 623 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 402 624 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { ··· 412 634 }() 413 635 } 414 636 ctx = templ.InitializeContext(ctx) 415 - templ_7745c5c3_Var20 := templ.GetChildren(ctx) 416 - if templ_7745c5c3_Var20 == nil { 417 - templ_7745c5c3_Var20 = templ.NopComponent 637 + templ_7745c5c3_Var37 := templ.GetChildren(ctx) 638 + if templ_7745c5c3_Var37 == nil { 639 + templ_7745c5c3_Var37 = templ.NopComponent 418 640 } 419 641 ctx = templ.ClearChildren(ctx) 420 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">Time</th><th style=\"width:4rem\">Iters</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">Time A</th><th style=\"width:4rem\">Iters A</th><th style=\"width:4.5rem\">Time B</th><th style=\"width:4rem\">Iters B</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"") 642 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">") 643 + if templ_7745c5c3_Err != nil { 644 + return templ_7745c5c3_Err 645 + } 646 + var templ_7745c5c3_Var38 string 647 + templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time")) 648 + if templ_7745c5c3_Err != nil { 649 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 164, Col: 51} 650 + } 651 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) 652 + if templ_7745c5c3_Err != nil { 653 + return templ_7745c5c3_Err 654 + } 655 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</th><th style=\"width:4rem\">") 656 + if templ_7745c5c3_Err != nil { 657 + return templ_7745c5c3_Err 658 + } 659 + var templ_7745c5c3_Var39 string 660 + templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters")) 661 + if templ_7745c5c3_Err != nil { 662 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 165, Col: 50} 663 + } 664 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) 665 + if templ_7745c5c3_Err != nil { 666 + return templ_7745c5c3_Err 667 + } 668 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">") 669 + if templ_7745c5c3_Err != nil { 670 + return templ_7745c5c3_Err 671 + } 672 + var templ_7745c5c3_Var40 string 673 + templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_a")) 674 + if templ_7745c5c3_Err != nil { 675 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 168, Col: 53} 676 + } 677 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) 678 + if templ_7745c5c3_Err != nil { 679 + return templ_7745c5c3_Err 680 + } 681 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</th><th style=\"width:4rem\">") 682 + if templ_7745c5c3_Err != nil { 683 + return templ_7745c5c3_Err 684 + } 685 + var templ_7745c5c3_Var41 string 686 + templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_a")) 687 + if templ_7745c5c3_Err != nil { 688 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 169, Col: 52} 689 + } 690 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) 691 + if templ_7745c5c3_Err != nil { 692 + return templ_7745c5c3_Err 693 + } 694 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</th><th style=\"width:4.5rem\">") 695 + if templ_7745c5c3_Err != nil { 696 + return templ_7745c5c3_Err 697 + } 698 + var templ_7745c5c3_Var42 string 699 + templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_b")) 700 + if templ_7745c5c3_Err != nil { 701 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 170, Col: 53} 702 + } 703 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) 704 + if templ_7745c5c3_Err != nil { 705 + return templ_7745c5c3_Err 706 + } 707 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</th><th style=\"width:4rem\">") 708 + if templ_7745c5c3_Err != nil { 709 + return templ_7745c5c3_Err 710 + } 711 + var templ_7745c5c3_Var43 string 712 + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_b")) 713 + if templ_7745c5c3_Err != nil { 714 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 171, Col: 52} 715 + } 716 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) 717 + if templ_7745c5c3_Err != nil { 718 + return templ_7745c5c3_Err 719 + } 720 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"") 721 + if templ_7745c5c3_Err != nil { 722 + return templ_7745c5c3_Err 723 + } 724 + var templ_7745c5c3_Var44 string 725 + templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) 726 + if templ_7745c5c3_Err != nil { 727 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 180, Col: 166} 728 + } 729 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) 730 + if templ_7745c5c3_Err != nil { 731 + return templ_7745c5c3_Err 732 + } 733 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\"><p id=\"status\" style=\"max-width:256px\">") 734 + if templ_7745c5c3_Err != nil { 735 + return templ_7745c5c3_Err 736 + } 737 + var templ_7745c5c3_Var45 string 738 + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading")) 739 + if templ_7745c5c3_Err != nil { 740 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 181, Col: 66} 741 + } 742 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) 743 + if templ_7745c5c3_Err != nil { 744 + return templ_7745c5c3_Err 745 + } 746 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</p><script async type=\"module\" src=\"") 421 747 if templ_7745c5c3_Err != nil { 422 748 return templ_7745c5c3_Err 423 749 } 424 - var templ_7745c5c3_Var21 string 425 - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) 750 + var templ_7745c5c3_Var46 string 751 + templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version) 426 752 if templ_7745c5c3_Err != nil { 427 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 197, Col: 166} 753 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 182, Col: 138} 428 754 } 429 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) 755 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) 430 756 if templ_7745c5c3_Err != nil { 431 757 return templ_7745c5c3_Err 432 758 } 433 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\"><p id=\"status\" style=\"max-width:256px\">Loading...</p><script async type=\"module\" src=\"") 759 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\"></script><div id=\"sparkline\"></div><noscript><p>") 434 760 if templ_7745c5c3_Err != nil { 435 761 return templ_7745c5c3_Err 436 762 } 437 - var templ_7745c5c3_Var22 string 438 - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version) 763 + var templ_7745c5c3_Var47 string 764 + templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("benchmark_requires_js")) 765 + if templ_7745c5c3_Err != nil { 766 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 185, Col: 45} 767 + } 768 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) 439 769 if templ_7745c5c3_Err != nil { 440 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 199, Col: 138} 770 + return templ_7745c5c3_Err 441 771 } 442 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) 772 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">") 443 773 if templ_7745c5c3_Err != nil { 444 774 return templ_7745c5c3_Err 445 775 } 446 - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"></script><div id=\"sparkline\"></div><noscript><p>Running the benchmark tool requires JavaScript to be enabled.</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">Difficulty:</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">Algorithm:</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">Compare:</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>") 776 + var templ_7745c5c3_Var48 string 777 + templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("difficulty")) 778 + if templ_7745c5c3_Err != nil { 779 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 191, Col: 88} 780 + } 781 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) 782 + if templ_7745c5c3_Err != nil { 783 + return templ_7745c5c3_Err 784 + } 785 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">") 786 + if templ_7745c5c3_Err != nil { 787 + return templ_7745c5c3_Err 788 + } 789 + var templ_7745c5c3_Var49 string 790 + templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("algorithm")) 791 + if templ_7745c5c3_Err != nil { 792 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 195, Col: 87} 793 + } 794 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) 795 + if templ_7745c5c3_Err != nil { 796 + return templ_7745c5c3_Err 797 + } 798 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">") 799 + if templ_7745c5c3_Err != nil { 800 + return templ_7745c5c3_Err 801 + } 802 + var templ_7745c5c3_Var50 string 803 + templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("compare")) 804 + if templ_7745c5c3_Err != nil { 805 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 199, Col: 83} 806 + } 807 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) 808 + if templ_7745c5c3_Err != nil { 809 + return templ_7745c5c3_Err 810 + } 811 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>") 447 812 if templ_7745c5c3_Err != nil { 448 813 return templ_7745c5c3_Err 449 814 }
+94 -34
web/js/main.mjs
··· 18 18 cacheBuster, 19 19 }); 20 20 21 - const dependencies = [ 22 - { 23 - name: "WebCrypto", 24 - msg: "Your browser doesn't have a functioning web.crypto element. Are you viewing this over a secure context?", 25 - value: window.crypto, 26 - }, 27 - { 28 - name: "Web Workers", 29 - msg: "Your browser doesn't support web workers (Anubis uses this to avoid freezing your browser). Do you have a plugin like JShelter installed?", 30 - value: window.Worker, 31 - }, 32 - { 33 - name: "Cookies", 34 - msg: "Your browser doesn't store cookies. Anubis uses cookies to determine which clients have passed challenges by storing a signed token in a cookie. Please enable storing cookies for this domain. The names of the cookies Anubis stores may vary without notice. Cookie names and values are not part of the public API.", 35 - value: navigator.cookieEnabled, 36 - }, 37 - ]; 21 + // Detect available languages by loading the manifest 22 + const getAvailableLanguages = async () => { 23 + const basePrefix = JSON.parse( 24 + document.getElementById("anubis_base_prefix").textContent, 25 + ); 26 + 27 + try { 28 + const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`); 29 + if (response.ok) { 30 + const manifest = await response.json(); 31 + return manifest.supportedLanguages || ['en']; 32 + } 33 + } catch (error) { 34 + console.warn('Failed to load language manifest, falling back to default languages'); 35 + } 36 + 37 + // Fallback to default languages if manifest loading fails 38 + return ['en']; 39 + }; 40 + 41 + // Detect browser language 42 + const getBrowserLanguage = async () => { 43 + const lang = navigator.language || navigator.userLanguage; 44 + const availableLanguages = await getAvailableLanguages(); 45 + 46 + // Extract the language code (first 2 characters) 47 + const langCode = lang.substring(0, 2).toLowerCase(); 48 + 49 + // Return the language if supported, or use English 50 + return availableLanguages.includes(langCode) ? langCode : 'en'; 51 + }; 52 + 53 + // Load translations from JSON files 54 + const loadTranslations = async (lang) => { 55 + const basePrefix = JSON.parse( 56 + document.getElementById("anubis_base_prefix").textContent, 57 + ); 58 + try { 59 + const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`); 60 + return await response.json(); 61 + } catch (error) { 62 + console.warn(`Failed to load translations for ${lang}, falling back to English`); 63 + if (lang !== 'en') { 64 + return await loadTranslations('en'); 65 + } 66 + throw error; 67 + } 68 + }; 69 + 70 + let translations = {}; 71 + let currentLang; 72 + 73 + // Initialize translations 74 + const initTranslations = async () => { 75 + currentLang = await getBrowserLanguage(); 76 + translations = await loadTranslations(currentLang); 77 + }; 78 + 79 + const t = (key) => translations[`js_${key}`] || translations[key] || key; 38 80 39 81 (async () => { 82 + // Initialize translations first 83 + await initTranslations(); 84 + 85 + const dependencies = [ 86 + { 87 + name: "WebCrypto", 88 + msg: t('web_crypto_error'), 89 + value: window.crypto, 90 + }, 91 + { 92 + name: "Web Workers", 93 + msg: t('web_workers_error'), 94 + value: window.Worker, 95 + }, 96 + { 97 + name: "Cookies", 98 + msg: t('cookies_error'), 99 + value: navigator.cookieEnabled, 100 + }, 101 + ]; 40 102 const status = document.getElementById("status"); 41 103 const image = document.getElementById("image"); 42 104 const title = document.getElementById("title"); ··· 67 129 68 130 if (!window.isSecureContext) { 69 131 ohNoes({ 70 - titleMsg: "Your context is not secure!", 71 - statusMsg: `Try connecting over HTTPS or let the admin know to set up HTTPS. For more information, see <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure">MDN</a>.`, 132 + titleMsg: t('context_not_secure'), 133 + statusMsg: t('context_not_secure_msg'), 72 134 imageSrc: imageURL("reject", anubisVersion, basePrefix), 73 135 }); 74 136 return; 75 137 } 76 138 77 - status.innerHTML = "Calculating..."; 139 + status.innerHTML = t('calculating'); 78 140 79 141 for (const { value, name, msg } of dependencies) { 80 142 if (!value) { 81 143 ohNoes({ 82 - titleMsg: `Missing feature ${name}`, 144 + titleMsg: `${t('missing_feature')} ${name}`, 83 145 statusMsg: msg, 84 146 imageSrc: imageURL("reject", anubisVersion, basePrefix), 85 147 }); ··· 94 156 const process = algorithms[rules.algorithm]; 95 157 if (!process) { 96 158 ohNoes({ 97 - titleMsg: "Challenge error!", 98 - statusMsg: `Failed to resolve check algorithm. You may want to reload the page.`, 159 + titleMsg: t('challenge_error'), 160 + statusMsg: t('challenge_error_msg'), 99 161 imageSrc: imageURL("reject", anubisVersion, basePrefix), 100 162 }); 101 163 return; 102 164 } 103 165 104 - status.innerHTML = `Calculating...<br/>Difficulty: ${rules.report_as}, `; 166 + status.innerHTML = `${t('calculating_difficulty')} ${rules.report_as}, `; 105 167 progress.style.display = "inline-block"; 106 168 107 169 // the whole text, including "Speed:", as a single node, because some browsers 108 170 // (Firefox mobile) present screen readers with each node as a separate piece 109 171 // of text. 110 - const rateText = document.createTextNode("Speed: 0kH/s"); 172 + const rateText = document.createTextNode(`${t('speed')} 0kH/s`); 111 173 status.appendChild(rateText); 112 174 113 175 let lastSpeedUpdate = 0; ··· 125 187 // only update the speed every second so it's less visually distracting 126 188 if (delta - lastSpeedUpdate > 1000) { 127 189 lastSpeedUpdate = delta; 128 - rateText.data = `Speed: ${(iters / delta).toFixed(3)}kH/s`; 190 + rateText.data = `${t('speed')} ${(iters / delta).toFixed(3)}kH/s`; 129 191 } 130 192 // the probability of still being on the page is (1 - likelihood) ^ iters. 131 193 // by definition, half of the time the progress bar only gets to half, so ··· 141 203 if (probability < 0.1 && !showingApology) { 142 204 status.append( 143 205 document.createElement("br"), 144 - document.createTextNode( 145 - "Verification is taking longer than expected. Please do not refresh the page.", 146 - ), 206 + document.createTextNode(t('verification_longer')), 147 207 ); 148 208 showingApology = true; 149 209 } ··· 152 212 const t1 = Date.now(); 153 213 console.log({ hash, nonce }); 154 214 155 - title.innerHTML = "Success!"; 156 - status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`; 215 + title.innerHTML = t('success'); 216 + status.innerHTML = `${t('done_took')} ${t1 - t0}ms, ${nonce} ${t('iterations')}`; 157 217 image.src = imageURL("happy", anubisVersion, basePrefix); 158 218 progress.style.display = "none"; 159 219 ··· 174 234 container.style.outlineOffset = "2px"; 175 235 container.style.width = "min(20rem, 90%)"; 176 236 container.style.margin = "1rem auto 2rem"; 177 - container.innerHTML = "I've finished reading, continue →"; 237 + container.innerHTML = t('finished_reading'); 178 238 179 239 function onDetailsExpand() { 180 240 const redir = window.location.href; ··· 205 265 } 206 266 } catch (err) { 207 267 ohNoes({ 208 - titleMsg: "Calculation error!", 209 - statusMsg: `Failed to calculate challenge: ${err.message}`, 268 + titleMsg: t('calculation_error'), 269 + statusMsg: `${t('calculation_error_msg')} ${err.message}`, 210 270 imageSrc: imageURL("reject", anubisVersion, basePrefix), 211 271 }); 212 272 }