From aac4c4772cccb232a8a854327ae0993f1fd5f330 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 4 Apr 2022 12:15:26 +1000 Subject: [PATCH] feat(web): i18n asset overrides (#3040) This allows overriding translation files in folders with lowercase RFC5646 / BCP47 Format language codes. This also fixes an issues where languages which don't expressly match the language code specified due to having a variant will also match the existing codes. Co-authored-by: Amir Zarrinkafsh --- docs/configuration/migration.md | 18 ++-- docs/configuration/server.md | 54 ++++++++++-- internal/middlewares/asset_override.go | 29 +++---- internal/middlewares/const.go | 1 - internal/server/asset.go | 87 +++++++++++++++++++ internal/server/const.go | 2 +- internal/server/locales/de/portal.json | 58 +++++++++++++ internal/server/locales/en/portal.json | 65 ++++++++++++++ internal/server/locales/es/portal.json | 65 ++++++++++++++ internal/server/server.go | 22 +++-- internal/server/template.go | 2 +- web/src/App.tsx | 53 +++++------ web/src/i18n/index.ts | 39 ++++----- web/src/i18n/locales/de.json | 60 ------------- web/src/i18n/locales/en.json | 67 -------------- web/src/i18n/locales/es.json | 67 -------------- .../RegisterOneTimePassword.tsx | 2 +- web/src/views/LoadingPage/BaseLoadingPage.tsx | 22 +++++ web/src/views/LoadingPage/LoadingPage.tsx | 16 +--- web/src/views/LoginPortal/Authenticated.tsx | 2 +- .../AuthenticatedView/AuthenticatedView.tsx | 2 +- .../LoginPortal/ConsentView/ConsentView.tsx | 2 +- .../FirstFactor/FirstFactorForm.tsx | 2 +- .../SecondFactor/MethodContainer.tsx | 4 +- .../SecondFactor/MethodSelectionDialog.tsx | 2 +- .../SecondFactor/OneTimePasswordMethod.tsx | 2 +- .../SecondFactor/SecondFactorForm.tsx | 2 +- web/src/views/LoginPortal/SignOut/SignOut.tsx | 2 +- .../ResetPassword/ResetPasswordStep1.tsx | 2 +- .../ResetPassword/ResetPasswordStep2.tsx | 2 +- 30 files changed, 442 insertions(+), 311 deletions(-) create mode 100644 internal/server/asset.go create mode 100644 internal/server/locales/de/portal.json create mode 100644 internal/server/locales/en/portal.json create mode 100644 internal/server/locales/es/portal.json delete mode 100644 web/src/i18n/locales/de.json delete mode 100644 web/src/i18n/locales/en.json delete mode 100644 web/src/i18n/locales/es.json create mode 100644 web/src/views/LoadingPage/BaseLoadingPage.tsx diff --git a/docs/configuration/migration.md b/docs/configuration/migration.md index ee726e89d..f1022320b 100644 --- a/docs/configuration/migration.md +++ b/docs/configuration/migration.md @@ -34,15 +34,15 @@ logged for users. ### 4.30.0 The following changes occurred in 4.30.0: -|Previous Key |New Key | -|:-----------:|:--------------------:| -|host |server.host | -|port |server.port | -|tls_key |server.tls.key | -|tls_cert |server.tls.certificate| -|log_level |log.level | -|log_file_path|log.file_path | -|log_format |log.format | +| Previous Key | New Key | +|:-------------:|:----------------------:| +| host | server.host | +| port | server.port | +| tls_key | server.tls.key | +| tls_cert | server.tls.certificate | +| log_level | log.level | +| log_file_path | log.file_path | +| log_format | log.format | _**Please Note:** you can no longer define secrets for providers that you are not using. For example if you're using the [filesystem notifier](./notifier/filesystem.md) you must ensure that the `AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE` diff --git a/docs/configuration/server.md b/docs/configuration/server.md index a724cc384..5f3d28e27 100644 --- a/docs/configuration/server.md +++ b/docs/configuration/server.md @@ -108,13 +108,15 @@ Example: ```console /config/assets/ ├── favicon.ico -└── logo.png +├── logo.png +└── locales/[-[variant]]/.json ``` -|Asset |File name| -|:-----:|:---------------:| -|Favicon|favicon.ico | -|Logo |logo.png | +| Asset | File name | +|:-------:|:-------------:| +| Favicon | favicon.ico | +| Logo | logo.png | +| locales | see [locales] | ### read_buffer_size
@@ -241,4 +243,44 @@ you're able to tune these individually depending on your needs. ### Asset Overrides If replacing the Logo for your Authelia portal it is recommended to upload a transparent PNG of your desired logo. -Authelia will automatically resize the logo to an appropriate size to present in the frontend. \ No newline at end of file +Authelia will automatically resize the logo to an appropriate size to present in the frontend. + +#### locales + +The locales folder holds folders of internationalization locales. This folder can be utilized to override these locales. +They are the names of locales that are returned by the `navigator.langauge` ECMAScript command. These are generally +those in the [RFC5646 / BCP47 Format](https://datatracker.ietf.org/doc/html/rfc5646) except the name normalized to +lowercase for consistency ease; for example the `en-US` locale should be in the directory `en-us`. + +Each directory has json files which you can explore the format of in the +[internal/server/locales](https://github.com/authelia/authelia/tree/master/internal/server/locales) directory on +GitHub. The important part is the key names you wish to override. Each file represents a translation namespace. The list +of current namespaces are below: + +| Namespace | Purpose | +|:---------:|:-------------------:| +| portal | Portal translations | + +A full example for the `en-US` locale for the portal namespace is `locales/en-us/portal.json`. + +Languages in browsers are supported in two forms. In their language only form such as `en` for English, and in their +variant form such as `en-AU` for English (Australian). If a user has the browser language `en-AU` we automatically load +the `en` and `en-AU` languages, where any keys in the `en-AU` language take precedence over the `en` language, and the +translations for the `en` language only applying when a translation from `en-AU` is not available. + +List of supported languages and variants: + +| Description | Language | Additional Variants | Location | +|:-----------:|:--------:|:-------------------:|:----------------------:| +| English | en | N/A | locales/en/portal.json | +| Spanish | es | N/A | locales/es/portal.json | +| German | de | N/A | locales/de/portal.json | + +_**Important Note** Currently users can only override languages that already exist in this list either by overriding +the language itself, or adding a variant form of that language. If you'd like support for another language feel free +to make a PR. We also encourage people to make PR's for variants where the difference in the variants is important._ + +_**Important Note** Overriding these files will not guarantee any form of stability. Users who planning to utilize these +overrides should either check for changes to the files in the +[en](https://github.com/authelia/authelia/tree/master/internal/server/locales/en) translation prior to upgrading or PR +their translation to ensure it is maintained._ \ No newline at end of file diff --git a/internal/middlewares/asset_override.go b/internal/middlewares/asset_override.go index 649ed965b..887d60117 100644 --- a/internal/middlewares/asset_override.go +++ b/internal/middlewares/asset_override.go @@ -2,28 +2,27 @@ package middlewares import ( "os" - "strings" + "path/filepath" "github.com/valyala/fasthttp" - - "github.com/authelia/authelia/v4/internal/utils" ) // AssetOverrideMiddleware allows overriding and serving of specific embedded assets from disk. -func AssetOverrideMiddleware(assetPath string, next fasthttp.RequestHandler) fasthttp.RequestHandler { +func AssetOverrideMiddleware(root string, strip int, next fasthttp.RequestHandler) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { - uri := string(ctx.RequestURI()) - file := uri[strings.LastIndex(uri, "/")+1:] - - if assetPath != "" && utils.IsStringInSlice(file, validOverrideAssets) { - _, err := os.Stat(assetPath + file) - if err != nil { - next(ctx) - } else { - fasthttp.FSHandler(assetPath, strings.Count(uri, "/")-1)(ctx) - } - } else { + if root == "" { 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) } } diff --git a/internal/middlewares/const.go b/internal/middlewares/const.go index 8e0d4aca1..e83a25a36 100644 --- a/internal/middlewares/const.go +++ b/internal/middlewares/const.go @@ -57,6 +57,5 @@ const ( ) var protoHostSeparator = []byte("://") -var validOverrideAssets = []string{"favicon.ico", "logo.png"} var errPasswordPolicyNoMet = errors.New("the supplied password does not met the security policy") diff --git a/internal/server/asset.go b/internal/server/asset.go new file mode 100644 index 000000000..9fc277c33 --- /dev/null +++ b/internal/server/asset.go @@ -0,0 +1,87 @@ +package server + +import ( + "embed" + "errors" + "fmt" + "io/fs" + "net/http" + + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpadaptor" + + "github.com/authelia/authelia/v4/internal/utils" +) + +//go:embed locales +var locales embed.FS + +//go:embed public_html +var assets embed.FS + +func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler { + embeddedPath, _ := fs.Sub(assets, "public_html") + + return fasthttpadaptor.NewFastHTTPHandler(http.FileServer(http.FS(embeddedPath))) +} + +func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) { + var languages []string + + entries, err := locales.ReadDir("locales") + if err == nil { + for _, entry := range entries { + if entry.IsDir() && len(entry.Name()) == 2 { + languages = append(languages, entry.Name()) + } + } + } + + return func(ctx *fasthttp.RequestCtx) { + var ( + language, variant, locale, namespace string + ) + + language = ctx.UserValue("language").(string) + namespace = ctx.UserValue("namespace").(string) + locale = language + + if v := ctx.UserValue("variant"); v != nil { + variant = v.(string) + locale = fmt.Sprintf("%s-%s", language, locale) + } + + var data []byte + + if data, err = locales.ReadFile(fmt.Sprintf("locales/%s/%s.json", locale, namespace)); err != nil { + if variant != "" && utils.IsStringInSliceFold(language, languages) { + data = []byte("{}") + } + + if len(data) == 0 { + hfsHandleErr(ctx, err) + + return + } + } + + ctx.SetContentType("application/json") + ctx.SetBody(data) + } +} + +func hfsHandleErr(ctx *fasthttp.RequestCtx, err error) { + switch { + case errors.Is(err, fs.ErrNotExist): + writeStatus(ctx, fasthttp.StatusNotFound) + case errors.Is(err, fs.ErrPermission): + writeStatus(ctx, fasthttp.StatusForbidden) + default: + writeStatus(ctx, fasthttp.StatusInternalServerError) + } +} + +func writeStatus(ctx *fasthttp.RequestCtx, status int) { + ctx.SetStatusCode(status) + ctx.SetBodyString(fmt.Sprintf("%d %s", status, fasthttp.StatusMessage(status))) +} diff --git a/internal/server/const.go b/internal/server/const.go index a5b79668e..8fb605e23 100644 --- a/internal/server/const.go +++ b/internal/server/const.go @@ -9,7 +9,7 @@ const ( ) var ( - rootFiles = []string{"favicon.ico", "manifest.json", "robots.txt"} + rootFiles = []string{"manifest.json", "robots.txt"} swaggerFiles = []string{ "favicon-16x16.png", "favicon-32x32.png", diff --git a/internal/server/locales/de/portal.json b/internal/server/locales/de/portal.json new file mode 100644 index 000000000..84a8d387b --- /dev/null +++ b/internal/server/locales/de/portal.json @@ -0,0 +1,58 @@ +{ + "An email has been sent to your address to complete the process": "Es wurde eine E-Mail an Ihre Adresse geschickt, um den Vorgang abzuschließen.", + "Authenticated": "Authentifiziert", + "Cancel": "Abbrechen", + "Contact your administrator to register a device": "Wenden Sie sich an Ihren Administrator, um ein Gerät zu registrieren.", + "Could not obtain user settings": "Benutzereinstellungen konnten nicht abgerufen werden", + "Done": "Erledigt", + "Enter new password": "Neues Passwort eingeben", + "Enter one-time password": "Einmal-Passwort eingeben", + "Failed to register device, the provided link is expired or has already been used": "Gerät konnte nicht registriert werden, der angegebene Link ist abgelaufen oder wurde bereits verwendet", + "Hi": "Hallo", + "Incorrect username or password": "Falscher Benutzername oder falsches Passwort.", + "Loading": "Laden", + "Logout": "Abmelden", + "Lost your device?": "Haben Sie Ihr Gerät verloren?", + "Methods": "Verfahren", + "Need Google Authenticator?": "Benötigen Sie Google Authenticator?", + "New password": "Neues Passwort", + "No verification token provided": "Kein Verifizierungs-Token vorhanden", + "OTP Secret copied to clipboard": "OTP Secret in die Zwischenablage kopiert.", + "OTP URL copied to clipboard": "OTP-URL in die Zwischenablage kopiert.", + "One-Time Password": "One-Time-Passwort", + "Password has been reset": "Das Passwort wurde zurückgesetzt.", + "Password": "Passwort", + "Passwords do not match": "Die Passwörter stimmen nicht überein.", + "Push Notification": "Push-Benachrichtigung", + "Register device": "Gerät registrieren", + "Register your first device by clicking on the link below": "Registrieren Sie Ihr erstes Gerät, indem Sie auf den unten stehenden Link klicken.", + "Remember me": "Angemeldet bleiben", + "Repeat new password": "Neues Passwort wiederholen", + "Reset password": "Passwort zurücksetzen", + "Reset password?": "Passwort zurücksetzen?", + "Reset": "Zurücksetzen", + "Scan QR Code": "QR-Code scannen", + "Secret": "Geheimnis", + "Security Key - WebAuthN": "Sicherheitsschlüssel - WebAuthN", + "Select a Device": "Gerät auswählen", + "Sign in": "Anmelden", + "Sign out": "Abmelden", + "The resource you're attempting to access requires two-factor authentication": "Die Ressource, auf die Sie zuzugreifen versuchen, erfordert eine Zwei-Faktor-Authentifizierung.", + "There was a problem initiating the registration process": "Es gab ein Problem beim Starten des Registrierungsprozesses.", + "There was an issue completing the process. The verification token might have expired": "Es gab ein Problem beim Abschluss des Prozesses. Das Verifizierungs-Token könnte abgelaufen sein.", + "There was an issue initiating the password reset process": "Es gab ein Problem beim beim Starten des Passwortrücksetzprozesses.", + "There was an issue resetting the password": "Es gab ein Problem beim Zurücksetzen des Passworts", + "There was an issue signing out": "Es gab ein Problem bei der Abmeldung", + "Time-based One-Time Password": "Zeitbasiertes One-Time-Passwort", + "Username": "Benutzername", + "You must open the link from the same device and browser that initiated the registration process": "Sie müssen den Link mit demselben Gerät und demselben Browser öffnen, mit dem Sie den Registrierungsprozess gestartet haben.", + "You're being signed out and redirected": "Sie werden abgemeldet und umgeleitet", + "Your supplied password does not meet the password policy requirements": "Ihr angegebenes Passwort entspricht nicht den Anforderungen der Passwortrichtlinie.", + "Use OpenID to verify your identity": "Verwenden Sie OpenID, um Ihre Identität zu überprüfen", + "Access your display name": "Zugriff auf Ihren Anzeigenamen", + "Access your group membership": "Zugriff auf Ihre Gruppenmitgliedschaft", + "Access your email addresses": "Zugriff auf Ihre E-Mail-Adressen", + "Accept": "Annehmen", + "Deny": "Ablehnen", + "The above application is requesting the following permissions": "Die oben genannte Anwendung bittet um die folgenden Berechtigungen" +} diff --git a/internal/server/locales/en/portal.json b/internal/server/locales/en/portal.json new file mode 100644 index 000000000..978765818 --- /dev/null +++ b/internal/server/locales/en/portal.json @@ -0,0 +1,65 @@ +{ + "An email has been sent to your address to complete the process": "An email has been sent to your address to complete the process.", + "Authenticated": "Authenticated", + "Cancel": "Cancel", + "Contact your administrator to register a device": "Contact your administrator to register a device.", + "Could not obtain user settings": "Could not obtain user settings", + "Done": "Done", + "Enter new password": "Enter new password", + "Enter one-time password": "Enter one-time password", + "Failed to register device, the provided link is expired or has already been used": "Failed to register device, the provided link is expired or has already been used", + "Hi": "Hi", + "Incorrect username or password": "Incorrect username or password.", + "Loading": "Loading", + "Logout": "Logout", + "Lost your device?": "Lost your device?", + "Methods": "Methods", + "Need Google Authenticator?": "Need Google Authenticator?", + "New password": "New password", + "No verification token provided": "No verification token provided", + "OTP Secret copied to clipboard": "OTP Secret copied to clipboard.", + "OTP URL copied to clipboard": "OTP URL copied to clipboard.", + "One-Time Password": "One-Time Password", + "Password has been reset": "Password has been reset.", + "Password": "Password", + "Passwords do not match": "Passwords do not match.", + "Push Notification": "Push Notification", + "Register device": "Register device", + "Register your first device by clicking on the link below": "Register your first device by clicking on the link below.", + "Remember me": "Remember me", + "Repeat new password": "Repeat new password", + "Reset password": "Reset password", + "Reset password?": "Reset password?", + "Reset": "Reset", + "Scan QR Code": "Scan QR Code", + "Secret": "Secret", + "Security Key - WebAuthN": "Security Key - WebAuthN", + "Select a Device": "Select a Device", + "Sign in": "Sign in", + "Sign out": "Sign out", + "The resource you're attempting to access requires two-factor authentication": "The resource you're attempting to access requires two-factor authentication.", + "There was a problem initiating the registration process": "There was a problem initiating the registration process", + "There was an issue completing the process. The verification token might have expired": "There was an issue completing the process. The verification token might have expired.", + "There was an issue initiating the password reset process": "There was an issue initiating the password reset process.", + "There was an issue resetting the password": "There was an issue resetting the password", + "There was an issue signing out": "There was an issue signing out", + "Time-based One-Time Password": "Time-based One-Time Password", + "Username": "Username", + "You must open the link from the same device and browser that initiated the registration process": "You must open the link from the same device and browser that initiated the registration process", + "You're being signed out and redirected": "You're being signed out and redirected", + "Your supplied password does not meet the password policy requirements": "Your supplied password does not meet the password policy requirements.", + "Use OpenID to verify your identity": "Use OpenID to verify your identity", + "Access your display name": "Access your display name", + "Access your group membership": "Access your group membership", + "Access your email addresses": "Access your email addresses", + "Accept": "Accept", + "Deny": "Deny", + "The above application is requesting the following permissions": "The above application is requesting the following permissions", + "The password does not meet the password policy": "The password does not meet the password policy", + "Must have at least one lowercase letter": "Must have at least one lowercase letter", + "Must have at least one UPPERCASE letter": "Must have at least one UPPERCASE letter", + "Must have at least one number": "Must have at least one number", + "Must have at least one special character": "Must have at least one special character", + "Must be at least {{len}} characters in length": "Must be at least {{len}} characters in length", + "Must not be more than {{len}} characters in length": "Must not be more than {{len}} characters in length" +} \ No newline at end of file diff --git a/internal/server/locales/es/portal.json b/internal/server/locales/es/portal.json new file mode 100644 index 000000000..a47aacf5a --- /dev/null +++ b/internal/server/locales/es/portal.json @@ -0,0 +1,65 @@ +{ + "An email has been sent to your address to complete the process": "Un correo ha sido enviado a su cuenta para completar el proceso", + "Authenticated": "Autenticado", + "Cancel": "Cancelar", + "Contact your administrator to register a device": "Contacte a su administrador para registrar un dispositivo.", + "Could not obtain user settings": "Error al obtener configuración de usuario", + "Done": "Hecho", + "Enter new password": "Ingrese una nueva contraseña", + "Enter one-time password": "Ingrese contraseña de un solo uso (OTP)", + "Failed to register device, the provided link is expired or has already been used": "Error al registrar dispositivo, el link expiró o ya ha sido utilizado", + "Hi": "Hola", + "Incorrect username or password": "Usuario y/o contraseña incorrectos", + "Loading": "Cargando", + "Logout": "Cerrar Sesión", + "Lost your device?": "Perdió su dispositivo?", + "Methods": "Métodos", + "Need Google Authenticator?": "Necesita Google Authenticator?", + "New password": "Nueva contraseña", + "No verification token provided": "No se ha recibido el token de verificación", + "OTP Secret copied to clipboard": "La clave OTP ha sido copiada al portapapeles", + "OTP URL copied to clipboard": "la URL OTP ha sido copiada al portapapeles.", + "One-Time Password": "Contraseña de un solo uso (OTP)", + "Password has been reset": "La contraseña ha sido restablecida.", + "Password": "Contraseña", + "Passwords do not match": "Las contraseñas no coinciden.", + "Push Notification": "Notificaciones Push", + "Register device": "Registrar Dispositivo", + "Register your first device by clicking on the link below": "Registre su primer dispositivo, haciendo click en el siguiente link.", + "Remember me": "Recordarme", + "Repeat new password": "Repetir la contraseña", + "Reset password": "Restablecer Contraseña", + "Reset password?": "Olvidé mi contraseña", + "Reset": "Restablecer", + "Scan QR Code": "Escanear Código QR", + "Secret": "Secreto", + "Security Key - WebAuthN": "Llave de Seguridad - WebAuthN", + "Select a Device": "Seleccionar Dispositivo", + "Sign in": "Iniciar Sesión", + "Sign out": "Cerrar Sesión", + "The resource you're attempting to access requires two-factor authentication": "El recurso que intenta alcanzar requiere un segundo factor de autenticación (2FA).", + "There was a problem initiating the registration process": "Ocurrió un problema al iniciar el proceso de registración", + "There was an issue completing the process. The verification token might have expired": "Ocurrió un problema mientras se completaba el proceso. El token de verificación pudo haber expirado.", + "There was an issue initiating the password reset process": "Ha ocurrido un error al iniciar el proceso de proceso de restauración de contraseña.", + "There was an issue resetting the password": "Ocurrió un error al intentar restablecer la contraseña", + "There was an issue signing out": "Ocurrió un error al intentar cerrar sesión", + "Time-based One-Time Password": "Contraseña de uso único - OTP", + "Username": "Usuario", + "You must open the link from the same device and browser that initiated the registration process": "Debe abrir el link desde el mismo dispositivo y navegador desde el que inició el proceso de registración", + "You're being signed out and redirected": "Cerrando Sesión y redirigiendo", + "Your supplied password does not meet the password policy requirements": "La contraseña suministrada no cumple con los requerimientos de la política de contraseñas", + "Use OpenID to verify your identity": "Utilizar OpenID para verificar su identidad", + "Access your display name": "Acceso a su nombre", + "Access your group membership": "Acceso a su(s) grupo(s)", + "Access your email addresses": "Acceso a su dirección de correo", + "Accept": "Aceptar", + "Deny": "Denegar", + "The above application is requesting the following permissions": "La aplicación solicita los siguientes permisos", + "The password does not meet the password policy": "La contraseña no cumple con la política de contraseñas", + "Must have at least one lowercase letter": "Debe contener al menos una letra minúscula", + "Must have at least one UPPERCASE letter": "Debe contener al menos una letra MAYUSCULA", + "Must have at least one number": "Debe contener al menos un número", + "Must have at least one special character": "Debe contener al menos un caracter especial", + "Must be at least {{len}} characters in length": "La longitud mínima es de {{len}} caracteres", + "Must not be more than {{len}} characters in length": "La longitud máxima es de {{len}} caracteres" +} \ No newline at end of file diff --git a/internal/server/server.go b/internal/server/server.go index 660eb5cba..6c5b19ce1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,10 +1,7 @@ package server import ( - "embed" - "io/fs" "net" - "net/http" "os" "strconv" "time" @@ -13,7 +10,6 @@ import ( "github.com/fasthttp/router" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/expvarhandler" - "github.com/valyala/fasthttp/fasthttpadaptor" "github.com/valyala/fasthttp/pprofhandler" "github.com/authelia/authelia/v4/internal/configuration/schema" @@ -23,9 +19,6 @@ import ( "github.com/authelia/authelia/v4/internal/middlewares" ) -//go:embed public_html -var assets embed.FS - func registerRoutes(configuration schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler { autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers) rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != schema.RememberMeDisabled) @@ -36,8 +29,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr duoSelfEnrollment = strconv.FormatBool(configuration.DuoAPI.EnableSelfEnrollment) } - embeddedPath, _ := fs.Sub(assets, "public_html") - embeddedFS := fasthttpadaptor.NewFastHTTPHandler(http.FileServer(http.FS(embeddedPath))) + handlerPublicHTML := newPublicHTMLEmbeddedHandler() + handlerLocales := newLocalesEmbeddedHandler() https := configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate != "" @@ -50,17 +43,22 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr r.OPTIONS("/", autheliaMiddleware(handleOPTIONS)) for _, f := range rootFiles { - r.GET("/"+f, middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS)) + r.GET("/"+f, handlerPublicHTML) } r.GET("/api/", autheliaMiddleware(serveSwaggerHandler)) r.GET("/api/"+apiFile, autheliaMiddleware(serveSwaggerAPIHandler)) for _, file := range swaggerFiles { - r.GET("/api/"+file, embeddedFS) + r.GET("/api/"+file, handlerPublicHTML) } - r.GET("/static/{filepath:*}", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS)) + r.GET("/favicon.ico", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, 0, handlerPublicHTML)) + r.GET("/static/media/logo.png", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, 2, handlerPublicHTML)) + r.GET("/static/{filepath:*}", handlerPublicHTML) + + r.GET("/locales/{language:[a-z]{1,3}}-{variant:[a-z0-9-]+}/{namespace:[a-z]+}.json", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, 0, handlerLocales)) + r.GET("/locales/{language:[a-z]{1,3}}/{namespace:[a-z]+}.json", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, 0, handlerLocales)) r.GET("/api/health", autheliaMiddleware(handlers.HealthGet)) r.GET("/api/state", autheliaMiddleware(handlers.StateGet)) diff --git a/internal/server/template.go b/internal/server/template.go index 2c9690a89..9c8d4a5f3 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -43,7 +43,7 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM logoOverride := f if assetPath != "" { - if _, err := os.Stat(assetPath + logoFile); err == nil { + if _, err := os.Stat(filepath.Join(assetPath, logoFile)); err == nil { logoOverride = t } } diff --git a/web/src/App.tsx b/web/src/App.tsx index fdde7ea60..635669bef 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, Suspense } from "react"; import { config as faConfig } from "@fortawesome/fontawesome-svg-core"; import { CssBaseline, ThemeProvider } from "@material-ui/core"; @@ -21,6 +21,7 @@ import { getBasePath } from "@utils/BasePath"; import { getDuoSelfEnrollment, getRememberMe, getResetPassword, getTheme } from "@utils/Configuration"; import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword"; import RegisterWebauthn from "@views/DeviceRegistration/RegisterWebauthn"; +import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage"; import ConsentView from "@views/LoginPortal/ConsentView/ConsentView"; import LoginPortal from "@views/LoginPortal/LoginPortal"; import SignOut from "@views/LoginPortal/SignOut/SignOut"; @@ -60,30 +61,32 @@ const App: React.FC = () => { }, []); return ( - - - - setNotification(null)} /> - - } /> - } /> - } /> - } /> - } /> - } /> - - } - /> - - - + }> + + + + setNotification(null)} /> + + } /> + } /> + } /> + } /> + } /> + } /> + + } + /> + + + + ); }; diff --git a/web/src/i18n/index.ts b/web/src/i18n/index.ts index 8fe2a1222..035f30559 100644 --- a/web/src/i18n/index.ts +++ b/web/src/i18n/index.ts @@ -1,33 +1,28 @@ import i18n from "i18next"; import LanguageDetector from "i18next-browser-languagedetector"; -import XHR from "i18next-http-backend"; +import Backend from "i18next-http-backend"; import { initReactI18next } from "react-i18next"; -import langDe from "@i18n/locales/de.json"; -import langEn from "@i18n/locales/en.json"; -import langEs from "@i18n/locales/es.json"; - -const resources = { - en: langEn, - es: langEs, - de: langDe, -}; - -const options = { - order: ["querystring", "navigator"], - lookupQuerystring: "lng", -}; - -i18n.use(XHR) +i18n.use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ - detection: options, - resources, - ns: [""], - defaultNS: "", - fallbackLng: "en", + detection: { + order: ["querystring", "navigator"], + lookupQuerystring: "lng", + }, + backend: { + loadPath: "/locales/{{lng}}/{{ns}}.json", + }, + ns: ["portal"], + defaultNS: "portal", + fallbackLng: { + default: ["en"], + }, + load: "all", supportedLngs: ["en", "es", "de"], + lowerCaseLng: true, + nonExplicitSupportedLngs: true, interpolation: { escapeValue: false, }, diff --git a/web/src/i18n/locales/de.json b/web/src/i18n/locales/de.json deleted file mode 100644 index 66d6fbb2f..000000000 --- a/web/src/i18n/locales/de.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "Portal": { - "An email has been sent to your address to complete the process": "Es wurde eine E-Mail an Ihre Adresse geschickt, um den Vorgang abzuschließen.", - "Authenticated": "Authentifiziert", - "Cancel": "Abbrechen", - "Contact your administrator to register a device": "Wenden Sie sich an Ihren Administrator, um ein Gerät zu registrieren.", - "Could not obtain user settings": "Benutzereinstellungen konnten nicht abgerufen werden", - "Done": "Erledigt", - "Enter new password": "Neues Passwort eingeben", - "Enter one-time password": "Einmal-Passwort eingeben", - "Failed to register device, the provided link is expired or has already been used": "Gerät konnte nicht registriert werden, der angegebene Link ist abgelaufen oder wurde bereits verwendet", - "Hi": "Hallo", - "Incorrect username or password": "Falscher Benutzername oder falsches Passwort.", - "Loading": "Laden", - "Logout": "Abmelden", - "Lost your device?": "Haben Sie Ihr Gerät verloren?", - "Methods": "Verfahren", - "Need Google Authenticator?": "Benötigen Sie Google Authenticator?", - "New password": "Neues Passwort", - "No verification token provided": "Kein Verifizierungs-Token vorhanden", - "OTP Secret copied to clipboard": "OTP Secret in die Zwischenablage kopiert.", - "OTP URL copied to clipboard": "OTP-URL in die Zwischenablage kopiert.", - "One-Time Password": "One-Time-Passwort", - "Password has been reset": "Das Passwort wurde zurückgesetzt.", - "Password": "Passwort", - "Passwords do not match": "Die Passwörter stimmen nicht überein.", - "Push Notification": "Push-Benachrichtigung", - "Register device": "Gerät registrieren", - "Register your first device by clicking on the link below": "Registrieren Sie Ihr erstes Gerät, indem Sie auf den unten stehenden Link klicken.", - "Remember me": "Angemeldet bleiben", - "Repeat new password": "Neues Passwort wiederholen", - "Reset password": "Passwort zurücksetzen", - "Reset password?": "Passwort zurücksetzen?", - "Reset": "Zurücksetzen", - "Scan QR Code": "QR-Code scannen", - "Secret": "Geheimnis", - "Security Key - WebAuthN": "Sicherheitsschlüssel - WebAuthN", - "Select a Device": "Gerät auswählen", - "Sign in": "Anmelden", - "Sign out": "Abmelden", - "The resource you're attempting to access requires two-factor authentication": "Die Ressource, auf die Sie zuzugreifen versuchen, erfordert eine Zwei-Faktor-Authentifizierung.", - "There was a problem initiating the registration process": "Es gab ein Problem beim Starten des Registrierungsprozesses.", - "There was an issue completing the process. The verification token might have expired": "Es gab ein Problem beim Abschluss des Prozesses. Das Verifizierungs-Token könnte abgelaufen sein.", - "There was an issue initiating the password reset process": "Es gab ein Problem beim beim Starten des Passwortrücksetzprozesses.", - "There was an issue resetting the password": "Es gab ein Problem beim Zurücksetzen des Passworts", - "There was an issue signing out": "Es gab ein Problem bei der Abmeldung", - "Time-based One-Time Password": "Zeitbasiertes One-Time-Passwort", - "Username": "Benutzername", - "You must open the link from the same device and browser that initiated the registration process": "Sie müssen den Link mit demselben Gerät und demselben Browser öffnen, mit dem Sie den Registrierungsprozess gestartet haben.", - "You're being signed out and redirected": "Sie werden abgemeldet und umgeleitet", - "Your supplied password does not meet the password policy requirements": "Ihr angegebenes Passwort entspricht nicht den Anforderungen der Passwortrichtlinie.", - "Use OpenID to verify your identity": "Verwenden Sie OpenID, um Ihre Identität zu überprüfen", - "Access your display name": "Zugriff auf Ihren Anzeigenamen", - "Access your group membership": "Zugriff auf Ihre Gruppenmitgliedschaft", - "Access your email addresses": "Zugriff auf Ihre E-Mail-Adressen", - "Accept": "Annehmen", - "Deny": "Ablehnen", - "The above application is requesting the following permissions": "Die oben genannte Anwendung bittet um die folgenden Berechtigungen" - } -} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json deleted file mode 100644 index b2378a408..000000000 --- a/web/src/i18n/locales/en.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "Portal": { - "An email has been sent to your address to complete the process": "An email has been sent to your address to complete the process.", - "Authenticated": "Authenticated", - "Cancel": "Cancel", - "Contact your administrator to register a device": "Contact your administrator to register a device.", - "Could not obtain user settings": "Could not obtain user settings", - "Done": "Done", - "Enter new password": "Enter new password", - "Enter one-time password": "Enter one-time password", - "Failed to register device, the provided link is expired or has already been used": "Failed to register device, the provided link is expired or has already been used", - "Hi": "Hi", - "Incorrect username or password": "Incorrect username or password.", - "Loading": "Loading", - "Logout": "Logout", - "Lost your device?": "Lost your device?", - "Methods": "Methods", - "Need Google Authenticator?": "Need Google Authenticator?", - "New password": "New password", - "No verification token provided": "No verification token provided", - "OTP Secret copied to clipboard": "OTP Secret copied to clipboard.", - "OTP URL copied to clipboard": "OTP URL copied to clipboard.", - "One-Time Password": "One-Time Password", - "Password has been reset": "Password has been reset.", - "Password": "Password", - "Passwords do not match": "Passwords do not match.", - "Push Notification": "Push Notification", - "Register device": "Register device", - "Register your first device by clicking on the link below": "Register your first device by clicking on the link below.", - "Remember me": "Remember me", - "Repeat new password": "Repeat new password", - "Reset password": "Reset password", - "Reset password?": "Reset password?", - "Reset": "Reset", - "Scan QR Code": "Scan QR Code", - "Secret": "Secret", - "Security Key - WebAuthN": "Security Key - WebAuthN", - "Select a Device": "Select a Device", - "Sign in": "Sign in", - "Sign out": "Sign out", - "The resource you're attempting to access requires two-factor authentication": "The resource you're attempting to access requires two-factor authentication.", - "There was a problem initiating the registration process": "There was a problem initiating the registration process", - "There was an issue completing the process. The verification token might have expired": "There was an issue completing the process. The verification token might have expired.", - "There was an issue initiating the password reset process": "There was an issue initiating the password reset process.", - "There was an issue resetting the password": "There was an issue resetting the password", - "There was an issue signing out": "There was an issue signing out", - "Time-based One-Time Password": "Time-based One-Time Password", - "Username": "Username", - "You must open the link from the same device and browser that initiated the registration process": "You must open the link from the same device and browser that initiated the registration process", - "You're being signed out and redirected": "You're being signed out and redirected", - "Your supplied password does not meet the password policy requirements": "Your supplied password does not meet the password policy requirements.", - "Use OpenID to verify your identity": "Use OpenID to verify your identity", - "Access your display name": "Access your display name", - "Access your group membership": "Access your group membership", - "Access your email addresses": "Access your email addresses", - "Accept": "Accept", - "Deny": "Deny", - "The above application is requesting the following permissions": "The above application is requesting the following permissions", - "The password does not meet the password policy": "The password does not meet the password policy", - "Must have at least one lowercase letter": "Must have at least one lowercase letter", - "Must have at least one UPPERCASE letter": "Must have at least one UPPERCASE letter", - "Must have at least one number": "Must have at least one number", - "Must have at least one special character": "Must have at least one special character", - "Must be at least {{len}} characters in length": "Must be at least {{len}} characters in length", - "Must not be more than {{len}} characters in length": "Must not be more than {{len}} characters in length" - } -} diff --git a/web/src/i18n/locales/es.json b/web/src/i18n/locales/es.json deleted file mode 100644 index 34b6e1b22..000000000 --- a/web/src/i18n/locales/es.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "Portal": { - "An email has been sent to your address to complete the process": "Un correo ha sido enviado a su cuenta para completar el proceso", - "Authenticated": "Autenticado", - "Cancel": "Cancelar", - "Contact your administrator to register a device": "Contacte a su administrador para registrar un dispositivo.", - "Could not obtain user settings": "Error al obtener configuración de usuario", - "Done": "Hecho", - "Enter new password": "Ingrese una nueva contraseña", - "Enter one-time password": "Ingrese contraseña de un solo uso (OTP)", - "Failed to register device, the provided link is expired or has already been used": "Error al registrar dispositivo, el link expiró o ya ha sido utilizado", - "Hi": "Hola", - "Incorrect username or password": "Usuario y/o contraseña incorrectos", - "Loading": "Cargando", - "Logout": "Cerrar Sesión", - "Lost your device?": "Perdió su dispositivo?", - "Methods": "Métodos", - "Need Google Authenticator?": "Necesita Google Authenticator?", - "New password": "Nueva contraseña", - "No verification token provided": "No se ha recibido el token de verificación", - "OTP Secret copied to clipboard": "La clave OTP ha sido copiada al portapapeles", - "OTP URL copied to clipboard": "la URL OTP ha sido copiada al portapapeles.", - "One-Time Password": "Contraseña de un solo uso (OTP)", - "Password has been reset": "La contraseña ha sido restablecida.", - "Password": "Contraseña", - "Passwords do not match": "Las contraseñas no coinciden.", - "Push Notification": "Notificaciones Push", - "Register device": "Registrar Dispositivo", - "Register your first device by clicking on the link below": "Registre su primer dispositivo, haciendo click en el siguiente link.", - "Remember me": "Recordarme", - "Repeat new password": "Repetir la contraseña", - "Reset password": "Restablecer Contraseña", - "Reset password?": "Olvidé mi contraseña", - "Reset": "Restablecer", - "Scan QR Code": "Escanear Código QR", - "Secret": "Secreto", - "Security Key - WebAuthN": "Llave de Seguridad - WebAuthN", - "Select a Device": "Seleccionar Dispositivo", - "Sign in": "Iniciar Sesión", - "Sign out": "Cerrar Sesión", - "The resource you're attempting to access requires two-factor authentication": "El recurso que intenta alcanzar requiere un segundo factor de autenticación (2FA).", - "There was a problem initiating the registration process": "Ocurrió un problema al iniciar el proceso de registración", - "There was an issue completing the process. The verification token might have expired": "Ocurrió un problema mientras se completaba el proceso. El token de verificación pudo haber expirado.", - "There was an issue initiating the password reset process": "Ha ocurrido un error al iniciar el proceso de proceso de restauración de contraseña.", - "There was an issue resetting the password": "Ocurrió un error al intentar restablecer la contraseña", - "There was an issue signing out": "Ocurrió un error al intentar cerrar sesión", - "Time-based One-Time Password": "Contraseña de uso único - OTP", - "Username": "Usuario", - "You must open the link from the same device and browser that initiated the registration process": "Debe abrir el link desde el mismo dispositivo y navegador desde el que inició el proceso de registración", - "You're being signed out and redirected": "Cerrando Sesión y redirigiendo", - "Your supplied password does not meet the password policy requirements": "La contraseña suministrada no cumple con los requerimientos de la política de contraseñas", - "Use OpenID to verify your identity": "Utilizar OpenID para verificar su identidad", - "Access your display name": "Acceso a su nombre", - "Access your group membership": "Acceso a su(s) grupo(s)", - "Access your email addresses": "Acceso a su dirección de correo", - "Accept": "Aceptar", - "Deny": "Denegar", - "The above application is requesting the following permissions": "La aplicación solicita los siguientes permisos", - "The password does not meet the password policy": "La contraseña no cumple con la política de contraseñas", - "Must have at least one lowercase letter": "Debe contener al menos una letra minúscula", - "Must have at least one UPPERCASE letter": "Debe contener al menos una letra MAYUSCULA", - "Must have at least one number": "Debe contener al menos un número", - "Must have at least one special character": "Debe contener al menos un caracter especial", - "Must be at least {{len}} characters in length": "La longitud mínima es de {{len}} caracteres", - "Must not be more than {{len}} characters in length": "La longitud máxima es de {{len}} caracteres" - } -} diff --git a/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx b/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx index ff0db417d..aec29e1eb 100644 --- a/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx +++ b/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx @@ -27,7 +27,7 @@ const RegisterOneTimePassword = function () { const { createSuccessNotification, createErrorNotification } = useNotifications(); const [hasErrored, setHasErrored] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); // Get the token from the query param to give it back to the API when requesting // the secret for OTP. diff --git a/web/src/views/LoadingPage/BaseLoadingPage.tsx b/web/src/views/LoadingPage/BaseLoadingPage.tsx new file mode 100644 index 000000000..39dd58132 --- /dev/null +++ b/web/src/views/LoadingPage/BaseLoadingPage.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +import { Grid, Typography, useTheme } from "@material-ui/core"; +import ReactLoading from "react-loading"; + +export interface Props { + message: string; +} + +const BaseLoadingPage = function (props: Props) { + const theme = useTheme(); + return ( + + + + {props.message}... + + + ); +}; + +export default BaseLoadingPage; diff --git a/web/src/views/LoadingPage/LoadingPage.tsx b/web/src/views/LoadingPage/LoadingPage.tsx index da7d0e52e..5181ef879 100644 --- a/web/src/views/LoadingPage/LoadingPage.tsx +++ b/web/src/views/LoadingPage/LoadingPage.tsx @@ -1,20 +1,12 @@ import React from "react"; -import { useTheme, Typography, Grid } from "@material-ui/core"; import { useTranslation } from "react-i18next"; -import ReactLoading from "react-loading"; + +import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage"; const LoadingPage = function () { - const theme = useTheme(); - const { t: translate } = useTranslation("Portal"); - return ( - - - - {translate("Loading")}... - - - ); + const { t: translate } = useTranslation(); + return ; }; export default LoadingPage; diff --git a/web/src/views/LoginPortal/Authenticated.tsx b/web/src/views/LoginPortal/Authenticated.tsx index aa2f4dd98..42aa714a5 100644 --- a/web/src/views/LoginPortal/Authenticated.tsx +++ b/web/src/views/LoginPortal/Authenticated.tsx @@ -7,7 +7,7 @@ import SuccessIcon from "@components/SuccessIcon"; const Authenticated = function () { const classes = useStyles(); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); return (
diff --git a/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx b/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx index 15a73d623..9033745d2 100644 --- a/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx +++ b/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx @@ -15,7 +15,7 @@ export interface Props { const AuthenticatedView = function (props: Props) { const style = useStyles(); const navigate = useNavigate(); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); const handleLogoutClick = () => { navigate(SignOutRoute); diff --git a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx index d159f6170..a67429fbd 100644 --- a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx +++ b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx @@ -46,7 +46,7 @@ const ConsentView = function (props: Props) { const redirect = useRedirector(); const { createErrorNotification, resetNotification } = useNotifications(); const [resp, fetch, , err] = useRequestedScopes(); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); useEffect(() => { if (err) { diff --git a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx index df92f0610..cf0d02ca6 100644 --- a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx +++ b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx @@ -38,7 +38,7 @@ const FirstFactorForm = function (props: Props) { // TODO (PR: #806, Issue: #511) potentially refactor const usernameRef = useRef() as MutableRefObject; const passwordRef = useRef() as MutableRefObject; - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); useEffect(() => { const timeout = setTimeout(() => usernameRef.current.focus(), 10); return () => clearTimeout(timeout); diff --git a/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx b/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx index 1d36b1804..ff4ae927b 100644 --- a/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx +++ b/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx @@ -28,7 +28,7 @@ export interface Props { const DefaultMethodContainer = function (props: Props) { const style = useStyles(); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); const registerMessage = props.registered ? props.title === "Push Notification" ? "" @@ -97,7 +97,7 @@ interface NotRegisteredContainerProps { } function NotRegisteredContainer(props: NotRegisteredContainerProps) { - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); const theme = useTheme(); return ( diff --git a/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx b/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx index 5022ade91..cae62a646 100644 --- a/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx +++ b/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx @@ -29,7 +29,7 @@ export interface Props { const MethodSelectionDialog = function (props: Props) { const style = useStyles(); const theme = useTheme(); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); const pieChartIcon = ( diff --git a/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx b/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx index d8f38a779..40c5c8314 100644 --- a/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx +++ b/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx @@ -33,7 +33,7 @@ const OneTimePasswordMethod = function (props: Props) { props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle, ); const redirectionURL = useRedirectionURL(); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); const { onSignInSuccess, onSignInError } = props; const onSignInErrorCallback = useRef(onSignInError).current; diff --git a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx index 06387ef7e..73b48cca7 100644 --- a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx +++ b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx @@ -41,7 +41,7 @@ const SecondFactorForm = function (props: Props) { const { createInfoNotification, createErrorNotification } = useNotifications(); const [registrationInProgress, setRegistrationInProgress] = useState(false); const [webauthnSupported, setWebauthnSupported] = useState(false); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); useEffect(() => { setWebauthnSupported(isWebauthnSupported()); diff --git a/web/src/views/LoginPortal/SignOut/SignOut.tsx b/web/src/views/LoginPortal/SignOut/SignOut.tsx index f0c64fe67..39422217c 100644 --- a/web/src/views/LoginPortal/SignOut/SignOut.tsx +++ b/web/src/views/LoginPortal/SignOut/SignOut.tsx @@ -22,7 +22,7 @@ const SignOut = function (props: Props) { const redirector = useRedirector(); const [timedOut, setTimedOut] = useState(false); const [safeRedirect, setSafeRedirect] = useState(false); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); const doSignOut = useCallback(async () => { try { diff --git a/web/src/views/ResetPassword/ResetPasswordStep1.tsx b/web/src/views/ResetPassword/ResetPasswordStep1.tsx index 6c6761806..0c1d3c67a 100644 --- a/web/src/views/ResetPassword/ResetPasswordStep1.tsx +++ b/web/src/views/ResetPassword/ResetPasswordStep1.tsx @@ -16,7 +16,7 @@ const ResetPasswordStep1 = function () { const [error, setError] = useState(false); const { createInfoNotification, createErrorNotification } = useNotifications(); const navigate = useNavigate(); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); const doInitiateResetPasswordProcess = async () => { if (username === "") { diff --git a/web/src/views/ResetPassword/ResetPasswordStep2.tsx b/web/src/views/ResetPassword/ResetPasswordStep2.tsx index b308bc9d3..bf1e61463 100644 --- a/web/src/views/ResetPassword/ResetPasswordStep2.tsx +++ b/web/src/views/ResetPassword/ResetPasswordStep2.tsx @@ -25,7 +25,7 @@ const ResetPasswordStep2 = function () { const [errorPassword1, setErrorPassword1] = useState(false); const [errorPassword2, setErrorPassword2] = useState(false); const { createSuccessNotification, createErrorNotification } = useNotifications(); - const { t: translate } = useTranslation("Portal"); + const { t: translate } = useTranslation(); const navigate = useNavigate(); const [showPassword, setShowPassword] = useState(false);