From ad5430612fe222fc74f99577ec74373b9f5cdd3e Mon Sep 17 00:00:00 2001 From: Laurent Laffont Date: Fri, 27 Jun 2025 19:49:15 +0200 Subject: [PATCH] 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 * fix(challenge/metarefresh): enable localization Signed-off-by: Xe Iaso * 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 * chore: spelling Signed-off-by: Xe Iaso * chore:(js): fix forbidden patterns Signed-off-by: Xe Iaso * chore: add goi18n to tools Signed-off-by: Xe Iaso * test(lib/localization): dynamically determine the list of supported languages Signed-off-by: Xe Iaso --------- Signed-off-by: Xe Iaso Co-authored-by: Xe Iaso --- .github/actions/spelling/excludes.txt | 2 + .github/actions/spelling/expect.txt | 4 +- .gitignore | 2 + docs/docs/CHANGELOG.md | 2 + go.mod | 4 +- go.sum | 4 + lib/anubis.go | 41 +- lib/challenge/metarefresh/metarefresh.go | 5 +- lib/challenge/metarefresh/metarefresh.templ | 7 +- .../metarefresh/metarefresh_templ.go | 41 +- lib/challenge/proofofwork/proofofwork.go | 4 +- lib/config.go | 3 +- lib/http.go | 27 +- lib/localization/locales/en.json | 63 ++ lib/localization/locales/es.json | 63 ++ lib/localization/locales/fr.json | 63 ++ lib/localization/locales/manifest.json | 3 + lib/localization/localization.go | 100 +++ lib/localization/localization_test.go | 116 +++ web/build.sh | 6 +- web/index.go | 21 +- web/index.templ | 89 +-- web/index_templ.go | 721 +++++++++++++----- web/js/main.mjs | 128 +++- 24 files changed, 1205 insertions(+), 314 deletions(-) create mode 100644 lib/localization/locales/en.json create mode 100644 lib/localization/locales/es.json create mode 100644 lib/localization/locales/fr.json create mode 100644 lib/localization/locales/manifest.json create mode 100644 lib/localization/localization.go create mode 100644 lib/localization/localization_test.go diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 37bb248..0c16101 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -89,3 +89,5 @@ ^lib/policy/config/testdata/bad/unparseable\.json$ ignore$ robots.txt +^lib/localization/locales/.*\.json$ +^lib/localization/.*_test.go$ diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 8c8b2b0..2a3663d 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -33,7 +33,7 @@ Caddyfile caninetools Cardyb celchecker -CELPHASE +celphase cerr certresolver cespare @@ -42,6 +42,7 @@ cgr chainguard chall challengemozilla +Chargement checkpath checkresult chibi @@ -183,6 +184,7 @@ mozilla nbf netsurf nginx +nicksnyder nobots NONINFRINGEMENT nosleep diff --git a/.gitignore b/.gitignore index a716c66..0fc14c2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ node_modules # how does this get here doc/VERSION + +web/static/locales/*.json \ No newline at end of file diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 98f4612..b008a5c 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Implement localization system. Find locale files in lib/localization/locales/. + ## v1.20.0: Thancred Waters The big ticket items are as follows: diff --git a/go.mod b/go.mod index 0f879e9..774c311 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( cel.dev/expr v0.23.1 // indirect dario.cat/mergo v1.0.2 // indirect github.com/AlekSi/pointer v1.2.0 // indirect - github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect @@ -87,6 +87,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/natefinch/atomic v1.0.1 // indirect + github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -133,6 +134,7 @@ require ( tool ( github.com/TecharoHQ/yeet/cmd/yeet github.com/a-h/templ/cmd/templ + github.com/nicksnyder/go-i18n/v2/goi18n github.com/suzuki-shunsuke/pinact/cmd/pinact golang.org/x/tools/cmd/deadcode golang.org/x/tools/cmd/goimports diff --git a/go.sum b/go.sum index 609123c..77c31a5 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= @@ -237,6 +239,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= +github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= diff --git a/lib/anubis.go b/lib/anubis.go index 22c32d5..940be04 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -26,6 +26,7 @@ import ( "github.com/TecharoHQ/anubis/internal/dnsbl" "github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy/checker" "github.com/TecharoHQ/anubis/lib/policy/config" @@ -87,6 +88,8 @@ func (s *Server) getTokenKeyfunc() jwt.Keyfunc { } } + + func (s *Server) challengeFor(r *http.Request, difficulty int) string { var fp [32]byte if len(s.hs512Secret) == 0 { @@ -126,7 +129,8 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS cr, rule, err := s.check(r) if err != nil { lg.Error("check failed", "err", err) - 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\"") + localizer := localization.GetLocalizer(r) + s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy\"", localizer.T("internal_server_error"))) return } @@ -210,6 +214,8 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/" } + localizer := localization.GetLocalizer(r) + switch cr.Rule { case config.RuleAllow: lg.Debug("allowing traffic to origin (explicit)") @@ -220,13 +226,13 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch lg.Info("explicit deny") if rule == nil { lg.Error("rule is nil, cannot calculate checksum") - s.respondWithError(w, r, "Internal Server Error: Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.RuleDeny\"") + s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.RuleDeny\"", localizer.T("internal_server_error"))) return true } hash := rule.Hash() lg.Debug("rule hash", "hash", hash) - s.respondWithStatus(w, r, fmt.Sprintf("Access Denied: error code %s", hash), s.policy.StatusCodes.Deny) + s.respondWithStatus(w, r, fmt.Sprintf("%s %s", localizer.T("access_denied"), hash), s.policy.StatusCodes.Deny) return true case config.RuleChallenge: lg.Debug("challenge requested") @@ -237,7 +243,7 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch default: s.ClearCookie(w, s.cookieName, cookiePath, r.Host) slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule) - 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\"") + s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.Rules\"", localizer.T("internal_server_error"))) return true } return false @@ -258,7 +264,12 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string, if resp != dnsbl.AllGood { lg.Info("DNSBL hit", "status", resp.String()) - 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) + localizer := localization.GetLocalizer(r) + s.respondWithStatus(w, r, fmt.Sprintf("%s: %s, %s https://dronebl.org/lookup?ip=%s", + localizer.T("dronebl_entry"), + resp.String(), + localizer.T("see_dronebl_lookup"), + ip), s.policy.StatusCodes.Deny) return true } } @@ -267,6 +278,7 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string, func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { lg := internal.GetRequestLogger(r) + localizer := localization.GetLocalizer(r) redir := r.FormValue("redir") if redir == "" { @@ -276,7 +288,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { encoder.Encode(struct { Error string `json:"error"` }{ - Error: "Invalid invocation of MakeChallenge", + Error: localizer.T("invalid_invocation"), }) return } @@ -291,7 +303,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { err := encoder.Encode(struct { Error string `json:"error"` }{ - Error: "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"makeChallenge\"", + Error: fmt.Sprintf("%s \"makeChallenge\"", localizer.T("internal_server_error")), }) if err != nil { lg.Error("failed to encode error response", "err", err) @@ -322,6 +334,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { lg := internal.GetRequestLogger(r) + localizer := localization.GetLocalizer(r) // Adjust cookie path if base prefix is not empty cookiePath := "/" @@ -333,7 +346,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { s.ClearCookie(w, s.cookieName, cookiePath, r.Host) s.ClearCookie(w, anubis.TestCookieName, "/", r.Host) lg.Warn("user has cookies disabled, this is not an anubis bug") - 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") + s.respondWithError(w, r, localizer.T("cookies_disabled")) return } @@ -343,7 +356,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { redirURL, err := url.ParseRequestURI(redir) if err != nil { lg.Error("invalid redirect", "err", err) - s.respondWithError(w, r, "Invalid redirect") + s.respondWithError(w, r, localizer.T("invalid_redirect")) return } // used by the path checker rule @@ -351,18 +364,18 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { urlParsed, err := r.URL.Parse(redir) if err != nil { - s.respondWithError(w, r, "Redirect URL not parseable") + s.respondWithError(w, r, localizer.T("redirect_not_parseable")) return } if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host { - s.respondWithError(w, r, "Redirect domain not allowed") + s.respondWithError(w, r, localizer.T("redirect_domain_not_allowed")) return } cr, rule, err := s.check(r) if err != nil { lg.Error("check failed", "err", err) - 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\"") + s.respondWithError(w, r, fmt.Sprintf("%s \"passChallenge\"", localizer.T("internal_server_error"))) return } lg = lg.With("check_result", cr) @@ -370,7 +383,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { impl, ok := challenge.Get(rule.Challenge.Algorithm) if !ok { lg.Error("check failed", "err", err) - 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)) + s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm)) return } @@ -403,7 +416,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { if err != nil { lg.Error("failed to sign JWT", "err", err) s.ClearCookie(w, s.cookieName, cookiePath, r.Host) - s.respondWithError(w, r, "failed to sign JWT") + s.respondWithError(w, r, localizer.T("failed_to_sign_jwt")) return } diff --git a/lib/challenge/metarefresh/metarefresh.go b/lib/challenge/metarefresh/metarefresh.go index 99afc28..68a2ed0 100644 --- a/lib/challenge/metarefresh/metarefresh.go +++ b/lib/challenge/metarefresh/metarefresh.go @@ -8,6 +8,7 @@ import ( "github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/web" "github.com/a-h/templ" @@ -34,7 +35,9 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *challenge.IssueInput) q.Set("challenge", in.Challenge) u.RawQuery = q.Encode() - 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) + loc := localization.GetLocalizer(r) + 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) + if err != nil { return nil, fmt.Errorf("can't render page: %w", err) } diff --git a/lib/challenge/metarefresh/metarefresh.templ b/lib/challenge/metarefresh/metarefresh.templ index 1dba375..e4549b6 100644 --- a/lib/challenge/metarefresh/metarefresh.templ +++ b/lib/challenge/metarefresh/metarefresh.templ @@ -4,14 +4,15 @@ import ( "fmt" "github.com/TecharoHQ/anubis" + "github.com/TecharoHQ/anubis/lib/localization" ) -templ page(challenge, redir string, difficulty int) { +templ page(challenge, redir string, difficulty int, loc *localization.SimpleLocalizer) {
Loading...

