diff --git a/core/artwork.go b/core/artwork.go index 59df626..7ac6b9d 100644 --- a/core/artwork.go +++ b/core/artwork.go @@ -29,14 +29,14 @@ const ( R18G XRestrict = 2 ) -func (x XRestrict) Localize(l i18n.Localizer) string { +func (x XRestrict) String() string { switch x { case Safe: - return l.Tr("Safe") + return i18n.Tr("Safe") case R18: - return l.Tr("R18") + return i18n.Tr("R18") case R18G: - return l.Tr("R18G") + return i18n.Tr("R18G") } log.Panicf("invalid value: %#v", int(x)) return "" @@ -56,14 +56,14 @@ const ( AI AiType = 2 ) -func (x AiType) Localize(l i18n.Localizer) string { +func (x AiType) String() string { switch x { case Unrated: - return l.Tr("Unrated") + return i18n.Tr("Unrated") case NotAI: - return l.Tr("Not AI") + return i18n.Tr("Not AI") case AI: - return l.Tr("AI") + return i18n.Tr("AI") } log.Panicf("invalid value: %#v", int(x)) return "" diff --git a/doc/dev/features/i18n.md b/doc/dev/features/i18n.md index f9850f5..b5b4ca9 100644 --- a/doc/dev/features/i18n.md +++ b/doc/dev/features/i18n.md @@ -1,10 +1,11 @@ # Localization -Notes on i18n/ +Important notice: please don't translate server logs with `i18n.*()`. It uses the request locale, not the server one. ## Todo -- [ ] Add env PIXIVFE_SERVER_LOCALE and set `i18n.GlobalLocale` to that on startup +- [x] Add cookie: Locale +- [ ] Add option to set locale in settings page - [ ] Rewrite jet template on load to use translated strings - [ ] Check if any jet template strings are ignored (false negative) - Setup Crowdin diff --git a/go.mod b/go.mod index 153cf2b..c7fa20b 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/sethvargo/go-limiter v1.0.0 github.com/soluble-ai/go-jnode v0.1.11 github.com/tidwall/gjson v1.18.0 + github.com/timandy/routine v1.1.4 github.com/yargevad/filepathx v1.0.0 github.com/zeebo/xxh3 v1.0.2 golang.org/x/net v0.30.0 diff --git a/go.sum b/go.sum index c93120f..b79b705 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-faker/faker/v4 v4.5.0 h1:ARzAY2XoOL9tOUK+KSecUQzyXQsUaZHefjyF8x6YFHc= @@ -37,18 +39,24 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= github.com/sethvargo/go-envconfig v1.1.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4= github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= github.com/soluble-ai/go-jnode v0.1.11 h1:To9h6zWI6qr5Vphz0XO4JtCRlkhfilH2EDNwtAZkZT4= github.com/soluble-ai/go-jnode v0.1.11/go.mod h1:mKFUM7xxXMBVOygZeE8od2Qls1f3E3NHY1ZhMlzwx/U= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/timandy/routine v1.1.4 h1:L9eAli/ROJcW6LhmwZcusYQcdAqxAXGOQhEXLQSNWOA= +github.com/timandy/routine v1.1.4/go.mod h1:siBcl8iIsGmhLCajRGRcy7Y7FVcicNXkr97JODdt9fc= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -97,3 +105,5 @@ golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/i18n/global.go b/i18n/global.go index d470c7c..cb73d50 100644 --- a/i18n/global.go +++ b/i18n/global.go @@ -3,26 +3,36 @@ package i18n import ( "errors" "fmt" + + "github.com/timandy/routine" ) -var GlobalLocale string = "en" +var Locale = routine.NewInheritableThreadLocal[string]() + +func getLocalizer() Localizer { + locale := Locale.Get() + if locale == "" { + locale = "en" + } + return LocalizerOf(locale) +} func Error(text string) error { - text = LocalizerOf(GlobalLocale).lookup(text) + text = getLocalizer().lookup(text) return errors.New(text) } func Errorf(format string, a ...any) error { - format = LocalizerOf(GlobalLocale).lookup(format) + format = getLocalizer().lookup(format) return fmt.Errorf(format, a...) } func Sprintf(format string, a ...any) string { - format = LocalizerOf(GlobalLocale).lookup(format) + format = getLocalizer().lookup(format) return fmt.Sprintf(format, a...) } // translate string func Tr(text string) string { - return LocalizerOf(GlobalLocale).lookup(text) + return getLocalizer().lookup(text) } diff --git a/i18n/lookup.go b/i18n/lookup.go index 5772a8a..49e3f70 100644 --- a/i18n/lookup.go +++ b/i18n/lookup.go @@ -22,7 +22,7 @@ func LocalizerOf(locale string) Localizer { return Localizer{locale: locale} } -// translate string +// lookup string in the database func (l Localizer) lookup(text string) string { translation_map, exist := locales[l.locale] if !exist { diff --git a/main.go b/main.go index 205f9b6..9c250e0 100644 --- a/main.go +++ b/main.go @@ -30,28 +30,30 @@ func main() { // Conditionally initialize and start the proxy checker if config.GlobalConfig.ProxyCheckEnabled { - var wg_firstCheck sync.WaitGroup - wg_firstCheck.Add(1) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.ProxyCheckTimeout) - proxy_checker.CheckProxies(ctx) - cancel() - wg_firstCheck.Done() - if config.GlobalConfig.ProxyCheckInterval != 0 { - for { - time.Sleep(config.GlobalConfig.ProxyCheckInterval) - ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.ProxyCheckTimeout) - proxy_checker.CheckProxies(ctx) - cancel() - } - } - }() + var wg_firstCheck sync.WaitGroup + wg_firstCheck.Add(1) - // Wait for the first proxy check to complete - log.Println("Waiting for initial proxy check to complete...") - wg_firstCheck.Wait() - log.Println("Initial proxy check completed.") + go func() { + ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.ProxyCheckTimeout) + proxy_checker.CheckProxies(ctx) + cancel() + wg_firstCheck.Done() + if config.GlobalConfig.ProxyCheckInterval != 0 { + for { + time.Sleep(config.GlobalConfig.ProxyCheckInterval) + ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.ProxyCheckTimeout) + proxy_checker.CheckProxies(ctx) + cancel() + } + } + }() + + // Wait for the first proxy check to complete + log.Println("Waiting for initial proxy check to complete...") + wg_firstCheck.Wait() + log.Println("Initial proxy check completed.") + }() } else { log.Println("Skipping proxy checker initialization.") } @@ -60,10 +62,11 @@ func main() { router := middleware.DefineRoutes() // the first middleware is the most outer / first executed one - router.Use(middleware.ProvideUserContext) // needed for everything else - router.Use(middleware.LogRequest) // all pages need this - router.Use(middleware.SetPrivacyHeaders) // all pages need this - router.Use(middleware.HandleError) // if the inner handler fails, this shows the error page instead + router.Use(middleware.ProvideUserContext) // needed for everything else + router.Use(middleware.SetLocaleFromCookie) // needed for i18n.*() + router.Use(middleware.LogRequest) // all pages need this + router.Use(middleware.SetPrivacyHeaders) // all pages need this + router.Use(middleware.HandleError) // if the inner handler fails, this shows the error page instead router.Use(middleware.InitializeRateLimiter()) // watch and compile sass when in development mode diff --git a/server/middleware/set_locale.go b/server/middleware/set_locale.go new file mode 100644 index 0000000..e85544b --- /dev/null +++ b/server/middleware/set_locale.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "net/http" + + "codeberg.org/vnpower/pixivfe/v2/i18n" + "codeberg.org/vnpower/pixivfe/v2/server/session" +) + +// ProvideUserContext is a middleware that wraps an http.Handler +// to inject a user context into each incoming request. +func SetLocaleFromCookie(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + i18n.Locale.Set(session.GetCookie(r, session.Cookie_Locale)) + h.ServeHTTP(w, r) + }) +} diff --git a/server/session/cookie.go b/server/session/cookie.go index 302e04e..e8f8643 100644 --- a/server/session/cookie.go +++ b/server/session/cookie.go @@ -21,6 +21,7 @@ const ( // the __Host thing force it to be secure and same-origin (no subdomain) Cookie_HideArtR18 CookieName = "pixivfe-HideArtR18" Cookie_HideArtR18G CookieName = "pixivfe-HideArtR18G" Cookie_HideArtAI CookieName = "pixivfe-HideArtAI" + Cookie_Locale CookieName = "pixivfe-Locale" ) // Go can't make this a const... @@ -35,6 +36,7 @@ var AllCookieNames []CookieName = []CookieName{ Cookie_HideArtR18, Cookie_HideArtR18G, Cookie_HideArtAI, + Cookie_Locale, } func GetCookie(r *http.Request, name CookieName) string {