fix(server): i18n etags missing (#3973)
This fixes missing etags from locales assets.pull/3972/head
parent
ec88b67cf1
commit
15110b732a
10
crowdin.yml
10
crowdin.yml
|
@ -6,14 +6,4 @@ files:
|
|||
- source: /internal/server/locales/en/*
|
||||
translation: /internal/server/locales/%locale%/%original_file_name%
|
||||
skip_untranslated_files: true
|
||||
languages_mapping:
|
||||
locale:
|
||||
"de-DE": de
|
||||
"en-EN": en
|
||||
"es-ES": es
|
||||
"fr-FR": fr
|
||||
"nl-NL": nl
|
||||
"pt-PT": pt
|
||||
"ru-RU": ru
|
||||
"zh-CH": zh
|
||||
...
|
||||
|
|
|
@ -9,20 +9,22 @@ import (
|
|||
|
||||
// AssetOverride allows overriding and serving of specific embedded assets from disk.
|
||||
func AssetOverride(root string, strip int, next fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
if root == "" {
|
||||
return next
|
||||
}
|
||||
|
||||
handler := fasthttp.FSHandler(root, strip)
|
||||
stripper := fasthttp.NewPathSlashesStripper(strip)
|
||||
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
asset := filepath.Join(root, string(stripper(ctx)))
|
||||
|
||||
if _, err := os.Stat(asset); err != nil {
|
||||
next(ctx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, err := os.Stat(filepath.Join(root, string(fasthttp.NewPathSlashesStripper(strip)(ctx))))
|
||||
if err != nil {
|
||||
next(ctx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fasthttp.FSHandler(root, strip)(ctx)
|
||||
handler(ctx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,19 +20,21 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
//go:embed locales
|
||||
var locales embed.FS
|
||||
|
||||
var (
|
||||
//go:embed public_html
|
||||
var assets embed.FS
|
||||
assets embed.FS
|
||||
|
||||
//go:embed locales
|
||||
locales embed.FS
|
||||
)
|
||||
|
||||
func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler {
|
||||
etags := map[string][]byte{}
|
||||
|
||||
getEmbedETags(assets, "public_html", etags)
|
||||
getEmbedETags(assets, assetsRoot, etags)
|
||||
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
p := path.Join("public_html", string(ctx.Path()))
|
||||
p := path.Join(assetsRoot, string(ctx.Path()))
|
||||
|
||||
if etag, ok := etags[p]; ok {
|
||||
ctx.Response.Header.SetBytesKV(headerETag, etag)
|
||||
|
@ -66,8 +68,10 @@ func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler {
|
|||
}
|
||||
}
|
||||
|
||||
func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
|
||||
var languages []string
|
||||
func newLocalesPathResolver() func(ctx *fasthttp.RequestCtx) (supported bool, asset string) {
|
||||
var (
|
||||
languages, dirs []string
|
||||
)
|
||||
|
||||
entries, err := locales.ReadDir("locales")
|
||||
if err == nil {
|
||||
|
@ -84,6 +88,10 @@ func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
|
|||
lng = strings.SplitN(entry.Name(), "-", 2)[0]
|
||||
}
|
||||
|
||||
if !utils.IsStringInSlice(entry.Name(), dirs) {
|
||||
dirs = append(dirs, entry.Name())
|
||||
}
|
||||
|
||||
if utils.IsStringInSlice(lng, languages) {
|
||||
continue
|
||||
}
|
||||
|
@ -93,32 +101,77 @@ func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
var (
|
||||
language, variant, locale, namespace string
|
||||
)
|
||||
aliases := map[string]string{
|
||||
"sv": "sv-SE",
|
||||
"zh": "zh-CN",
|
||||
}
|
||||
|
||||
language = ctx.UserValue("language").(string)
|
||||
namespace = ctx.UserValue("namespace").(string)
|
||||
locale = language
|
||||
return func(ctx *fasthttp.RequestCtx) (supported bool, asset string) {
|
||||
var language, namespace, variant, locale string
|
||||
|
||||
language, namespace = ctx.UserValue("language").(string), ctx.UserValue("namespace").(string)
|
||||
|
||||
if !utils.IsStringInSlice(language, languages) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if v := ctx.UserValue("variant"); v != nil {
|
||||
variant = v.(string)
|
||||
locale = fmt.Sprintf("%s-%s", language, variant)
|
||||
} else {
|
||||
locale = language
|
||||
}
|
||||
|
||||
var data []byte
|
||||
ll := language + "-" + strings.ToUpper(language)
|
||||
alias, ok := aliases[locale]
|
||||
|
||||
if data, err = locales.ReadFile(fmt.Sprintf("locales/%s/%s.json", locale, namespace)); err != nil {
|
||||
if utils.IsStringInSliceFold(language, languages) {
|
||||
data = []byte("{}")
|
||||
switch {
|
||||
case ok:
|
||||
return true, fmt.Sprintf("locales/%s/%s.json", alias, namespace)
|
||||
case utils.IsStringInSlice(locale, dirs):
|
||||
return true, fmt.Sprintf("locales/%s/%s.json", locale, namespace)
|
||||
case utils.IsStringInSlice(ll, dirs):
|
||||
return true, fmt.Sprintf("locales/%s-%s/%s.json", language, strings.ToUpper(language), namespace)
|
||||
default:
|
||||
return true, fmt.Sprintf("locales/%s/%s.json", locale, namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
hfsHandleErr(ctx, err)
|
||||
func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
|
||||
etags := map[string][]byte{}
|
||||
|
||||
getEmbedETags(locales, "locales", etags)
|
||||
|
||||
getAssetName := newLocalesPathResolver()
|
||||
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
supported, asset := getAssetName(ctx)
|
||||
|
||||
if !supported {
|
||||
handlers.SetStatusCodeResponse(ctx, fasthttp.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if etag, ok := etags[asset]; ok {
|
||||
ctx.Response.Header.SetBytesKV(headerETag, etag)
|
||||
ctx.Response.Header.SetBytesKV(headerCacheControl, headerValueCacheControlETaggedAssets)
|
||||
|
||||
if bytes.Equal(etag, ctx.Request.Header.PeekBytes(headerIfNoneMatch)) {
|
||||
ctx.SetStatusCode(fasthttp.StatusNotModified)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
data []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if data, err = locales.ReadFile(asset); err != nil {
|
||||
data = []byte("{}")
|
||||
}
|
||||
|
||||
middlewares.SetContentTypeApplicationJSON(ctx)
|
||||
|
|
|
@ -5,16 +5,17 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
embeddedAssets = "public_html/"
|
||||
swaggerAssets = embeddedAssets + "api/"
|
||||
apiFile = "openapi.yml"
|
||||
indexFile = "index.html"
|
||||
logoFile = "logo.png"
|
||||
assetsRoot = "public_html"
|
||||
assetsSwagger = assetsRoot + "/api"
|
||||
|
||||
fileOpenAPI = "openapi.yml"
|
||||
fileIndexHTML = "index.html"
|
||||
fileLogo = "logo.png"
|
||||
)
|
||||
|
||||
var (
|
||||
rootFiles = []string{"manifest.json", "robots.txt"}
|
||||
swaggerFiles = []string{
|
||||
filesRoot = []string{"manifest.json", "robots.txt"}
|
||||
filesSwagger = []string{
|
||||
"favicon-16x16.png",
|
||||
"favicon-32x32.png",
|
||||
"index.css",
|
||||
|
@ -35,7 +36,7 @@ var (
|
|||
}
|
||||
|
||||
// Directories excluded from the not found handler proceeding to the next() handler.
|
||||
httpServerDirs = []struct {
|
||||
dirsHTTPServer = []struct {
|
||||
name, prefix string
|
||||
}{
|
||||
{name: "/api", prefix: "/api/"},
|
||||
|
|
|
@ -79,8 +79,8 @@ func handleNotFound(next fasthttp.RequestHandler) fasthttp.RequestHandler {
|
|||
return func(ctx *fasthttp.RequestCtx) {
|
||||
path := strings.ToLower(string(ctx.Path()))
|
||||
|
||||
for i := 0; i < len(httpServerDirs); i++ {
|
||||
if path == httpServerDirs[i].name || strings.HasPrefix(path, httpServerDirs[i].prefix) {
|
||||
for i := 0; i < len(dirsHTTPServer); i++ {
|
||||
if path == dirsHTTPServer[i].name || strings.HasPrefix(path, dirsHTTPServer[i].prefix) {
|
||||
handlers.SetStatusCodeResponse(ctx, fasthttp.StatusNotFound)
|
||||
|
||||
return
|
||||
|
@ -104,9 +104,9 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
|
|||
|
||||
https := config.Server.TLS.Key != "" && config.Server.TLS.Certificate != ""
|
||||
|
||||
serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
|
||||
serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
|
||||
serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
|
||||
serveIndexHandler := ServeTemplatedFile(assetsRoot, fileIndexHTML, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
|
||||
serveSwaggerHandler := ServeTemplatedFile(assetsSwagger, fileIndexHTML, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
|
||||
serveSwaggerAPIHandler := ServeTemplatedFile(assetsSwagger, fileOpenAPI, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
|
||||
|
||||
handlerPublicHTML := newPublicHTMLEmbeddedHandler()
|
||||
handlerLocales := newLocalesEmbeddedHandler()
|
||||
|
@ -124,7 +124,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
|
|||
// Static Assets.
|
||||
r.GET("/", middleware(serveIndexHandler))
|
||||
|
||||
for _, f := range rootFiles {
|
||||
for _, f := range filesRoot {
|
||||
r.GET("/"+f, handlerPublicHTML)
|
||||
}
|
||||
|
||||
|
@ -139,10 +139,10 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
|
|||
// Swagger.
|
||||
r.GET("/api/", middleware(serveSwaggerHandler))
|
||||
r.OPTIONS("/api/", policyCORSPublicGET.HandleOPTIONS)
|
||||
r.GET("/api/"+apiFile, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler)))
|
||||
r.OPTIONS("/api/"+apiFile, policyCORSPublicGET.HandleOPTIONS)
|
||||
r.GET("/api/"+fileOpenAPI, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler)))
|
||||
r.OPTIONS("/api/"+fileOpenAPI, policyCORSPublicGET.HandleOPTIONS)
|
||||
|
||||
for _, file := range swaggerFiles {
|
||||
for _, file := range filesSwagger {
|
||||
r.GET("/api/"+file, handlerPublicHTML)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"Accept": "Acceptera",
|
||||
"Access your email addresses": "Hantera din e-postadress",
|
||||
"Access your group membership": "Hantera dina gruppmedlemskap",
|
||||
"Access your profile information": "Hantera din profilinformation",
|
||||
"An email has been sent to your address to complete the process": "Ett mejl har skickats till din e-postadress för att slutföra processen.",
|
||||
"Authenticated": "Autentiserad",
|
||||
"Cancel": "Avbryt",
|
||||
"Client ID": "Klient-ID: {{client_id}}",
|
||||
"Consent Request": "Begäran om medgivande",
|
||||
"Contact your administrator to register a device": "Kontakta din administratör för att registrera en enhet.",
|
||||
"Could not obtain user settings": "Misslyckades med att hämta användarinställningarna.",
|
||||
"Deny": "Avböj",
|
||||
"Done": "Klar",
|
||||
"Enter new password": "Skriv ditt nya lösenord",
|
||||
"Enter one-time password": "Skriv ditt engångslösenord",
|
||||
"Failed to register device, the provided link is expired or has already been used": "Enhetsregistreringen misslyckades, den angivna länken har utgått eller redan blivit använd.",
|
||||
"Hi": "Hej",
|
||||
"Incorrect username or password": "Fel användarnamn eller lösenord",
|
||||
"Loading": "Läser in",
|
||||
"Logout": "Logga ut",
|
||||
"Lost your device?": "Har du tappat bort din enhet?",
|
||||
"Methods": "Metoder",
|
||||
"Must be at least {{len}} characters in length": "Måste vara minst {{len}} tecken långt",
|
||||
"Must have at least one UPPERCASE letter": "Måste innehålla minst en stor bokstav",
|
||||
"Must have at least one lowercase letter": "Måste innehålla minst en liten bokstav",
|
||||
"Must have at least one number": "Måste innehålla minst ett nummer",
|
||||
"Must have at least one special character": "Måste innehålla minst ett specialtecken",
|
||||
"Must not be more than {{len}} characters in length": "Får inte vara längre än {{len}} tecken",
|
||||
"Need Google Authenticator?": "Behöver du Google Authenticator?",
|
||||
"New password": "Nytt lösenord",
|
||||
"No verification token provided": "Ingen verifieringskod tillhandahålls",
|
||||
"OTP Secret copied to clipboard": "OTP koden har kopierats till urklipp",
|
||||
"OTP URL copied to clipboard": "OTP länken har kopierats till urklipp",
|
||||
"One-Time Password": "Engångslösenord",
|
||||
"Password has been reset": "Lösenordet har blivit återställt",
|
||||
"Password": "Lösenord",
|
||||
"Passwords do not match": "Lösenorden matchar inte",
|
||||
"Push Notification": "Push-avisering",
|
||||
"Register device": "Registrera enhet",
|
||||
"Register your first device by clicking on the link below": "Registrera din första enhet genom att klicka på länken nedan",
|
||||
"Remember Consent": "Kom ihåg samtycke",
|
||||
"Remember me": "Kom ihåg mig",
|
||||
"Repeat new password": "Upprepa nya lösenordet",
|
||||
"Reset password": "Återställ lösenord",
|
||||
"Reset password?": "Återställ lösenord?",
|
||||
"Reset": "Återställ",
|
||||
"Scan QR Code": "Skanna QR koden",
|
||||
"Secret": "Kod",
|
||||
"Security Key - WebAuthN": "Säkerhetsnyckel - WebAuthN",
|
||||
"Select a Device": "Välj en enhet",
|
||||
"Sign in": "Logga in",
|
||||
"Sign out": "Logga ut",
|
||||
"The above application is requesting the following permissions": "Ovanstående program begär följande behörigheter",
|
||||
"The password does not meet the password policy": "Lösenordet uppfyller inte lösenordspolicyn",
|
||||
"The resource you're attempting to access requires two-factor authentication": "Resursen du vill komma åt kräver tvåstegsverifiering",
|
||||
"There was a problem initiating the registration process": "Ett problem uppstod när registreringsprocessen skulle starta",
|
||||
"There was an issue completing the process. The verification token might have expired": "Det uppstod ett problem med att slutföra processen. Verifieringskoden kan ha gått ut",
|
||||
"There was an issue initiating the password reset process": "Ett problem uppstod när processen för lösenordsåterställning startade",
|
||||
"There was an issue resetting the password": "Ett problem uppstod med att återställa lösenordet",
|
||||
"There was an issue signing out": "Ett problem uppstod med att logga ut",
|
||||
"This saves this consent as a pre-configured consent for future use": "Spara detta samtycke som ett förkonfigurerat samtycke för framtida användning",
|
||||
"Time-based One-Time Password": "Tidsbaserat engångslösenord",
|
||||
"Use OpenID to verify your identity": "Använd OpenID till att verifiera din identitet",
|
||||
"Username": "Användarnamn",
|
||||
"You must open the link from the same device and browser that initiated the registration process": "Du måste öppna länken från samma enhet och webbläsare som startade registreringsprocessen",
|
||||
"You're being signed out and redirected": "Du blir utloggad och omdirigerad",
|
||||
"Your supplied password does not meet the password policy requirements": "Det angivna lösenordet möter inte lösenordskraven"
|
||||
}
|
|
@ -4,10 +4,13 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
|
@ -19,7 +22,7 @@ import (
|
|||
func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, session, theme string, https bool) middlewares.RequestHandler {
|
||||
logger := logging.Logger()
|
||||
|
||||
a, err := assets.Open(publicDir + file)
|
||||
a, err := assets.Open(path.Join(publicDir, file))
|
||||
if err != nil {
|
||||
logger.Fatalf("Unable to open %s: %s", file, err)
|
||||
}
|
||||
|
@ -43,7 +46,7 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
|
|||
logoOverride := f
|
||||
|
||||
if assetPath != "" {
|
||||
if _, err := os.Stat(filepath.Join(assetPath, logoFile)); err == nil {
|
||||
if _, err := os.Stat(filepath.Join(assetPath, fileLogo)); err == nil {
|
||||
logoOverride = t
|
||||
}
|
||||
}
|
||||
|
@ -71,14 +74,14 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
|
|||
}
|
||||
|
||||
switch {
|
||||
case publicDir == swaggerAssets:
|
||||
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("base-uri 'self'; default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'", nonce, nonce))
|
||||
case publicDir == assetsSwagger:
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf("base-uri 'self'; default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'", nonce, nonce))
|
||||
case ctx.Configuration.Server.Headers.CSPTemplate != "":
|
||||
ctx.Response.Header.Add("Content-Security-Policy", strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, cspNoncePlaceholder, nonce))
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, cspNoncePlaceholder, nonce))
|
||||
case os.Getenv("ENVIRONMENT") == dev:
|
||||
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf(cspDefaultTemplate, " 'unsafe-eval'", nonce))
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(cspDefaultTemplate, " 'unsafe-eval'", nonce))
|
||||
default:
|
||||
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf(cspDefaultTemplate, "", nonce))
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(cspDefaultTemplate, "", nonce))
|
||||
}
|
||||
|
||||
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, DuoSelfEnrollment, LogoOverride, RememberMe, ResetPassword, ResetPasswordCustomURL, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, DuoSelfEnrollment: duoSelfEnrollment, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, ResetPasswordCustomURL: resetPasswordCustomURL, Session: session, Theme: theme})
|
||||
|
|
|
@ -18,9 +18,9 @@ i18n.use(Backend)
|
|||
backend: {
|
||||
loadPath: basePath + "/locales/{{lng}}/{{ns}}.json",
|
||||
},
|
||||
load: "all",
|
||||
ns: ["portal"],
|
||||
defaultNS: "portal",
|
||||
load: "all",
|
||||
fallbackLng: {
|
||||
default: ["en"],
|
||||
de: ["en"],
|
||||
|
@ -33,7 +33,7 @@ i18n.use(Backend)
|
|||
"sv-SE": ["sv", "en"],
|
||||
zh: ["en"],
|
||||
"zh-CN": ["zh", "en"],
|
||||
"zh-TW": ["zh", "en"],
|
||||
"zh-TW": ["en"],
|
||||
},
|
||||
supportedLngs: ["en", "de", "es", "fr", "nl", "pt", "ru", "sv", "sv-SE", "zh", "zh-CN", "zh-TW"],
|
||||
lowerCaseLng: false,
|
||||
|
|
Loading…
Reference in New Issue