-

Please wait a moment while we ensure the security of your connection.

+

{ loc.T("loading") }

+

{ loc.T("connection_security") }

} diff --git a/lib/challenge/metarefresh/metarefresh_templ.go b/lib/challenge/metarefresh/metarefresh_templ.go index 5a24c00..172395a 100644 --- a/lib/challenge/metarefresh/metarefresh_templ.go +++ b/lib/challenge/metarefresh/metarefresh_templ.go @@ -12,9 +12,10 @@ import ( "fmt" "github.com/TecharoHQ/anubis" + "github.com/TecharoHQ/anubis/lib/localization" ) -func page(challenge, redir string, difficulty int) templ.Component { +func page(challenge, redir string, difficulty int, loc *localization.SimpleLocalizer) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -42,7 +43,7 @@ func page(challenge, redir string, difficulty int) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 11, Col: 165} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 165} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -55,26 +56,52 @@ func page(challenge, redir string, difficulty int) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 174} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 13, Col: 174} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">

Loading...

Please wait a moment while we ensure the security of your connection.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var4 string - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir)) + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(loc.T("loading")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 15, Col: 83} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 14, Col: 35} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(loc.T("connection_security")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 15, Col: 35} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/lib/challenge/proofofwork/proofofwork.go b/lib/challenge/proofofwork/proofofwork.go index 9f02ce7..74a3551 100644 --- a/lib/challenge/proofofwork/proofofwork.go +++ b/lib/challenge/proofofwork/proofofwork.go @@ -10,6 +10,7 @@ import ( "github.com/TecharoHQ/anubis/internal" chall "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/web" "github.com/a-h/templ" @@ -29,7 +30,8 @@ func (i *Impl) Setup(mux *http.ServeMux) { } func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) { - component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags) + loc := localization.GetLocalizer(r) + component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), web.Index(loc), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags, loc) if err != nil { return nil, fmt.Errorf("can't render page: %w", err) } diff --git a/lib/config.go b/lib/config.go index 6c832aa..8041da8 100644 --- a/lib/config.go +++ b/lib/config.go @@ -20,6 +20,7 @@ import ( "github.com/TecharoHQ/anubis/internal/dnsbl" "github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/web" @@ -155,7 +156,7 @@ func New(opts Options) (*Server, error) { if opts.Policy.Impressum != nil { registerWithPrefix(anubis.APIPrefix+"imprint", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { templ.Handler( - web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum), + web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)), ).ServeHTTP(w, r) }), "GET") } diff --git a/lib/http.go b/lib/http.go index 664a934..1a5480c 100644 --- a/lib/http.go +++ b/lib/http.go @@ -12,6 +12,7 @@ import ( "github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/web" "github.com/a-h/templ" @@ -83,9 +84,11 @@ func randomChance(n int) bool { } func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) { + localizer := localization.GetLocalizer(r) + if returnHTTPStatusOnly { w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("Authorization required")) + w.Write([]byte(localizer.T("authorization_required"))) return } @@ -93,7 +96,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) { lg.Error("client was given a challenge but does not in fact support gzip compression") - s.respondWithError(w, r, "Client Error: Please ensure your browser is up to date and try again later.") + s.respondWithError(w, r, localizer.T("client_error_browser")) } challengesIssued.WithLabelValues("embedded").Add(1) @@ -118,7 +121,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic impl, ok := challenge.Get(rule.Challenge.Algorithm) if !ok { lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm) - 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)) + s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm)) return } @@ -132,7 +135,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic component, err := impl.Issue(r, lg, in) if err != nil { 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. - s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"") + s.respondWithError(w, r, fmt.Sprintf("%s \"RenderIndex\"", localizer.T("internal_server_error"))) return } @@ -144,8 +147,10 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic } func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) { + localizer := localization.GetLocalizer(r) + templ.Handler( - web.Base("Benchmarking Anubis!", web.Bench(), s.policy.Impressum), + web.Base(localizer.T("benchmarking_anubis"), web.Bench(localizer), s.policy.Impressum, localizer), ).ServeHTTP(w, r) } @@ -154,7 +159,9 @@ func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, messag } func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg string, status int) { - templ.Handler(web.Base("Oh noes!", web.ErrorPage(msg, s.opts.WebmasterEmail), s.policy.Impressum), templ.WithStatus(status)).ServeHTTP(w, r) + localizer := localization.GetLocalizer(r) + + 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) } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -189,15 +196,17 @@ func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request { func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) { if s.next == nil { + localizer := localization.GetLocalizer(r) + redir := r.FormValue("redir") urlParsed, err := r.URL.Parse(redir) if err != nil { - s.respondWithStatus(w, r, "Redirect URL not parseable", http.StatusBadRequest) + s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), http.StatusBadRequest) return } if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host { - s.respondWithStatus(w, r, "Redirect domain not allowed", http.StatusBadRequest) + s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), http.StatusBadRequest) return } @@ -207,7 +216,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) { } templ.Handler( - web.Base("You are not a bot!", web.StaticHappy(), s.policy.Impressum), + web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer), ).ServeHTTP(w, r) } else { requestsProxied.WithLabelValues(r.Host).Inc() diff --git a/lib/localization/locales/en.json b/lib/localization/locales/en.json new file mode 100644 index 0000000..1a9f0e0 --- /dev/null +++ b/lib/localization/locales/en.json @@ -0,0 +1,63 @@ +{ + "loading": "Loading...", + "why_am_i_seeing": "Why am I seeing this?", + "protected_by": "Protected by", + "made_with": "Made with ❤️ in 🇨🇦", + "mascot_design": "Mascot design by", + "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.", + "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.", + "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.", + "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.", + "version_info": "This website is running Anubis version", + "try_again": "Try again", + "go_home": "Go home", + "contact_webmaster": "or if you believe you should not be blocked, please contact the webmaster at", + "connection_security": "Please wait a moment while we ensure the security of your connection.", + "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.", + "benchmark_requires_js": "Running the benchmark tool requires JavaScript to be enabled.", + "difficulty": "Difficulty:", + "algorithm": "Algorithm:", + "compare": "Compare:", + "time": "Time", + "iters": "Iters", + "time_a": "Time A", + "iters_a": "Iters A", + "time_b": "Time B", + "iters_b": "Iters B", + "static_check_endpoint": "This is just a check endpoint for your reverse proxy to use.", + "authorization_required": "Authorization required", + "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", + "access_denied": "Access Denied: error code", + "dronebl_entry": "DroneBL reported an entry", + "see_dronebl_lookup": "see", + "internal_server_error": "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around", + "invalid_redirect": "Invalid redirect", + "redirect_not_parseable": "Redirect URL not parseable", + "redirect_domain_not_allowed": "Redirect domain not allowed", + "failed_to_sign_jwt": "failed to sign JWT", + "invalid_invocation": "Invalid invocation of MakeChallenge", + "client_error_browser": "Client Error: Please ensure your browser is up to date and try again later.", + "oh_noes": "Oh noes!", + "benchmarking_anubis": "Benchmarking Anubis!", + "you_are_not_a_bot": "You are not a bot!", + "making_sure_not_bot": "Making sure you're not a bot!", + "celphase": "CELPHASE", + "js_web_crypto_error": "Your browser doesn't have a functioning web.crypto element. Are you viewing this over a secure context?", + "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?", + "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.", + "js_context_not_secure": "Your context is not secure!", + "js_context_not_secure_msg": "Try connecting over HTTPS or let the admin know to set up HTTPS. For more information, see MDN.", + "js_calculating": "Calculating...", + "js_missing_feature": "Missing feature", + "js_challenge_error": "Challenge error!", + "js_challenge_error_msg": "Failed to resolve check algorithm. You may want to reload the page.", + "js_calculating_difficulty": "Calculating...
Difficulty:", + "js_speed": "Speed:", + "js_verification_longer": "Verification is taking longer than expected. Please do not refresh the page.", + "js_success": "Success!", + "js_done_took": "Done! Took", + "js_iterations": "iterations", + "js_finished_reading": "I've finished reading, continue →", + "js_calculation_error": "Calculation error!", + "js_calculation_error_msg": "Failed to calculate challenge:" +} \ No newline at end of file diff --git a/lib/localization/locales/es.json b/lib/localization/locales/es.json new file mode 100644 index 0000000..c6d3bdd --- /dev/null +++ b/lib/localization/locales/es.json @@ -0,0 +1,63 @@ +{ + "loading": "Cargando...", + "why_am_i_seeing": "¿Por qué veo esto?", + "protected_by": "Protegido por", + "made_with": "Hecho con ❤️ en 🇨🇦", + "mascot_design": "Diseño de la mascota por", + "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.", + "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.", + "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.", + "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.", + "version_info": "Este sitio web utiliza Anubis versión", + "try_again": "Intentar de nuevo", + "go_home": "Inicio", + "contact_webmaster": "o si crees que no deberías estar bloqueado, por favor contacta al webmaster en", + "connection_security": "Espere un momento mientras garantizamos la seguridad de su conexión.", + "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.", + "benchmark_requires_js": "Ejecutar la herramienta de benchmark requiere que JavaScript esté habilitado.", + "difficulty": "Dificultad:", + "algorithm": "Algoritmo:", + "compare": "Comparar:", + "time": "Tiempo", + "iters": "Iteraciones", + "time_a": "Tiempo A", + "iters_a": "Iter. A", + "time_b": "Tiempo B", + "iters_b": "Iter. B", + "static_check_endpoint": "Este es solo un endpoint de verificación para que tu proxy inverso lo use.", + "authorization_required": "Autorización requerida", + "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", + "access_denied": "Acceso denegado: código de error", + "dronebl_entry": "DroneBL reportó una entrada", + "see_dronebl_lookup": "ver", + "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", + "invalid_redirect": "Redirección inválida", + "redirect_not_parseable": "URL de redirección no analizable", + "redirect_domain_not_allowed": "Dominio de redirección no permitido", + "failed_to_sign_jwt": "falló al firmar JWT", + "invalid_invocation": "Invocación inválida de MakeChallenge", + "client_error_browser": "Error del cliente: Por favor asegúrate de que tu navegador esté actualizado e inténtalo de nuevo más tarde.", + "oh_noes": "¡Oh no!", + "benchmarking_anubis": "¡Benchmarking de Anubis!", + "you_are_not_a_bot": "¡No eres un robot!", + "making_sure_not_bot": "¡Asegurándonos de que no eres un robot!", + "celphase": "CELPHASE", + "js_web_crypto_error": "Tu navegador no tiene un elemento web.crypto funcional. ¿Estás viendo esta página en un contexto seguro?", + "js_web_workers_error": "Tu navegador no soporta web workers (Anubis los usa para evitar bloquear tu navegador). ¿Tienes un plugin como JShelter instalado?", + "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.", + "js_context_not_secure": "¡Tu contexto no es seguro!", + "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 MDN.", + "js_calculating": "Calculando...", + "js_missing_feature": "Característica faltante", + "js_challenge_error": "¡Error de desafío!", + "js_challenge_error_msg": "Falló al resolver el algoritmo de verificación. Puedes intentar recargar la página.", + "js_calculating_difficulty": "Calculando...
Dificultad:", + "js_speed": "Velocidad:", + "js_verification_longer": "La verificación está tomando más tiempo del esperado. Por favor no actualices la página.", + "js_success": "¡Éxito!", + "js_done_took": "¡Terminado! Tomó", + "js_iterations": "iteraciones", + "js_finished_reading": "He terminado de leer, continuar →", + "js_calculation_error": "¡Error de cálculo!", + "js_calculation_error_msg": "Falló al calcular el desafío:" +} \ No newline at end of file diff --git a/lib/localization/locales/fr.json b/lib/localization/locales/fr.json new file mode 100644 index 0000000..796616c --- /dev/null +++ b/lib/localization/locales/fr.json @@ -0,0 +1,63 @@ +{ + "loading": "Chargement...", + "why_am_i_seeing": "Pourquoi je vois ceci ?", + "protected_by": "Protégé par", + "made_with": "Fait avec ❤️ au 🇨🇦", + "mascot_design": "Design de la mascotte par", + "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.", + "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.", + "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.", + "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.", + "version_info": "Ce site web utilise Anubis version", + "try_again": "Réessayer", + "go_home": "Accueil", + "contact_webmaster": "ou si vous pensez que vous ne devriez pas être bloqué, veuillez contacter le webmaster à", + "connection_security": "Veuillez patienter un instant pendant que nous assurons la sécurité de votre connexion.", + "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.", + "benchmark_requires_js": "L'exécution de l'outil de benchmark nécessite l'activation de JavaScript.", + "difficulty": "Difficulté :", + "algorithm": "Algorithme :", + "compare": "Comparer :", + "time": "Temps", + "iters": "Itérations", + "time_a": "Temps A", + "iters_a": "Itér. A", + "time_b": "Temps B", + "iters_b": "Itér. B", + "static_check_endpoint": "Ceci est juste un point de terminaison de vérification pour votre proxy inverse à utiliser.", + "authorization_required": "Autorisation requise", + "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", + "access_denied": "Accès refusé : code d'erreur", + "dronebl_entry": "DroneBL a signalé une entrée", + "see_dronebl_lookup": "voir", + "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", + "invalid_redirect": "Redirection invalide", + "redirect_not_parseable": "URL de redirection non analysable", + "redirect_domain_not_allowed": "Domaine de redirection non autorisé", + "failed_to_sign_jwt": "échec de la signature JWT", + "invalid_invocation": "Invocation invalide de MakeChallenge", + "client_error_browser": "Erreur client : Veuillez vous assurer que votre navigateur est à jour et réessayez plus tard.", + "oh_noes": "Oh non !", + "benchmarking_anubis": "Test de performance d'Anubis !", + "you_are_not_a_bot": "Vous n'êtes pas un robot !", + "making_sure_not_bot": "Vérification que vous n'êtes pas un robot !", + "celphase": "PHASE de CEL", + "js_web_crypto_error": "Votre navigateur n'a pas d'élément web.crypto fonctionnel. Consultez-vous cette page dans un contexte sécurisé ?", + "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é ?", + "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.", + "js_context_not_secure": "Votre contexte n'est pas sécurisé !", + "js_context_not_secure_msg": "Essayez de vous connecter via HTTPS ou informez l'administrateur de configurer HTTPS. Pour plus d'informations, voir MDN.", + "js_calculating": "Calcul en cours...", + "js_missing_feature": "Fonctionnalité manquante", + "js_challenge_error": "Erreur de défi !", + "js_challenge_error_msg": "Échec de la résolution de l'algorithme de vérification. Vous pouvez essayer de recharger la page.", + "js_calculating_difficulty": "Calcul en cours...
Difficulté :", + "js_speed": "Vitesse :", + "js_verification_longer": "La vérification prend plus de temps que prévu. Veuillez ne pas actualiser la page.", + "js_success": "Succès !", + "js_done_took": "Terminé ! A pris", + "js_iterations": "itérations", + "js_finished_reading": "J'ai fini de lire, continuer →", + "js_calculation_error": "Erreur de calcul !", + "js_calculation_error_msg": "Échec du calcul du défi :" +} \ No newline at end of file diff --git a/lib/localization/locales/manifest.json b/lib/localization/locales/manifest.json new file mode 100644 index 0000000..e7d6e6d --- /dev/null +++ b/lib/localization/locales/manifest.json @@ -0,0 +1,3 @@ +{ + "supportedLanguages": ["en", "fr", "es"] +} diff --git a/lib/localization/localization.go b/lib/localization/localization.go new file mode 100644 index 0000000..d8af92c --- /dev/null +++ b/lib/localization/localization.go @@ -0,0 +1,100 @@ +package localization + +import ( + "embed" + "encoding/json" + "net/http" + "strings" + "sync" + + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" +) + +//go:embed locales/*.json +var localeFS embed.FS + +type LocalizationService struct { + bundle *i18n.Bundle +} + +var ( + globalService *LocalizationService + once sync.Once +) + +func NewLocalizationService() *LocalizationService { + once.Do(func() { + bundle := i18n.NewBundle(language.English) + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + + // Read all JSON files from the locales directory + entries, err := localeFS.ReadDir("locales") + if err != nil { + // Try fallback - create a minimal service with default messages + globalService = &LocalizationService{bundle: bundle} + return + } + + loadedAny := false + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") { + filePath := "locales/" + entry.Name() + _, err := bundle.LoadMessageFileFS(localeFS, filePath) + if err != nil { + // Log error but continue with other files + continue + } + loadedAny = true + } + } + + if !loadedAny { + // If no files were loaded successfully, create minimal service + globalService = &LocalizationService{bundle: bundle} + return + } + + globalService = &LocalizationService{bundle: bundle} + }) + + // Safety check - if globalService is still nil, create a minimal one + if globalService == nil { + bundle := i18n.NewBundle(language.English) + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + globalService = &LocalizationService{bundle: bundle} + } + + return globalService +} + +func (ls *LocalizationService) GetLocalizer(lang string) *i18n.Localizer { + return i18n.NewLocalizer(ls.bundle, lang) +} + +func (ls *LocalizationService) GetLocalizerFromRequest(r *http.Request) *i18n.Localizer { + if ls == nil || ls.bundle == nil { + // Fallback to a basic bundle if service is not properly initialized + bundle := i18n.NewBundle(language.English) + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + return i18n.NewLocalizer(bundle, "en") + } + acceptLanguage := r.Header.Get("Accept-Language") + return i18n.NewLocalizer(ls.bundle, acceptLanguage, "en") +} + +// SimpleLocalizer wraps i18n.Localizer with a more convenient API +type SimpleLocalizer struct { + Localizer *i18n.Localizer +} + +// T provides a concise way to localize messages +func (sl *SimpleLocalizer) T(messageID string) string { + return sl.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID}) +} + +// GetLocalizer creates a localizer based on the request's Accept-Language header +func GetLocalizer(r *http.Request) *SimpleLocalizer { + localizer := NewLocalizationService().GetLocalizerFromRequest(r) + return &SimpleLocalizer{Localizer: localizer} +} diff --git a/lib/localization/localization_test.go b/lib/localization/localization_test.go new file mode 100644 index 0000000..d416fc6 --- /dev/null +++ b/lib/localization/localization_test.go @@ -0,0 +1,116 @@ +package localization + +import ( + "encoding/json" + "sort" + "testing" + + "github.com/nicksnyder/go-i18n/v2/i18n" +) + +func TestLocalizationService(t *testing.T) { + service := NewLocalizationService() + + t.Run("English localization", func(t *testing.T) { + localizer := service.GetLocalizer("en") + result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "loading"}) + if result != "Loading..." { + t.Errorf("Expected 'Loading...', got '%s'", result) + } + }) + + t.Run("French localization", func(t *testing.T) { + localizer := service.GetLocalizer("fr") + result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "loading"}) + if result != "Chargement..." { + t.Errorf("Expected 'Chargement...', got '%s'", result) + } + }) + + t.Run("All required keys exist in English", func(t *testing.T) { + localizer := service.GetLocalizer("en") + requiredKeys := []string{ + "loading", "why_am_i_seeing", "protected_by", "made_with", + "mascot_design", "try_again", "go_home", "javascript_required", + } + + for _, key := range requiredKeys { + result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: key}) + if result == "" { + t.Errorf("Key '%s' returned empty string", key) + } + } + }) + + t.Run("All required keys exist in French", func(t *testing.T) { + localizer := service.GetLocalizer("fr") + requiredKeys := []string{ + "loading", "why_am_i_seeing", "protected_by", "made_with", + "mascot_design", "try_again", "go_home", "javascript_required", + } + + for _, key := range requiredKeys { + result := localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: key}) + if result == "" { + t.Errorf("Key '%s' returned empty string", key) + } + } + }) +} + +type manifest struct { + SupportedLanguages []string `json:"supported_languages"` +} + +func loadManifest(t *testing.T) manifest { + t.Helper() + + fin, err := localeFS.Open("locales/manifest.json") + if err != nil { + t.Fatal(err) + } + defer fin.Close() + + var result manifest + if err := json.NewDecoder(fin).Decode(&result); err != nil { + t.Fatal(err) + } + + return result +} + +func TestComprehensiveTranslations(t *testing.T) { + service := NewLocalizationService() + + var translations = map[string]any{} + fin, err := localeFS.Open("locales/en.json") + if err != nil { + t.Fatal(err) + } + defer fin.Close() + + if err := json.NewDecoder(fin).Decode(&translations); err != nil { + t.Fatal(err) + } + + var keys []string + for k := range translations { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, lang := range loadManifest(t).SupportedLanguages { + t.Run(lang, func(t *testing.T) { + loc := service.GetLocalizer(lang) + sl := SimpleLocalizer{Localizer: loc} + for _, key := range keys { + t.Run(key, func(t *testing.T) { + if result := sl.T(key); result == "" { + t.Error("key not defined") + } + }) + } + }) + } +} diff --git a/web/build.sh b/web/build.sh index 8b5f652..26583fe 100755 --- a/web/build.sh +++ b/web/build.sh @@ -32,9 +32,13 @@ THE SOFTWARE. for the JavaScript code in this page. */' +# Copy localization files to static directory +mkdir -p static/locales +cp ../lib/localization/locales/*.json static/locales/ + esbuild js/main.mjs --sourcemap --bundle --minify --outfile=static/js/main.mjs "--banner:js=${LICENSE}" gzip -f -k -n static/js/main.mjs zstd -f -k --ultra -22 static/js/main.mjs brotli -fZk static/js/main.mjs -esbuild js/bench.mjs --sourcemap --bundle --minify --outfile=static/js/bench.mjs \ No newline at end of file +esbuild js/bench.mjs --sourcemap --bundle --minify --outfile=static/js/bench.mjs diff --git a/web/index.go b/web/index.go index 0e4ade1..90bf98f 100644 --- a/web/index.go +++ b/web/index.go @@ -3,31 +3,32 @@ package web import ( "github.com/a-h/templ" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy/config" ) -func Base(title string, body templ.Component, impressum *config.Impressum) templ.Component { - return base(title, body, impressum, nil, nil) +func Base(title string, body templ.Component, impressum *config.Impressum, localizer *localization.SimpleLocalizer) templ.Component { + return base(title, body, impressum, nil, nil, localizer) } -func BaseWithChallengeAndOGTags(title string, body templ.Component, impressum *config.Impressum, challenge string, rules *config.ChallengeRules, ogTags map[string]string) (templ.Component, error) { +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) { return base(title, body, impressum, struct { Rules *config.ChallengeRules `json:"rules"` Challenge string `json:"challenge"` }{ Challenge: challenge, Rules: rules, - }, ogTags), nil + }, ogTags, localizer), nil } -func Index() templ.Component { - return index() +func Index(localizer *localization.SimpleLocalizer) templ.Component { + return index(localizer) } -func ErrorPage(msg string, mail string) templ.Component { - return errorPage(msg, mail) +func ErrorPage(msg string, mail string, localizer *localization.SimpleLocalizer) templ.Component { + return errorPage(msg, mail, localizer) } -func Bench() templ.Component { - return bench() +func Bench(localizer *localization.SimpleLocalizer) templ.Component { + return bench(localizer) } diff --git a/web/index.templ b/web/index.templ index 59e0559..d9e1910 100644 --- a/web/index.templ +++ b/web/index.templ @@ -3,11 +3,12 @@ package web import ( "fmt" "github.com/TecharoHQ/anubis" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/xess" ) -templ base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string) { +templ base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) { @@ -71,14 +72,16 @@ templ base(title string, body templ.Component, impressum *config.Impressum, chal @@ -87,79 +90,59 @@ templ base(title string, body templ.Component, impressum *config.Impressum, chal } -templ index() { +templ index(localizer *localization.SimpleLocalizer) {
Loading...

+

{ localizer.T("loading") }

- +
- +
- + diff --git a/web/index_templ.go b/web/index_templ.go index 8502c01..ab9328b 100644 --- a/web/index_templ.go +++ b/web/index_templ.go @@ -11,11 +11,12 @@ import templruntime "github.com/a-h/templ/runtime" import ( "fmt" "github.com/TecharoHQ/anubis" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/xess" ) -func base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string) templ.Component { +func base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -43,7 +44,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 14, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 15, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -56,7 +57,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + xess.URL) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 15, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 16, Col: 61} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -74,7 +75,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 19, Col: 24} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 20, Col: 24} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -87,7 +88,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(value) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 19, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 20, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -123,7 +124,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 68, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 69, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -137,12 +138,64 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

Protected by Anubis from Techaro. Made with ❤️ in 🇨🇦.

Mascot design by CELPHASE.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("protected_by")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 75, Col: 36} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " Anubis from Techaro. ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("made_with")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 77, Col: 40} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ".

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("mascot_design")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 79, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("celphase")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 79, Col: 123} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ".

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if impressum != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -150,25 +203,25 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "-- Imprint

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">Imprint

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -176,7 +229,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall }) } -func index() templ.Component { +func index(localizer *localization.SimpleLocalizer) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -192,203 +245,168 @@ func index() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var8 := templ.GetChildren(ctx) - if templ_7745c5c3_Var8 == nil { - templ_7745c5c3_Var8 = templ.NopComponent + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

Loading...

Why am I seeing this?

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.

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.

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.

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.

This website is running Anubis version ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.Version) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 131, Col: 67} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ".

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func errorPage(message string, mail string) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
\"Sad

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(message) + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 147, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 97, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ".

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("why_am_i_seeing")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 103, Col: 44} } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var18 := templ.GetChildren(ctx) - if templ_7745c5c3_Var18 == nil { - templ_7745c5c3_Var18 = templ.NopComponent + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("ai_companies_explanation")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 105, Col: 45} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + - anubis.Version) + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("anubis_compromise")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 168, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 108, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\">

This is just a check endpoint for your reverse proxy to use.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("hack_purpose")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 111, Col: 33} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("jshelter_note")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 114, Col: 34} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("version_info")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 116, Col: 35} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.Version) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 116, Col: 60} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ".

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -396,7 +414,7 @@ func StaticHappy() templ.Component { }) } -func bench() templ.Component { +func errorPage(message string, mail string, localizer *localization.SimpleLocalizer) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -412,38 +430,385 @@ func bench() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var20 := templ.GetChildren(ctx) - if templ_7745c5c3_Var20 == nil { - templ_7745c5c3_Var20 = templ.NopComponent + templ_7745c5c3_Var25 := templ.GetChildren(ctx) + if templ_7745c5c3_Var25 == nil { + templ_7745c5c3_Var25 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
TimeIters
Time AIters ATime BIters B
\"Sad

Loading...

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, ".

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if mail != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 134, Col: 40} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var30 string + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("contact_webmaster")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 134, Col: 81} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(mail) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 136, Col: 11} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("go_home")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 140, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func StaticHappy(localizer *localization.SimpleLocalizer) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var34 := templ.GetChildren(ctx) + if templ_7745c5c3_Var34 == nil { + templ_7745c5c3_Var34 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var36 string + templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("static_check_endpoint")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 153, Col: 43} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func bench(localizer *localization.SimpleLocalizer) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var37 := templ.GetChildren(ctx) + if templ_7745c5c3_Var37 == nil { + templ_7745c5c3_Var37 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var38 string + templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 164, Col: 51} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var39 string + templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 165, Col: 50} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var40 string + templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_a")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 168, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var41 string + templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_a")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 169, Col: 52} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var42 string + templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("time_b")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 170, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("iters_b")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 171, Col: 52} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 181, Col: 66} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/js/main.mjs b/web/js/main.mjs index eecc382..0b1e69b 100644 --- a/web/js/main.mjs +++ b/web/js/main.mjs @@ -18,25 +18,87 @@ const imageURL = (mood, cacheBuster, basePrefix) => cacheBuster, }); -const dependencies = [ - { - name: "WebCrypto", - msg: "Your browser doesn't have a functioning web.crypto element. Are you viewing this over a secure context?", - value: window.crypto, - }, - { - name: "Web Workers", - msg: "Your browser doesn't support web workers (Anubis uses this to avoid freezing your browser). Do you have a plugin like JShelter installed?", - value: window.Worker, - }, - { - name: "Cookies", - 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.", - value: navigator.cookieEnabled, - }, -]; +// Detect available languages by loading the manifest +const getAvailableLanguages = async () => { + const basePrefix = JSON.parse( + document.getElementById("anubis_base_prefix").textContent, + ); + + try { + const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`); + if (response.ok) { + const manifest = await response.json(); + return manifest.supportedLanguages || ['en']; + } + } catch (error) { + console.warn('Failed to load language manifest, falling back to default languages'); + } + + // Fallback to default languages if manifest loading fails + return ['en']; +}; + +// Detect browser language +const getBrowserLanguage = async () => { + const lang = navigator.language || navigator.userLanguage; + const availableLanguages = await getAvailableLanguages(); + + // Extract the language code (first 2 characters) + const langCode = lang.substring(0, 2).toLowerCase(); + + // Return the language if supported, or use English + return availableLanguages.includes(langCode) ? langCode : 'en'; +}; + +// Load translations from JSON files +const loadTranslations = async (lang) => { + const basePrefix = JSON.parse( + document.getElementById("anubis_base_prefix").textContent, + ); + try { + const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`); + return await response.json(); + } catch (error) { + console.warn(`Failed to load translations for ${lang}, falling back to English`); + if (lang !== 'en') { + return await loadTranslations('en'); + } + throw error; + } +}; + +let translations = {}; +let currentLang; + +// Initialize translations +const initTranslations = async () => { + currentLang = await getBrowserLanguage(); + translations = await loadTranslations(currentLang); +}; + +const t = (key) => translations[`js_${key}`] || translations[key] || key; (async () => { + // Initialize translations first + await initTranslations(); + + const dependencies = [ + { + name: "WebCrypto", + msg: t('web_crypto_error'), + value: window.crypto, + }, + { + name: "Web Workers", + msg: t('web_workers_error'), + value: window.Worker, + }, + { + name: "Cookies", + msg: t('cookies_error'), + value: navigator.cookieEnabled, + }, + ]; const status = document.getElementById("status"); const image = document.getElementById("image"); const title = document.getElementById("title"); @@ -67,19 +129,19 @@ const dependencies = [ if (!window.isSecureContext) { ohNoes({ - titleMsg: "Your context is not secure!", - statusMsg: `Try connecting over HTTPS or let the admin know to set up HTTPS. For more information, see MDN.`, + titleMsg: t('context_not_secure'), + statusMsg: t('context_not_secure_msg'), imageSrc: imageURL("reject", anubisVersion, basePrefix), }); return; } - status.innerHTML = "Calculating..."; + status.innerHTML = t('calculating'); for (const { value, name, msg } of dependencies) { if (!value) { ohNoes({ - titleMsg: `Missing feature ${name}`, + titleMsg: `${t('missing_feature')} ${name}`, statusMsg: msg, imageSrc: imageURL("reject", anubisVersion, basePrefix), }); @@ -94,20 +156,20 @@ const dependencies = [ const process = algorithms[rules.algorithm]; if (!process) { ohNoes({ - titleMsg: "Challenge error!", - statusMsg: `Failed to resolve check algorithm. You may want to reload the page.`, + titleMsg: t('challenge_error'), + statusMsg: t('challenge_error_msg'), imageSrc: imageURL("reject", anubisVersion, basePrefix), }); return; } - status.innerHTML = `Calculating...
Difficulty: ${rules.report_as}, `; + status.innerHTML = `${t('calculating_difficulty')} ${rules.report_as}, `; progress.style.display = "inline-block"; // the whole text, including "Speed:", as a single node, because some browsers // (Firefox mobile) present screen readers with each node as a separate piece // of text. - const rateText = document.createTextNode("Speed: 0kH/s"); + const rateText = document.createTextNode(`${t('speed')} 0kH/s`); status.appendChild(rateText); let lastSpeedUpdate = 0; @@ -125,7 +187,7 @@ const dependencies = [ // only update the speed every second so it's less visually distracting if (delta - lastSpeedUpdate > 1000) { lastSpeedUpdate = delta; - rateText.data = `Speed: ${(iters / delta).toFixed(3)}kH/s`; + rateText.data = `${t('speed')} ${(iters / delta).toFixed(3)}kH/s`; } // the probability of still being on the page is (1 - likelihood) ^ iters. // by definition, half of the time the progress bar only gets to half, so @@ -141,9 +203,7 @@ const dependencies = [ if (probability < 0.1 && !showingApology) { status.append( document.createElement("br"), - document.createTextNode( - "Verification is taking longer than expected. Please do not refresh the page.", - ), + document.createTextNode(t('verification_longer')), ); showingApology = true; } @@ -152,8 +212,8 @@ const dependencies = [ const t1 = Date.now(); console.log({ hash, nonce }); - title.innerHTML = "Success!"; - status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`; + title.innerHTML = t('success'); + status.innerHTML = `${t('done_took')} ${t1 - t0}ms, ${nonce} ${t('iterations')}`; image.src = imageURL("happy", anubisVersion, basePrefix); progress.style.display = "none"; @@ -174,7 +234,7 @@ const dependencies = [ container.style.outlineOffset = "2px"; container.style.width = "min(20rem, 90%)"; container.style.margin = "1rem auto 2rem"; - container.innerHTML = "I've finished reading, continue →"; + container.innerHTML = t('finished_reading'); function onDetailsExpand() { const redir = window.location.href; @@ -205,8 +265,8 @@ const dependencies = [ } } catch (err) { ohNoes({ - titleMsg: "Calculation error!", - statusMsg: `Failed to calculate challenge: ${err.message}`, + titleMsg: t('calculation_error'), + statusMsg: `${t('calculation_error_msg')} ${err.message}`, imageSrc: imageURL("reject", anubisVersion, basePrefix), }); }