Merge pull request #16 from Myzel394/improve-i18n

Improve i18n
This commit is contained in:
Myzel394 2023-03-05 12:14:21 +01:00 committed by GitHub
commit d88c4fc2e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 1706 additions and 1695 deletions

View File

@ -0,0 +1,71 @@
{
"title": "Global Settings",
"description": "Configure global settings for your instance.",
"updatedSuccessfullyMessage": "Settings have been saved successfully!",
"randomAliasesPreview": {
"title": "Random aliases will look like this",
"helperText": "This is just a preview. Those are not real aliases."
},
"randomAliasesIncreaseExplanation": "Random aliases' length will be increased from {{originalLength}} to {{increasedLength}} characters after {{amount}} aliases have been created.",
"resetLabel": "Reset to defaults",
"disabled": {
"title": "Global settings are disabled",
"description": "Global settings have been disabled. You can enable them in the configuration file."
},
"fields": {
"randomEmailIdMinLength": {
"label": "Minimum random alias ID length",
"description": "The minimum length for randomly generated emails. The server will automatically increase the length if required so."
},
"randomEmailIdChars": {
"label": "Random alias character pool",
"description": "Characters that are used to generate random emails."
},
"randomEmailLengthIncreaseOnPercentage": {
"label": "Percentage of used aliases",
"description": "If the percentage of used random email IDs is higher than this value, the length of the random email ID will be increased. This is used to prevent spammers from guessing the email ID."
},
"customEmailSuffixLength": {
"label": "Custom email suffix length",
"description": "The length of the custom email suffix."
},
"customEmailSuffixChars": {
"label": "Custom email suffix character pool",
"description": "Characters that are used to generate custom email suffixes."
},
"imageProxyStorageLifeTimeInHours": {
"label": "Image proxy storage lifetime",
"description": "The lifetime of images that are stored on the server in hours. After this time, the image will be deleted.",
"unit_one": "hour",
"unit_other": "hours"
},
"enableImageProxy": {
"label": "Enable image proxy",
"description": "If enabled, images will be proxied through the server. This enhances your user's privacy as their ip address will not be leaked."
},
"enableImageProxyStorage": {
"label": "Enable image proxy storage",
"description": "If enabled, images will be stored on the server and forwarded to the user. This makes email tracking nearly impossible as every message will be marked as read instantly, which is obviously not true as a user is typically not able to view an email in just a few seconds after it has been sent. This will only affect new images."
},
"userEmailEnableDisposableEmails": {
"label": "Enable disposable emails for new accounts",
"description": "If enabled, users will be able to use disposable emails when creating a new account. This will only affect new accounts."
},
"userEmailEnableOtherRelays": {
"label": "Enable other relays for new accounts",
"description": "If enabled, users will be able to use other relays (such as SimpleLogin or DuckDuckGo's Email Tracking Protection) when creating a new account. This will only affect new accounts."
},
"allowStatistics": {
"label": "Allow statistics",
"description": "If enabled, your instance will collect anonymous statistics and share them. They will only be stored locally on this instance but made public."
},
"allowAliasDeletion": {
"label": "Allow alias deletion",
"description": "If enabled, users will be able to delete their aliases."
},
"maxAliasesPerUser": {
"label": "Maximum aliases per user",
"description": "The maximum number of aliases a user can create. 0 means unlimited. Existing aliases will not be affected."
}
}
}

View File

@ -0,0 +1,44 @@
{
"title": "Reserved Aliases",
"detailsTitle": "Reserved Alias Details",
"pageActions": {
"search": {
"placeholder": "Search for aliases"
}
},
"actions": {
"create": {
"label": "Create new Reserved Alias"
},
"delete": {
"label": "Delete Reserved Alias",
"description": "Are you sure you want to delete this reserved alias?",
"continueActionLabel": "Delete Reserved Alias"
}
},
"userAmount_one": "Forwards to one user",
"userAmount_other": "Forwards to {{count}} users",
"emptyState": {
"title": "Create your first reserved alias",
"description": "Reserved aliases are aliases that will be forwarded to selected admin users. This is useful if you want to create aliases that are meant to be public, like contact@example.com or hello@example.com."
},
"fields": {
"active": {
"label": "Active"
},
"users": {
"label": "Users",
"me": "{{email}} (Me)"
}
},
"createNew": {
"title": "Reserved Aliases",
"description": "Define what alias should forward to whom.",
"continueActionLabel": "Create Reserved Alias",
"explanation": {
"step1": "User from outside",
"step2": "Sends mail to",
"step4": "KleckRelay forwards to"
}
}
}

View File

@ -0,0 +1,12 @@
{
"title": "Seite konfigurieren",
"routes": {
"reservedAliases": "Reservierte Aliase",
"settings": "Einstellungen"
},
"serverStatus": {
"noRecentReports": "Es scheint, als gäbe es Probleme mit deinem Server. Cleanup Jobs wurden in den letzten Tagen nicht mehr ausgeführt. Der letzte Bereicht war am {{date}}.",
"error": "Es gab einen Fehler beim Ausführen des Cleanup Jobs {{relativeDescription}}. Bitte überprüfe die Logs für weitere Informationen.",
"success": "Alles okay mit deinem Server! Der letzte Cleanup Job {{relativeDescription}} wurde erfolgreich ausgeführt."
}
}

View File

@ -0,0 +1,33 @@
{
"title": "Notes",
"form": {
"createdAt": {
"label": "Created at",
"empty": "Unavailable"
},
"creationContext": {
"label": "Creation Context",
"values": {
"web": "Created on this instance",
"extension": "Created in the extension",
"extension-inline": "Created using the extension"
}
},
"createdOn": {
"label": "Created on"
},
"personalNotes": {
"label": "Personal Notes",
"helperText": "You can enter personal notes for this alias here. Notes are encrypted."
},
"websites": {
"label": "Websites",
"emptyText": "You haven't used this alias on any site yet.",
"placeholder": "https://example.com",
"helperText": "Add a website to this alias. Used to autofill.",
"errors": {
"invalid": "This URL is invalid."
}
}
}
}

View File

@ -0,0 +1,84 @@
{
"title": "Aliases",
"detailsTitle": "Alias Details",
"isInCopyMode": "You are in copy mode. Click on an alias to copy it to your clipboard.",
"emptyState": {
"title": "Welcome to your Aliases!",
"description": "Create your first Alias to get started."
},
"pageActions": {
"search": {
"placeholder": "Search for names"
},
"searchFilter": {
"active": "Active",
"inactive": "Inactive"
},
"typeFilter": {
"custom": "Custom made",
"random": "Randomly generated"
}
},
"actions": {
"createRandomAlias": {
"title": "Create Random Alias"
},
"createCustomAlias": {
"title": "Create Custom Alias",
"description": "You can define your own custom alias. Note that a random suffix will be added at the end to avoid duplicates.",
"continueActionLabel": "Create Alias"
},
"delete": {
"label": "Delete Alias",
"description": "Are you sure you want to delete this alias?",
"continueActionLabel": "Delete Alias"
}
},
"aliasTypeExplanation": {
"random": "This is a randomly generated alias",
"custom": "This is a custom-made alias"
},
"settings": {
"title": "Settings",
"description": "These settings apply to this alias only. You can either set a value manually or refer to your default settings. Note that this does change in behavior. When you set a value to refer to your default setting, the alias will always use the latest value. So when you change your default setting, the alias will automatically use the new value.",
"continueActionLabel": "Save Settings",
"fields": {
"removeTrackers": {
"label": "Remove Trackers",
"helperText": "Remove single-pixel image trackers as well as url trackers."
},
"createMailReport": {
"label": "Create Mail Reports",
"helperText": "Create reports of emails sent to aliases. Reports are end-to-end encrypted. Only you can access them."
},
"proxyImages": {
"label": "Proxy Images",
"helperText": "Proxies images in your emails through this KleckRelay instance. This adds an extra layer of privacy. Images are loaded immediately after we receive the email. They then will be stored for some time (cache time). During that time, the image will be served from us. This means the sender has no idea you have opened the mail. After the cache time, the image is loaded from the sender, but it will be forwarded by us. This means the sender will not be able to access your IP address nor your browser data."
},
"imageProxyFormat": {
"label": "Image File Type",
"values": {
"jpeg": "JPEG",
"png": "PNG",
"webp": "WEBP"
}
},
"proxyUserAgent": {
"label": "Proxy User Agent",
"helperText": "An User Agent is a identifier each browser and email client sends when retrieving files, such as images. You can specify here what user agent you would like to be used when we forward it. User Agents are kept up-to-date.",
"values": {
"apple-mail": "Apple Mail",
"google-mail": "Google Mail",
"outlook-windows": "Outlook / Windows",
"outlook-macos": "Outlook / MacOS",
"firefox": "Firefox Browser",
"chrome": "Chrome Browser"
}
},
"expandUrlShorteners": {
"label": "Expand URL Shorteners",
"helperText": "Expand shortened URLs (for example bit.ly) to their original URL. This way those services can't track you."
}
}
}
}

View File

@ -0,0 +1,89 @@
{
"fields": {
"email": {
"label": "Email",
"placeholder": "johndoe@example.com",
"errors": {
"disposable": "Disposable email addresses are not allowed."
}
},
"2faCode": {
"label": "Code",
"placeholder": "123456",
"errors": {
"shouldOnlyBeDigits": "The code should only contain digits."
}
},
"recoveryCode": {
"label": "Recovery Code"
},
"password": {
"label": "Password",
"placeholder": "********",
"errors": {
"invalid": "Password is invalid."
}
},
"passwordConfirmation": {
"label": "Confirm Password",
"placeholder": "********",
"errors": {
"mismatch": "Passwords do not match."
}
},
"customAliasLocal": {
"label": "Address",
"placeholder": "awesome-fish"
},
"local": {
"label": "Address"
},
"search": {
"label": "Search"
}
},
"messages": {
"errors": {
"unknown": "An unknown error occurred.",
"copyFailed": "Copying to clipboard did not work. Please copy the text manually."
},
"alias": {
"addressCopied": "Address has been copied to your clipboard!",
"created": "Alias has been created successfully!",
"deleted": "Alias has been deleted!",
"updated": "Alias has been updated successfully!",
"changedToEnabled": "Alias has been enabled",
"changedToDisabled": "Alias has been disabled"
},
"report": {
"deleted": "Report has been deleted!"
}
},
"general": {
"cancelLabel": "Cancel",
"yesLabel": "Yes",
"noLabel": "No",
"continueLabel": "Continue",
"unavailableValue": "Unavailable",
"experimentalFeatureExplanation": "This is an experimental feature.",
"saveLabel": "Save",
"resetLabel": "Reset",
"loading": "Loading..."
},
"noSearchResults": {
"title": "Nothing found",
"description": "We couldn't find anything for your search query. Try again with a different query."
},
"navigation": {
"overview": "Overview",
"aliases": "Aliases",
"reports": "Reports",
"settings": "Settings",
"admin": "Admin"
},
"routes": {
"signup": "Sign up",
"login": "Log in",
"logout": "Log out"
}
}

View File

@ -0,0 +1,16 @@
{
"forms": {
"askForGeneration": {
"title": "Generate Email Reports?",
"description": "Would you like to create fully encrypted email reports for your mails? Only you will be able to access them. Not even we can decrypt them."
},
"enterPassword": {
"title": "Set up your password",
"description": "Please enter a safe password so that we can encrypt your data."
}
},
"alreadyCompleted": {
"title": "Encryption already enabled",
"description": "You already have encryption enabled. Changing passwords is currently not supported."
}
}

View File

@ -0,0 +1,33 @@
{
"ResendMailButton": {
"label": "Resend Mail"
},
"OpenMailButton": {
"label": "Open Mail"
},
"TimedButton": {
"remainingTime_one": "({{count}})",
"remainingTime_other": "({{count}})"
},
"ErrorLoadingDataMessage": {
"tryAgain": "Try Again"
},
"LockNavigationContextProvider": {
"title": "Are you sure you want to leave?",
"description": "You have unsaved changes. If you leave, your changes will be lost.",
"continueLabel": "Leave"
},
"StringPoolField": {
"addCustom": {
"label": "Add custom"
},
"forms": {
"addNew": {
"title": "Add new value",
"description": "Enter your characters you would like to include",
"label": "Characters",
"submit": "Add"
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"actions": {
"enterDecryptionPassword": {
"title": "Decrypt Reports",
"description": "Please enter your password so that your reports can de decrypted.",
"cancelActionLabel": "Decrypt later"
},
"passwordMissing": {
"unavailable": {
"title": "Encryption required",
"description": "You need to set up encryption to use this feature.",
"continueActionLabel": "Set up encryption"
},
"passwordRequired": {
"title": "Password required",
"description": "Your decryption password is required to view this section.",
"continueActionLabel": "Enter password"
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"sharePassword": {
"title": "Share Password?",
"description": "An extension is asking for your password. Do you want to share it? Only continue if you initiated this action.",
"warning": "THIS WILL SHARE YOUR PASSWORD WITH THE EXTENSION. ALL YOUR DATA CAN BE DECRYPTED USING IT. ONLY CONTINUE IF YOU TRUST THE EXTENSION AND IF YOU INITIATED THIS REQUEST.",
"sharePassword": "Share Password",
"doNotShare": "Do not share",
"decideLater": "Decide later",
"doNotAskAgain": "Do not ask again"
}
}

View File

@ -0,0 +1,35 @@
{
"title": "Log in",
"forms": {
"email": {
"description": "We will send you a code to log in",
"continueActionLabel": "Send code"
},
"confirmCode": {
"title": "You got mail!",
"description": "We sent you a code to your email. Enter it below to login",
"continueActionLabel": "Log in",
"allowLoginFromDifferentDevices": "Allow login from different devices",
"expiringSoonWarning": "Your code will expire in less than a minute.",
"fields": {
"code": {
"label": "Verification Code",
"errors": {
"invalidChars": "Invalid verification code"
}
}
}
},
"confirmFromDifferentDevice": {
"title": "Login failed",
"description": "You could not be logged in. This could either be because you are not allowed to login from different devices or the verification code is invalid or expired."
},
"otp": {
"title": "Two-Factor Authentication",
"description": "Enter the code from your authenticator app",
"isUnavailable": "Your OTP verification time expired or you exceeded the maximum number of attempts. Please log in again.",
"codesLostActionLabel": "I lost my codes",
"continueActionLabel": "Log in"
}
}
}

View File

@ -0,0 +1,4 @@
{
"title": "Log out",
"description": "We are logging you out..."
}

View File

@ -0,0 +1,10 @@
{
"title": "Recover Two-Factor Authentication",
"description": "We are very sorry if you lost your codes. Please enter a recovery code to continue. Note that this will disable two-factor authentication for your account. You can enable it again in the settings.",
"continueActionLabel": "Disable 2FA",
"events": {
"unauthorized": "Please make sure to log in first and then reset your two-factor authentication on its screen.",
"canLogInNow": "Two-factor authentication has been disabled. You can log in now.",
"loggedIn": "Two-factor authentication has been disabled. You are logged in now."
}
}

View File

@ -0,0 +1,6 @@
{
"title": "Email relay service detected",
"description": "We detected that you are using an email relay service to sign up. This KleckRelay instance does not support relaying to another email relay service. You can either choose a different instance or sign up with a different email address.",
"detectedExplanation": "Detected email relay:",
"closeActionLabel": "Got it"
}

View File

@ -0,0 +1,68 @@
{
"title": "Reports",
"detailsTitle": "Report Details",
"emptyState": {
"title": "Welcome to your Reports!",
"description": "Here you will find your email reports. Currently, you don't have any reports. Wait until you receive an email."
},
"pageActions": {
"sort": {
"label": "Sorting",
"values": {
"List": "List reports by their date",
"GroupByAlias": "Group reports by their alias"
}
}
},
"emailMeta": {
"flow": "{{from}} -> {{to}}",
"emptySubject": "<No Subject>"
},
"actions": {
"delete": {
"label": "Delete Report",
"description": "Are you sure you want to delete this report?",
"continueActionLabel": "Delete Report"
}
},
"sections": {
"information": {
"title": "Email Information",
"form": {
"from": {
"label": "From"
},
"to": {
"label": "To"
},
"subject": {
"label": "Subject"
}
}
},
"trackers": {
"title": "Trackers",
"results": {
"imageTrackers": {
"text_zero": "No image trackers found",
"text_one": "Removed 1 image tracker",
"text_other": "Removed {{count}} image trackers"
},
"proxiedImages": {
"text_zero": "No images found",
"text_one": "Forwarding 1 image",
"text_other": "Forwarding {{count}} images",
"status": {
"isStored": "Stored on Server",
"isProxying": "Being forwarded"
}
},
"expandedUrls": {
"text_zero": "No shortened URLs found",
"text_one": "Expanded 1 URL",
"text_other": "Expanded {{count}} URLs"
}
}
}
}
}

View File

@ -0,0 +1,33 @@
{
"title": "Two-Factor-Authentication",
"alreadyEnabled": "You have successfully enabled 2FA!",
"setup": {
"description": "Enable 2FA to add an extra layer of security to your account. Each time you log in, you will need to enter a code generated from your authenticator app. This makes it harder for an attacker to hack into your account as they would need to have access to your phone.",
"setupLabel": "Enable 2FA",
"continueActionLabel": "Enable 2FA",
"codeExpired": "The verification time for your current Two-Factor-Authentication code has expired. A new code has been generated.",
"recoveryCodes": {
"title": "Note down your recovery codes",
"description": "These codes are used to recover your account if you lose access to your authenticator app. Note them down and store them in a safe place. You will not be able to view them again. Do not store them in your password manager. IF YOU LOSE YOUR RECOVERY CODES, YOU WILL LOSE ACCESS TO YOUR ACCOUNT. WE WILL NOT BE ABLE TO HELP YOU.",
"continueActionLabel": "I have noted down my recovery codes"
},
"success": "You have successfully enabled 2FA!"
},
"delete": {
"title": "Disable 2FA",
"steps": {
"askType": {
"code": "I have my 2FA code",
"recoveryCode": "I have a recovery code"
},
"askCode": {
"label": "Code"
},
"askRecoveryCode": {
"label": "Recovery Code"
}
},
"continueActionLabel": "Disable 2FA",
"success": "You have successfully disabled 2FA!"
}
}

View File

@ -0,0 +1,5 @@
{
"title": "Alias Preferences",
"description": "Select default values for your aliases. This only affects aliases you haven't set a custom value for.",
"continueActionLabel": "Save preferences"
}

View File

@ -0,0 +1,7 @@
{
"title": "Settings",
"actions": {
"enable2fa": "Two-Factor-Authentication",
"aliasPreferences": "Alias Preferences"
}
}

View File

@ -0,0 +1,18 @@
{
"forms": {
"email": {
"title": "Sign up",
"description": "We only need your email and you are ready to go!",
"continueActionLabel": "Continue"
},
"mailVerification": {
"title": "You got mail!",
"description": "We sent you an email with a link to confirm your email address. Please check your inbox and click on the link to continue.",
"editEmail": {
"title": "Edit email address?",
"description": "Would you like to return to the previous step and edit your email address?",
"continueActionLabel": "Yes, edit email"
}
}
}
}

View File

@ -1,410 +1,35 @@
{ {
"general": { "general": {
"cancelLabel": "Abbrechen", "cancelLabel": "Cancel",
"emptyValue": "-", "emptyValue": "-",
"emptyUnavailableValue": "Nicht verfügbar", "emptyUnavailableValue": "Unavailable",
"saveLabel": "Save",
"defaultValueSelection": "Standard <{{value}}>", "defaultValueSelection": "Default <{{value}}>",
"defaultValueSelectionRaw": "<{{value}}>", "defaultValueSelectionRaw": "<{{value}}>",
"booleanSelection": { "booleanSelection": {
"true": "Ja", "true": "Yes",
"false": "Nein" "false": "No"
}, },
"defaultError": "Ein Fehler ist aufgetreten.", "defaultError": "An error occurred.",
"defaultSuccess": "Erfolgreich übernommen!", "defaultSuccess": "Success!",
"loading": "Lädt...", "loading": "Loading...",
"actionNotUndoable": "Diese Aktion kann nicht rückgängig gemacht werden!", "actionNotUndoable": "This action cannot be undone!",
"copyError": "Konnte nicht in Zwischenable kopieren. Bitte kopiere den Text manuell.", "copyError": "Copying to clipboard did not work. Please copy the text manually.",
"experimentalFeature": "Diese Funktion ist experimentell und kann Fehler verursachen." "experimentalFeature": "This is an experimental feature.",
}, "deletedSuccessfully": "Deleted successfully!",
"appError": "We are sorry but there was an error. Please try again later."
"routes": {
"OverviewRoute": {
"title": "Überblick",
"description": "Nicht viel zu sehen hier, bisher."
},
"LoginRoute": {
"forms": {
"email": {
"title": "Anmelden",
"description": "Wir senden dir einen Verifizierungscode an deine E-Mail Adresse.",
"continueAction": "Code senden",
"form": {
"email": {
"label": "E-Mail",
"placeholder": "maxmustermann@example.com"
}
}
},
"confirmCode": {
"title": "Du hast Mail!",
"description": "Wir haben einen Code an deine E-Mail gesendet. Gib ihn hier ein, um dich anzumelden.",
"continueAction": "Anmelden",
"allowLoginFromDifferentDevices": "Anmelden von anderen Geräten erlauben",
"expiringSoon": "Dein Code läuft in weniger als einer Minute ab.",
"form": {
"code": {
"label": "Verifizierungscode",
"errors": {
"invalidChars": "Ungültiger Verifizierungscode"
}
}
}
},
"confirmFromDifferentDevice": {
"title": "Anmelden fehlgeschlagen",
"description": "Du konntest nicht angemeldet werden. Dies könnte daran legen, dass du Anmeldeungen aus anderen Geräten nicht aktiviert hast oder der Verifizierungscode inkorrekt oder abgelaufen ist."
}
}
},
"SignupRoute": {
"forms": {
"email": {
"title": "Registrieren",
"description": "Wir brauchen nur deine E-Mail und du kannst direkt loslegen!",
"continueAction": "Weiter",
"form": {
"email": {
"label": "E-Mail",
"placeholder": "maxmustermann@example.com"
}
}
},
"mailVerification": {
"title": "Du hast Mail!",
"description": "Wir haben dir eine E-Mail mit einem Link geschickt, um deinen Account zu verifizieren. Bitte überprüfe deine E-Mails und klicke auf den Link, um fortzufahren.",
"editEmail": {
"title": "E-Mail ändern?",
"description": "Möchtest du einen Schritt zurückgehen um deine E-Mail zu ändern?",
"continueAction": "Ja, E-Mail ändern"
}
}
}
},
"VerifyEmailRoute": {
"title": "Bestätige deine E-Mail",
"isLoading": "Deine E-Mail wird bestätigt...",
"isCodeInvalid": "Der Verifizierungscode ist ungültig oder abgelaufen.",
"errors": {
"code": {
"invalid": "Dieser Verifizierungscode ist ungültig"
}
}
},
"CompleteAccountRoute": {
"forms": {
"generateReports": {
"title": "E-Mail-Berichte aktivieren?",
"description": "Möchtest du vollständig verschlüsselte Berichte für deine E-Mails erstellen lassen? Nur du wirst sie entschlüsseln können. Selbst wir können sie nicht entschlüsseln.",
"continueAction": "Ja",
"cancelAction": "Nein"
},
"password": {
"title": "Passwort festlegen",
"description": "Bitte gib ein sicheres Passwort ein, damit wir deine Berichte verschlüsseln können.",
"continueAction": "Weiter",
"form": {
"password": {
"label": "Passwort",
"placeholder": "********"
},
"passwordConfirm": {
"label": "Passwort bestätigen",
"placeholder": "Gib dein Passwort erneut ein",
"mustMatchHelperText": "Passwörter stimmen nicht überein."
}
}
},
"available": {
"title": "Verschlüsselung bereits eingestellt",
"description": "Du hast die Verschlüsselung bereits eingestellt. Passwörter können momentan noch nicht geändert werden."
}
}
},
"AliasesRoute": {
"title": "Aliase",
"isInCopyMode": "Du bist im Kopier-Modus. Klicke auf einen Alias um ihn in deine Zwischenablage zu kopieren.",
"emptyState": {
"title": "Willkommen zu deinen Aliases!",
"description": "Erstelle dein erstes Alias, um loszulegen."
},
"pageActions": {
"search": {
"label": "Suche",
"placeholder": "Suche nach Namen"
}
},
"actions": {
"createRandomAlias": {
"label": "Zufälliges Alias erstellen"
},
"createCustomAlias": {
"label": "Eigenes Alias erstellen",
"description": "Du kannst dein eigenes Alias erstellen. Beachte das ein zufälliges Suffix angehangen wird, um Duplikate zu vermeiden.",
"continueAction": "Alias erstellen",
"form": {
"address": {
"label": "Adresse",
"placeholder": "awesome-fish"
}
}
}
}
},
"AliasDetailRoute": {
"title": "Alias-Details",
"sections": {
"settings": {
"title": "Einstellungen",
"description": "Diese Einstellungen gelten nur für dieses Alias. Du kannst entweder einen manuellen Wert einstellen oder auf deine Standard-Werte verweisen. Beachte das dieser Wert das Verhalten ändert. Wenn du auf einen Standard-Wert verweist, verwendet dein Alias immer den aktuellsten Wert. Wenn du also deine Standard-Werte änderst, übernimmt dein Alias diese Änderungen."
},
"notes": {
"title": "Notizen",
"form": {
"createdAt": {
"label": "Erstellungsdatum",
"empty": "Nicht verfügbar"
},
"creationContext": {
"label": "Kontext",
"web": {
"label": "Auf dieser Instanz erstellt"
},
"extension": {
"label": "In der Erweiterung erstellt"
},
"extension-inline": {
"label": "Mit der Erweiterung erstellt"
}
},
"createdOn": {
"label": "Erstellt auf"
},
"personalNotes": {
"label": "Persönliche Notizen",
"empty": "-",
"helperText": "Hier kannst du persönliche Notizen für dieses Alias eingeben. Notizen sind verschlüsselt."
},
"websites": {
"label": "Webseiten",
"emptyText": "Du hast dieses Alias auf keiner Webseite bisher genutzt.",
"placeholder": "https://example.com",
"helperText": "Füge eine Webseite zu diesem Alias hinzu. Wird verwendet um automatisch E-Mail-Felder auszufüllen."
}
}
}
}
},
"ReportsRoute": {
"title": "Berichte",
"emptyState": {
"title": "Willkommen zu deinen Berichten!",
"description": "Hier kannst du deine E-Mail-Berichte finden. Momentan sind noch keine Berichte verfügbar. Warte, bis du eine E-Mail erhalten hast."
},
"pageActions": {
"sort": {
"List": "Berichte anhand ihrer Daten auflisten",
"GroupByAlias": "Berichte nach Alias gruppieren"
}
}
},
"ReportDetailRoute": {
"title": "Bericht-Details",
"actions": {
"delete": {
"label": "Bericht löschen",
"description": "Bist du dir sicher, dass du diesen Bericht löschen möchtest?",
"continueAction": "Bericht löschen"
}
},
"sections": {
"information": {
"title": "Email-Informationen",
"form": {
"from": {
"label": "Von"
},
"to": {
"label": "Zu"
},
"subject": {
"label": "Betreff"
}
}
},
"trackers": {
"title": "Tracker",
"results": {
"imageTrackers": {
"text_zero": "Keine Bild-Tracker gefunden",
"text_one": "Ein Bild-Tracker entfernt",
"text_other": "{{count}} Bild-Tracker entfernt"
},
"proxiedImages": {
"text_zero": "Keine Bilder gefunden",
"text_one": "Ein Bild wird weitergeleitet",
"text_other": "{{count}} Bilder werden weitergeleitet",
"status": {
"isStored": "Auf Server gespeichert",
"isProxying": "Wird weitergeleitet"
}
},
"expandedUrls": {
"text_zero": "Keine gekürzten URLs gefunden",
"text_one": "Eine URL entkürzt",
"text_other": "{{count}} URLs entkürzt"
}
}
}
}
},
"SettingsRoute": {
"title": "Einstellungen",
"forms": {
"aliasPreferences": {
"title": "Alias-Präferenzen",
"description": "Wähle die Standard-Werte für deine Aliase aus. Dies betrifft nur Aliase, bei denen du keinen manuellen Wert gesetzt hast.",
"saveAction": "Präferenzen speichern"
}
}
},
"LogoutRoute": {
"title": "Abmelden",
"description": "Wir sind dich am abmelden..."
}
}, },
"components": { "components": {
"NavigationButton": { "passwordShareConfirmationDialog": {
"overview": "Überblick", "title": "Share Password?",
"aliases": "Aliase", "description": "An extension is asking for your password. Do you want to share it? Only continue if you initiated this action.",
"reports": "Berichte", "warning": "THIS WILL SHARE YOUR PASSWORD WITH THE EXTENSION. ALL YOUR DATA CAN BE DECRYPTED USING IT. ONLY CONTINUE IF YOU TRUST THE EXTENSION AND IF YOU INITIATED THIS REQUEST.",
"settings": "Einstellungen" "continueAction": "Share Password",
}, "doNotShare": "Do not share",
"AuthenticateRoute": { "decideLater": "Decide later",
"signup": "Registrieren", "doNotAskAgain": "Do not ask again"
"login": "Anmelden"
},
"AuthenticatedRoute": {
"logout": "Abmelden"
},
"EnterDecryptionPassword": {
"title": "Berichte entschlüsseln",
"description": "Bitte gib dein Passwort ein, damit deine Berichte entschlüsselt werden können.",
"cancelAction": "Später entschlüsseln",
"continueAction": "Weiter",
"form": {
"password": {
"label": "Passwort",
"placeholder": "********",
"errors": {
"invalidPassword": "Das Passwort ist ungültig"
}
}
}
},
"ResendMailButton": {
"label": "E-Mail erneut senden"
},
"OpenMailButton": {
"label": "E-Mail öffnen"
},
"DecryptionPasswordMissingAlert": {
"unavailable": {
"title": "Verschlüsselung benötigt",
"description": "Du musst die Verschlüsselung aktivieren, um dieses Feature nutzen zu können.",
"continueAction": "Verschlüsselung aktivieren"
},
"passwordRequired": {
"title": "Passwort benötigt",
"description": "Dein Passwort wird benötigt, um dieses Feature nutzen zu können.",
"continueAction": "Passwort eingeben"
}
},
"TimedButton": {
"remainingTime_one": "({{count}})",
"remainingTime_other": "({{count}})"
},
"ErrorLoadingDataMessage": {
"tryAgain": "Neu laden"
},
"AliasTypeIndicator": {
"random": "Dies ist ein zufällig-generiertes Alias",
"custom": "Dies ist ein benutzerdefiniertes Alias"
},
"NoSearchResults": {
"title": "Keine Ergebnisse gefunden",
"description": "Wir konnten keine Ergebnisse für diese Suche finden. Versuche es mit einem anderen Suchbegriff."
},
"LockNavigationContextProvider": {
"title": "Möchtest du wirklich die Seite verlassen?",
"description": "Du hast Änderungen, welche noch nicht gespeichert wurden. Wenn du jetzt diese Seite verlässt, gehen deine Änderungen verloren.",
"continueLabel": "Verlassen"
}
},
"relations": {
"alias": {
"mutations": {
"success": {
"aliasCreation": "Alias wurde erfolgreich erstellt!",
"aliasUpdated": "Alias wurde erfolgreich upgedatet!",
"notesUpdated": "Notizen wurden erfolgreich upgedated & verschlüsselt!",
"aliasChangedToEnabled": "Alias wurde aktiviert",
"aliasChangedToDisabled": "Alias wurde deaktiviert",
"addressCopiedToClipboard": "E-Mail-Adresse wurde in deine Zwischenablage kopiert!"
}
},
"settings": {
"removeTrackers": {
"label": "Tracker entfernen",
"helperText": "Entferne Einzelpixel-Tracker und URL-Tracker"
},
"createMailReports": {
"label": "E-Mail-Berichte erstellen",
"helperText": "Erstelle Berichte von E-Mails, die an Aliase gesendet werden. Berichte sind Ende-zu-Ende verschlüsselt. Nur du kannst sie entschlüsseln."
},
"proxyImages": {
"label": "Bilder weiterleiten",
"helperText": "Leitet Bilder durch diese KleckRelay-Instanz weiter. Dies stellt einen weiteren Schutz deiner Privatspähre dar. Bilder werden direkt runtergeladen nachdem wir eine E-Mail erhalten haben. Diese werden dann für eine gewisse Zeit auf dem Server gespeichert. Während dieser Zeit wird das Bild von uns an dich gesendet. Dies bedeutet, dass der Absender keine Chance haben wird, herauszufinden, dass du diese E-Mail geöffnet hast. Nach der Zeit wird das Bild vom Absender geladen, aber durch uns weitergeleitet. Dies bedeutet, dass der Absender weder auf deine IP-Adresse, noch auf deine Browserdaten zugreifen kann."
},
"imageProxyFormat": {
"label": "Bild-Format",
"enumTexts": {
"jpeg": "JPEG",
"png": "PNG",
"webp": "WEBP"
}
},
"proxyUserAgent": {
"label": "Bild-Weiterleitungs-User-Agent",
"helperText": "Ein User-Agent ist eine Kennzeichnung, die jeden Browser und E-Mail-Client identifiziert, wenn Dateien runtergeladen werden, so wie beispielsweise Bilder. Du kannst hier einstellen, welchen User-Agent du beim Weiterleiten verwenden möchtest. User-Agents werden aktuell gehalten.",
"enumTexts": {
"apple-mail": "Apple Mail",
"google-mail": "Google Mail",
"outlook-windows": "Outlook / Windows",
"outlook-macos": "Outlook / MacOS",
"firefox": "Firefox Browser",
"chrome": "Chrome Browser"
}
},
"expandUrlShorteners": {
"label": "URL-Kürzer entkürzen",
"helperText": "Entkürzt URl-Kürzerer (wie zum Beispiel bit.ly) zu der Original-URL. Dadurch können dich diese Services nicht mehr tracken."
},
"saveAction": "Einstellungen speichern"
}
},
"report": {
"mutations": {
"success": {
"reportDeleted": "Bericht wurde gelöscht!"
}
},
"emailMeta": {
"flow": "{{from}} -> {{to}}",
"emptySubject": "<Kein Betreff>"
}
} }
} }
} }

View File

@ -0,0 +1,7 @@
{
"title": "Verify your email",
"isLoading": "We are verifying your email address...",
"errors": {
"invalid": "The verification link is invalid or has expired."
}
}

View File

@ -0,0 +1,71 @@
{
"title": "Global Settings",
"description": "Configure global settings for your instance.",
"updatedSuccessfullyMessage": "Settings have been saved successfully!",
"randomAliasesPreview": {
"title": "Random aliases will look like this",
"helperText": "This is just a preview. Those are not real aliases."
},
"randomAliasesIncreaseExplanation": "Random aliases' length will be increased from {{originalLength}} to {{increasedLength}} characters after {{amount}} aliases have been created.",
"resetLabel": "Reset to defaults",
"disabled": {
"title": "Global settings are disabled",
"description": "Global settings have been disabled. You can enable them in the configuration file."
},
"fields": {
"randomEmailIdMinLength": {
"label": "Minimum random alias ID length",
"description": "The minimum length for randomly generated emails. The server will automatically increase the length if required so."
},
"randomEmailIdChars": {
"label": "Random alias character pool",
"description": "Characters that are used to generate random emails."
},
"randomEmailLengthIncreaseOnPercentage": {
"label": "Percentage of used aliases",
"description": "If the percentage of used random email IDs is higher than this value, the length of the random email ID will be increased. This is used to prevent spammers from guessing the email ID."
},
"customEmailSuffixLength": {
"label": "Custom email suffix length",
"description": "The length of the custom email suffix."
},
"customEmailSuffixChars": {
"label": "Custom email suffix character pool",
"description": "Characters that are used to generate custom email suffixes."
},
"imageProxyStorageLifeTimeInHours": {
"label": "Image proxy storage lifetime",
"description": "The lifetime of images that are stored on the server in hours. After this time, the image will be deleted.",
"unit_one": "hour",
"unit_other": "hours"
},
"enableImageProxy": {
"label": "Enable image proxy",
"description": "If enabled, images will be proxied through the server. This enhances your user's privacy as their ip address will not be leaked."
},
"enableImageProxyStorage": {
"label": "Enable image proxy storage",
"description": "If enabled, images will be stored on the server and forwarded to the user. This makes email tracking nearly impossible as every message will be marked as read instantly, which is obviously not true as a user is typically not able to view an email in just a few seconds after it has been sent. This will only affect new images."
},
"userEmailEnableDisposableEmails": {
"label": "Enable disposable emails for new accounts",
"description": "If enabled, users will be able to use disposable emails when creating a new account. This will only affect new accounts."
},
"userEmailEnableOtherRelays": {
"label": "Enable other relays for new accounts",
"description": "If enabled, users will be able to use other relays (such as SimpleLogin or DuckDuckGo's Email Tracking Protection) when creating a new account. This will only affect new accounts."
},
"allowStatistics": {
"label": "Allow statistics",
"description": "If enabled, your instance will collect anonymous statistics and share them. They will only be stored locally on this instance but made public."
},
"allowAliasDeletion": {
"label": "Allow alias deletion",
"description": "If enabled, users will be able to delete their aliases."
},
"maxAliasesPerUser": {
"label": "Maximum aliases per user",
"description": "The maximum number of aliases a user can create. 0 means unlimited. Existing aliases will not be affected."
}
}
}

View File

@ -0,0 +1,44 @@
{
"title": "Reserved Aliases",
"detailsTitle": "Reserved Alias Details",
"pageActions": {
"search": {
"placeholder": "Search for aliases"
}
},
"actions": {
"create": {
"label": "Create new Reserved Alias"
},
"delete": {
"label": "Delete Reserved Alias",
"description": "Are you sure you want to delete this reserved alias?",
"continueActionLabel": "Delete Reserved Alias"
}
},
"userAmount_one": "Forwards to one user",
"userAmount_other": "Forwards to {{count}} users",
"emptyState": {
"title": "Create your first reserved alias",
"description": "Reserved aliases are aliases that will be forwarded to selected admin users. This is useful if you want to create aliases that are meant to be public, like contact@example.com or hello@example.com."
},
"fields": {
"active": {
"label": "Active"
},
"users": {
"label": "Users",
"me": "{{email}} (Me)"
}
},
"createNew": {
"title": "Reserved Aliases",
"description": "Define what alias should forward to whom.",
"continueActionLabel": "Create Reserved Alias",
"explanation": {
"step1": "User from outside",
"step2": "Sends mail to",
"step4": "KleckRelay forwards to"
}
}
}

View File

@ -0,0 +1,12 @@
{
"title": "Site configuration",
"routes": {
"reservedAliases": "Reserved Aliases",
"settings": "Global Settings"
},
"serverStatus": {
"noRecentReports": "There seems to be some issues with your server. The server hasn't done its cleanup in the last few days. The last report was on {{date}}.",
"error": "There was an error during the last server cleanup job from {{relativeDescription}}. Please check the logs for more information.",
"success": "Everything okay with your server! The last cleanup job was {{relativeDescription}}."
}
}

View File

@ -0,0 +1,33 @@
{
"title": "Notes",
"form": {
"createdAt": {
"label": "Created at",
"empty": "Unavailable"
},
"creationContext": {
"label": "Creation Context",
"values": {
"web": "Created on this instance",
"extension": "Created in the extension",
"extension-inline": "Created using the extension"
}
},
"createdOn": {
"label": "Created on"
},
"personalNotes": {
"label": "Personal Notes",
"helperText": "You can enter personal notes for this alias here. Notes are encrypted."
},
"websites": {
"label": "Websites",
"emptyText": "You haven't used this alias on any site yet.",
"placeholder": "https://example.com",
"helperText": "Add a website to this alias. Used to autofill.",
"errors": {
"invalid": "This URL is invalid."
}
}
}
}

View File

@ -0,0 +1,84 @@
{
"title": "Aliases",
"detailsTitle": "Alias Details",
"isInCopyMode": "You are in copy mode. Click on an alias to copy it to your clipboard.",
"emptyState": {
"title": "Welcome to your Aliases!",
"description": "Create your first Alias to get started."
},
"pageActions": {
"search": {
"placeholder": "Search for names"
},
"searchFilter": {
"active": "Active",
"inactive": "Inactive"
},
"typeFilter": {
"custom": "Custom made",
"random": "Randomly generated"
}
},
"actions": {
"createRandomAlias": {
"title": "Create Random Alias"
},
"createCustomAlias": {
"title": "Create Custom Alias",
"description": "You can define your own custom alias. Note that a random suffix will be added at the end to avoid duplicates.",
"continueActionLabel": "Create Alias"
},
"delete": {
"label": "Delete Alias",
"description": "Are you sure you want to delete this alias?",
"continueActionLabel": "Delete Alias"
}
},
"aliasTypeExplanation": {
"random": "This is a randomly generated alias",
"custom": "This is a custom-made alias"
},
"settings": {
"title": "Settings",
"description": "These settings apply to this alias only. You can either set a value manually or refer to your default settings. Note that this does change in behavior. When you set a value to refer to your default setting, the alias will always use the latest value. So when you change your default setting, the alias will automatically use the new value.",
"continueActionLabel": "Save Settings",
"fields": {
"removeTrackers": {
"label": "Remove Trackers",
"helperText": "Remove single-pixel image trackers as well as url trackers."
},
"createMailReport": {
"label": "Create Mail Reports",
"helperText": "Create reports of emails sent to aliases. Reports are end-to-end encrypted. Only you can access them."
},
"proxyImages": {
"label": "Proxy Images",
"helperText": "Proxies images in your emails through this KleckRelay instance. This adds an extra layer of privacy. Images are loaded immediately after we receive the email. They then will be stored for some time (cache time). During that time, the image will be served from us. This means the sender has no idea you have opened the mail. After the cache time, the image is loaded from the sender, but it will be forwarded by us. This means the sender will not be able to access your IP address nor your browser data."
},
"imageProxyFormat": {
"label": "Image File Type",
"values": {
"jpeg": "JPEG",
"png": "PNG",
"webp": "WEBP"
}
},
"proxyUserAgent": {
"label": "Proxy User Agent",
"helperText": "An User Agent is a identifier each browser and email client sends when retrieving files, such as images. You can specify here what user agent you would like to be used when we forward it. User Agents are kept up-to-date.",
"values": {
"apple-mail": "Apple Mail",
"google-mail": "Google Mail",
"outlook-windows": "Outlook / Windows",
"outlook-macos": "Outlook / MacOS",
"firefox": "Firefox Browser",
"chrome": "Chrome Browser"
}
},
"expandUrlShorteners": {
"label": "Expand URL Shorteners",
"helperText": "Expand shortened URLs (for example bit.ly) to their original URL. This way those services can't track you."
}
}
}
}

View File

@ -0,0 +1,89 @@
{
"fields": {
"email": {
"label": "Email",
"placeholder": "johndoe@example.com",
"errors": {
"disposable": "Disposable email addresses are not allowed."
}
},
"2faCode": {
"label": "Code",
"placeholder": "123456",
"errors": {
"shouldOnlyBeDigits": "The code should only contain digits."
}
},
"recoveryCode": {
"label": "Recovery Code"
},
"password": {
"label": "Password",
"placeholder": "********",
"errors": {
"invalid": "Password is invalid."
}
},
"passwordConfirmation": {
"label": "Confirm Password",
"placeholder": "********",
"errors": {
"mismatch": "Passwords do not match."
}
},
"customAliasLocal": {
"label": "Address",
"placeholder": "awesome-fish"
},
"local": {
"label": "Address"
},
"search": {
"label": "Search"
}
},
"messages": {
"errors": {
"unknown": "An unknown error occurred.",
"copyFailed": "Copying to clipboard did not work. Please copy the text manually."
},
"alias": {
"addressCopied": "Address has been copied to your clipboard!",
"created": "Alias has been created successfully!",
"deleted": "Alias has been deleted!",
"updated": "Alias has been updated successfully!",
"changedToEnabled": "Alias has been enabled",
"changedToDisabled": "Alias has been disabled"
},
"report": {
"deleted": "Report has been deleted!"
}
},
"general": {
"cancelLabel": "Cancel",
"yesLabel": "Yes",
"noLabel": "No",
"continueLabel": "Continue",
"unavailableValue": "Unavailable",
"experimentalFeatureExplanation": "This is an experimental feature.",
"saveLabel": "Save",
"resetLabel": "Reset",
"loading": "Loading..."
},
"noSearchResults": {
"title": "Nothing found",
"description": "We couldn't find anything for your search query. Try again with a different query."
},
"navigation": {
"overview": "Overview",
"aliases": "Aliases",
"reports": "Reports",
"settings": "Settings",
"admin": "Admin"
},
"routes": {
"signup": "Sign up",
"login": "Log in",
"logout": "Log out"
}
}

View File

@ -0,0 +1,16 @@
{
"forms": {
"askForGeneration": {
"title": "Generate Email Reports?",
"description": "Would you like to create fully encrypted email reports for your mails? Only you will be able to access them. Not even we can decrypt them."
},
"enterPassword": {
"title": "Set up your password",
"description": "Please enter a safe password so that we can encrypt your data."
}
},
"alreadyCompleted": {
"title": "Encryption already enabled",
"description": "You already have encryption enabled. Changing passwords is currently not supported."
}
}

View File

@ -0,0 +1,33 @@
{
"ResendMailButton": {
"label": "Resend Mail"
},
"OpenMailButton": {
"label": "Open Mail"
},
"TimedButton": {
"remainingTime_one": "({{count}})",
"remainingTime_other": "({{count}})"
},
"ErrorLoadingDataMessage": {
"tryAgain": "Try Again"
},
"LockNavigationContextProvider": {
"title": "Are you sure you want to leave?",
"description": "You have unsaved changes. If you leave, your changes will be lost.",
"continueLabel": "Leave"
},
"StringPoolField": {
"addCustom": {
"label": "Add custom"
},
"forms": {
"addNew": {
"title": "Add new value",
"description": "Enter your characters you would like to include",
"label": "Characters",
"submit": "Add"
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"actions": {
"enterDecryptionPassword": {
"title": "Decrypt Reports",
"description": "Please enter your password so that your reports can de decrypted.",
"cancelActionLabel": "Decrypt later"
},
"passwordMissing": {
"unavailable": {
"title": "Encryption required",
"description": "You need to set up encryption to use this feature.",
"continueActionLabel": "Set up encryption"
},
"passwordRequired": {
"title": "Password required",
"description": "Your decryption password is required to view this section.",
"continueActionLabel": "Enter password"
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"sharePassword": {
"title": "Share Password?",
"description": "An extension is asking for your password. Do you want to share it? Only continue if you initiated this action.",
"warning": "THIS WILL SHARE YOUR PASSWORD WITH THE EXTENSION. ALL YOUR DATA CAN BE DECRYPTED USING IT. ONLY CONTINUE IF YOU TRUST THE EXTENSION AND IF YOU INITIATED THIS REQUEST.",
"sharePassword": "Share Password",
"doNotShare": "Do not share",
"decideLater": "Decide later",
"doNotAskAgain": "Do not ask again"
}
}

View File

@ -0,0 +1,35 @@
{
"title": "Log in",
"forms": {
"email": {
"description": "We will send you a code to log in",
"continueActionLabel": "Send code"
},
"confirmCode": {
"title": "You got mail!",
"description": "We sent you a code to your email. Enter it below to login",
"continueActionLabel": "Log in",
"allowLoginFromDifferentDevices": "Allow login from different devices",
"expiringSoonWarning": "Your code will expire in less than a minute.",
"fields": {
"code": {
"label": "Verification Code",
"errors": {
"invalidChars": "Invalid verification code"
}
}
}
},
"confirmFromDifferentDevice": {
"title": "Login failed",
"description": "You could not be logged in. This could either be because you are not allowed to login from different devices or the verification code is invalid or expired."
},
"otp": {
"title": "Two-Factor Authentication",
"description": "Enter the code from your authenticator app",
"isUnavailable": "Your OTP verification time expired or you exceeded the maximum number of attempts. Please log in again.",
"codesLostActionLabel": "I lost my codes",
"continueActionLabel": "Log in"
}
}
}

View File

@ -0,0 +1,4 @@
{
"title": "Log out",
"description": "We are logging you out..."
}

View File

@ -0,0 +1,10 @@
{
"title": "Recover Two-Factor Authentication",
"description": "We are very sorry if you lost your codes. Please enter a recovery code to continue. Note that this will disable two-factor authentication for your account. You can enable it again in the settings.",
"continueActionLabel": "Disable 2FA",
"events": {
"unauthorized": "Please make sure to log in first and then reset your two-factor authentication on its screen.",
"canLogInNow": "Two-factor authentication has been disabled. You can log in now.",
"loggedIn": "Two-factor authentication has been disabled. You are logged in now."
}
}

View File

@ -0,0 +1,6 @@
{
"title": "Email relay service detected",
"description": "We detected that you are using an email relay service to sign up. This KleckRelay instance does not support relaying to another email relay service. You can either choose a different instance or sign up with a different email address.",
"detectedExplanation": "Detected email relay:",
"closeActionLabel": "Got it"
}

View File

@ -0,0 +1,68 @@
{
"title": "Reports",
"detailsTitle": "Report Details",
"emptyState": {
"title": "Welcome to your Reports!",
"description": "Here you will find your email reports. Currently, you don't have any reports. Wait until you receive an email."
},
"pageActions": {
"sort": {
"label": "Sorting",
"values": {
"List": "List reports by their date",
"GroupByAlias": "Group reports by their alias"
}
}
},
"emailMeta": {
"flow": "{{from}} -> {{to}}",
"emptySubject": "<No Subject>"
},
"actions": {
"delete": {
"label": "Delete Report",
"description": "Are you sure you want to delete this report?",
"continueActionLabel": "Delete Report"
}
},
"sections": {
"information": {
"title": "Email Information",
"form": {
"from": {
"label": "From"
},
"to": {
"label": "To"
},
"subject": {
"label": "Subject"
}
}
},
"trackers": {
"title": "Trackers",
"results": {
"imageTrackers": {
"text_zero": "No image trackers found",
"text_one": "Removed 1 image tracker",
"text_other": "Removed {{count}} image trackers"
},
"proxiedImages": {
"text_zero": "No images found",
"text_one": "Forwarding 1 image",
"text_other": "Forwarding {{count}} images",
"status": {
"isStored": "Stored on Server",
"isProxying": "Being forwarded"
}
},
"expandedUrls": {
"text_zero": "No shortened URLs found",
"text_one": "Expanded 1 URL",
"text_other": "Expanded {{count}} URLs"
}
}
}
}
}

View File

@ -0,0 +1,33 @@
{
"title": "Two-Factor-Authentication",
"alreadyEnabled": "You have successfully enabled 2FA!",
"setup": {
"description": "Enable 2FA to add an extra layer of security to your account. Each time you log in, you will need to enter a code generated from your authenticator app. This makes it harder for an attacker to hack into your account as they would need to have access to your phone.",
"setupLabel": "Enable 2FA",
"continueActionLabel": "Enable 2FA",
"codeExpired": "The verification time for your current Two-Factor-Authentication code has expired. A new code has been generated.",
"recoveryCodes": {
"title": "Note down your recovery codes",
"description": "These codes are used to recover your account if you lose access to your authenticator app. Note them down and store them in a safe place. You will not be able to view them again. Do not store them in your password manager. IF YOU LOSE YOUR RECOVERY CODES, YOU WILL LOSE ACCESS TO YOUR ACCOUNT. WE WILL NOT BE ABLE TO HELP YOU.",
"continueActionLabel": "I have noted down my recovery codes"
},
"success": "You have successfully enabled 2FA!"
},
"delete": {
"title": "Disable 2FA",
"steps": {
"askType": {
"code": "I have my 2FA code",
"recoveryCode": "I have a recovery code"
},
"askCode": {
"label": "Code"
},
"askRecoveryCode": {
"label": "Recovery Code"
}
},
"continueActionLabel": "Disable 2FA",
"success": "You have successfully disabled 2FA!"
}
}

View File

@ -0,0 +1,5 @@
{
"title": "Alias Preferences",
"description": "Select default values for your aliases. This only affects aliases you haven't set a custom value for.",
"continueActionLabel": "Save preferences"
}

View File

@ -0,0 +1,7 @@
{
"title": "Settings",
"actions": {
"enable2fa": "Two-Factor-Authentication",
"aliasPreferences": "Alias Preferences"
}
}

View File

@ -0,0 +1,18 @@
{
"forms": {
"email": {
"title": "Sign up",
"description": "We only need your email and you are ready to go!",
"continueActionLabel": "Continue"
},
"mailVerification": {
"title": "You got mail!",
"description": "We sent you an email with a link to confirm your email address. Please check your inbox and click on the link to continue.",
"editEmail": {
"title": "Edit email address?",
"description": "Would you like to return to the previous step and edit your email address?",
"continueActionLabel": "Yes, edit email"
}
}
}
}

View File

@ -21,555 +21,7 @@
"appError": "We are sorry but there was an error. Please try again later." "appError": "We are sorry but there was an error. Please try again later."
}, },
"routes": {
"OverviewRoute": {
"title": "Overview",
"description": "Not much to see here, yet."
},
"LoginRoute": {
"forms": {
"email": {
"title": "Sign in",
"description": "We'll send you a verification code to your email.",
"continueAction": "Send Code",
"form": {
"email": {
"label": "Email",
"placeholder": "johndoe@example.com"
}
}
},
"confirmCode": {
"title": "You got mail!",
"description": "We sent you a code to your email. Enter it below to login",
"continueAction": "Log in",
"allowLoginFromDifferentDevices": "Allow login from different devices",
"expiringSoon": "Your code will expire in less than a minute.",
"form": {
"code": {
"label": "Verification Code",
"errors": {
"invalidChars": "Invalid verification code"
}
}
}
},
"confirmFromDifferentDevice": {
"title": "Login failed",
"description": "You could not be logged in. This could either be because you are not allowed to login from different devices or the verification code is invalid or expired."
},
"otp": {
"title": "Two-factor authentication",
"description": "Please enter the code from your authenticator app.",
"submit": "Log in",
"lost": "I lost my codes",
"code": {
"label": "Code"
},
"unavailable": "Your OTP verification time expired or you exceeded the maximum number of attempts. Please log in again."
}
}
},
"SignupRoute": {
"forms": {
"email": {
"title": "Sign up",
"description": "We only need your email and you are ready to go!",
"continueAction": "Continue",
"form": {
"email": {
"label": "Email",
"placeholder": "johndoe@example.com"
}
}
},
"mailVerification": {
"title": "You got mail!",
"description": "We sent you an email with a link to confirm your email address. Please check your inbox and click on the link to continue.",
"editEmail": {
"title": "Edit email address?",
"description": "Would you like to return to the previous step and edit your email address?",
"continueAction": "Yes, edit email"
}
}
}
},
"VerifyEmailRoute": {
"title": "Verify your email",
"isLoading": "Verifying your email...",
"isCodeInvalid": "Sorry, but this verification code is invalid.",
"errors": {
"code": {
"invalid": "The verification code is invalid."
}
}
},
"Recover2FARoute": {
"title": "Recover Two-Factor-Authentication",
"description": "We are very sorry if you lost your codes. Please enter a recovery code to continue. Note that this will disable two-factor authentication for your account. You can enable it again in the settings.",
"forms": {
"recoveryCode": {
"label": "Recovery Code"
},
"submit": "Disable 2FA"
},
"unauthorized": "Please make sure to log in first and then reset your two-factor authentication on its screen.",
"canLoginNow": "Two-factor authentication has been disabled. You can now log in.",
"loggedIn": "Two-factor authentication has been disabled. You are now logged in."
},
"CompleteAccountRoute": {
"forms": {
"generateReports": {
"title": "Generate Email Reports?",
"description": "Would you like to create fully encrypted email reports for your mails? Only you will be able to access them. Not even we can decrypt them.",
"continueAction": "Yes",
"cancelAction": "No"
},
"password": {
"title": "Set up your password",
"description": "Please enter a safe password so that we can encrypt your data.",
"continueAction": "Continue",
"form": {
"password": {
"label": "Password",
"placeholder": "********"
},
"passwordConfirm": {
"label": "Confirm Password",
"placeholder": "Re-enter your password",
"mustMatchHelperText": "Passwords do not match."
}
}
},
"available": {
"title": "Encryption already enabled",
"description": "You already have encryption enabled. Changing passwords is currently not supported."
}
}
},
"AliasesRoute": {
"title": "Aliases",
"isInCopyMode": "You are in copy mode. Click on an alias to copy it to your clipboard.",
"emptyState": {
"title": "Welcome to your Aliases!",
"description": "Create your first Alias to get started."
},
"pageActions": {
"search": {
"label": "Search",
"placeholder": "Search for names"
},
"searchFilter": {
"active": "Active",
"inactive": "Inactive"
},
"typeFilter": {
"custom": "Custom made",
"random": "Randomly generated"
}
},
"actions": {
"createRandomAlias": {
"label": "Create Random Alias"
},
"createCustomAlias": {
"label": "Create Custom Alias",
"description": "You can define your own custom alias. Note that a random suffix will be added at the end to avoid duplicates.",
"continueAction": "Create Alias",
"form": {
"address": {
"label": "Address",
"placeholder": "awesome-fish"
}
}
}
}
},
"AliasDetailRoute": {
"title": "Alias Details",
"sections": {
"settings": {
"title": "Settings",
"description": "These settings apply to this alias only. You can either set a value manually or refer to your default settings. Note that this does change in behavior. When you set a value to refer to your default setting, the alias will always use the latest value. So when you change your default setting, the alias will automatically use the new value."
},
"notes": {
"title": "Notes",
"form": {
"createdAt": {
"label": "Created at",
"empty": "Unavailable"
},
"creationContext": {
"label": "Creation Context",
"web": {
"label": "Created on this instance"
},
"extension": {
"label": "Created in the extension"
},
"extension-inline": {
"label": "Created using the extension"
}
},
"createdOn": {
"label": "Created on"
},
"personalNotes": {
"label": "Personal Notes",
"empty": "-",
"helperText": "You can enter personal notes for this alias here. Notes are encrypted."
},
"websites": {
"label": "Websites",
"emptyText": "You haven't used this alias on any site yet.",
"placeholder": "https://example.com",
"helperText": "Add a website to this alias. Used to autofill."
}
}
}
},
"actions": {
"delete": {
"label": "Delete Alias",
"description": "Are you sure you want to delete this alias?",
"continueAction": "Delete Alias"
}
}
},
"ReportsRoute": {
"title": "Reports",
"emptyState": {
"title": "Welcome to your Reports!",
"description": "Here you will find your email reports. Currently, you don't have any reports. Wait until you receive an email."
},
"pageActions": {
"sort": {
"List": "List reports by their date",
"GroupByAlias": "Group reports by their alias"
}
}
},
"ReportDetailRoute": {
"title": "Report Details",
"actions": {
"delete": {
"label": "Delete Report",
"description": "Are you sure you want to delete this report?",
"continueAction": "Delete Report"
}
},
"sections": {
"information": {
"title": "Email Information",
"form": {
"from": {
"label": "From"
},
"to": {
"label": "To"
},
"subject": {
"label": "Subject"
}
}
},
"trackers": {
"title": "Trackers",
"results": {
"imageTrackers": {
"text_zero": "No image trackers found",
"text_one": "Removed 1 image tracker",
"text_other": "Removed {{count}} image trackers"
},
"proxiedImages": {
"text_zero": "No images found",
"text_one": "Forwarding 1 image",
"text_other": "Forwarding {{count}} images",
"status": {
"isStored": "Stored on Server",
"isProxying": "Being forwarded"
}
},
"expandedUrls": {
"text_zero": "No shortened URLs found",
"text_one": "Expanded 1 URL",
"text_other": "Expanded {{count}} URLs"
}
}
}
}
},
"SettingsRoute": {
"title": "Settings",
"forms": {
"aliasPreferences": {
"title": "Alias Preferences",
"description": "Select default values for your aliases. This only affects aliases you haven't set a custom value for.",
"saveAction": "Save preferences"
}
},
"actions": {
"enable2fa": "Two-Factor-Authentication",
"aliasPreferences": "Alias Preferences"
},
"2fa": {
"title": "Two-Factor-Authentication",
"alreadyEnabled": "You have successfully enabled 2FA!",
"setup": {
"description": "Enable 2FA to add an extra layer of security to your account. Each time you log in, you will need to enter a code generated from your authenticator app. This makes it harder for an attacker to hack into your account as they would need to have access to your phone.",
"setupLabel": "Enable 2FA",
"code": {
"label": "Code",
"description": "Enter the code generated by your authenticator app.",
"onlyDigits": "The code can only contain digits."
},
"submit": "Enable 2FA",
"expired": "The verification time for your current Two-Factor-Authentication code has expired. A new code has been generated.",
"recoveryCodes": {
"title": "Note down your recovery codes",
"description": "These codes are used to recover your account if you lose access to your authenticator app. Note them down and store them in a safe place. You will not be able to view them again. Do not store them in your password manager. IF YOU LOSE YOUR RECOVERY CODES, YOU WILL LOSE ACCESS TO YOUR ACCOUNT. WE WILL NOT BE ABLE TO HELP YOU.",
"submit": "I have noted down my recovery codes"
},
"success": "You have successfully enabled 2FA!"
},
"delete": {
"showAction": "Disable 2FA",
"askType": {
"code": "I have my 2FA code",
"recoveryCode": "I have a recovery code"
},
"askCode": {
"label": "Code"
},
"askRecoveryCode": {
"label": "Recovery Code"
},
"submit": "Disable 2FA",
"success": "You have successfully disabled 2FA!"
}
}
},
"LogoutRoute": {
"title": "Log out",
"description": "We are logging you out..."
},
"ReservedAliasesRoute": {
"title": "Reserved Aliases",
"pageActions": {
"search": {
"label": "Search",
"placeholder": "Search for aliases"
}
},
"actions": {
"create": {
"label": "Create new Reserved Alias"
}
},
"userAmount_one": "Forwards to one user",
"userAmount_other": "Forwards to {{count}} users",
"emptyState": {
"title": "Create your first reserved alias",
"description": "Reserved aliases are aliases that will be forwarded to selected admin users. This is useful if you want to create aliases that are meant to be public, like contact@example.com or hello@example.com."
}
},
"ReservedAliasDetailRoute": {
"title": "Reserved Alias Details",
"sections": {
"users": {
"title": "Users",
"fields": {
"users": {
"label": "Users",
"me": "{{email}} (Me)"
}
}
}
}
},
"AdminRoute": {
"title": "Site configuration",
"routes": {
"reservedAliases": "Reserved Aliases",
"settings": "Global Settings"
},
"forms": {
"reservedAliases": {
"title": "Reserved Aliases",
"description": "Define what alias should forward to whom.",
"saveAction": "Create Alias",
"fields": {
"local": {
"label": "Local"
},
"users": {
"label": "Users",
"me": "{{email}} (Me)"
}
},
"explanation": {
"step1": "User from outside",
"step2": "Sends mail to",
"step4": "KleckRelay forwards to"
}
},
"settings": {
"randomEmailIdMinLength": {
"label": "Minimum random alias ID length",
"description": "The minimum length for randomly generated emails. The server will automatically increase the length if required so."
},
"randomEmailIdChars": {
"label": "Random alias character pool",
"description": "Characters that are used to generate random emails."
},
"randomEmailLengthIncreaseOnPercentage": {
"label": "Percentage of used aliases",
"description": "If the percentage of used random email IDs is higher than this value, the length of the random email ID will be increased. This is used to prevent spammers from guessing the email ID."
},
"customEmailSuffixLength": {
"label": "Custom email suffix length",
"description": "The length of the custom email suffix."
},
"customEmailSuffixChars": {
"label": "Custom email suffix character pool",
"description": "Characters that are used to generate custom email suffixes."
},
"imageProxyStorageLifeTimeInHours": {
"label": "Image proxy storage lifetime",
"description": "The lifetime of images that are stored on the server in hours. After this time, the image will be deleted.",
"unit_one": "hour",
"unit_other": "hours"
},
"enableImageProxy": {
"label": "Enable image proxy",
"description": "If enabled, images will be proxied through the server. This enhances your user's privacy as their ip address will not be leaked."
},
"enableImageProxyStorage": {
"label": "Enable image proxy storage",
"description": "If enabled, images will be stored on the server and forwarded to the user. This makes email tracking nearly impossible as every message will be marked as read instantly, which is obviously not true as a user is typically not able to view an email in just a few seconds after it has been sent. This will only affect new images."
},
"userEmailEnableDisposableEmails": {
"label": "Enable disposable emails for new accounts",
"description": "If enabled, users will be able to use disposable emails when creating a new account. This will only affect new accounts."
},
"userEmailEnableOtherRelays": {
"label": "Enable other relays for new accounts",
"description": "If enabled, users will be able to use other relays (such as SimpleLogin or DuckDuckGo's Email Tracking Protection) when creating a new account. This will only affect new accounts."
},
"allowStatistics": {
"label": "Allow statistics",
"description": "If enabled, your instance will collect anonymous statistics and share them. They will only be stored locally on this instance but made public."
},
"allowAliasDeletion": {
"label": "Allow alias deletion",
"description": "If enabled, users will be able to delete their aliases."
},
"maxAliasesPerUser": {
"label": "Maximum aliases per user",
"description": "The maximum number of aliases a user can create. 0 means unlimited. Existing aliases will not be affected."
}
}
},
"settings": {
"title": "Global Settings",
"description": "Configure global settings for your instance.",
"successMessage": "Settings have been saved successfully!",
"randomAliasesPreview": {
"title": "Random aliases will look like this",
"helperText": "This is just a preview. Those are not real aliases."
},
"randomAliasesIncreaseExplanation": "Random aliases' length will be increased from {{originalLength}} to {{increasedLength}} characters after {{amount}} aliases have been created.",
"resetLabel": "Reset to defaults",
"disabled": {
"title": "Global settings are disabled",
"description": "Global settings have been disabled. You can enable them in the configuration file."
}
},
"reservedAlias": {
"actions": {
"delete": {
"label": "Delete Reserved Alias",
"description": "Are you sure you want to delete this reserved alias?",
"continueAction": "Delete Reserved Alias"
}
}
},
"serverStatus": {
"noRecentReports": "There seems to be some issues with your server. The server hasn't done its cleanup in the last few days. The last report was on {{date}}.",
"error": "There was an error during the last server cleanup job from {{relativeDescription}}. Please check the logs for more information.",
"success": "Everything okay with your server! The last cleanup job was {{relativeDescription}}."
}
}
},
"components": { "components": {
"NavigationButton": {
"overview": "Overview",
"aliases": "Aliases",
"reports": "Reports",
"settings": "Settings",
"admin": "Admin"
},
"AuthenticateRoute": {
"signup": "Sign up",
"login": "Log in"
},
"AuthenticatedRoute": {
"logout": "Logout"
},
"EnterDecryptionPassword": {
"title": "Decrypt Reports",
"description": "Please enter your password so that your reports can de decrypted.",
"cancelAction": "Decrypt later",
"continueAction": "Continue",
"form": {
"password": {
"label": "Password",
"placeholder": "********",
"errors": {
"invalidPassword": "Password is invalid"
}
}
}
},
"ResendMailButton": {
"label": "Resend Mail"
},
"OpenMailButton": {
"label": "Open Mail"
},
"DecryptionPasswordMissingAlert": {
"unavailable": {
"title": "Encryption required",
"description": "You need to set up encryption to use this feature.",
"continueAction": "Set up encryption"
},
"passwordRequired": {
"title": "Password required",
"description": "Your decryption password is required to view this section.",
"continueAction": "Enter password"
}
},
"TimedButton": {
"remainingTime_one": "({{count}})",
"remainingTime_other": "({{count}})"
},
"ErrorLoadingDataMessage": {
"tryAgain": "Try Again"
},
"AliasTypeIndicator": {
"random": "This is a randomly generated alias",
"custom": "This is a custom-made alias"
},
"NoSearchResults": {
"title": "Nothing found",
"description": "We couldn't find anything for your search query. Try again with a different query."
},
"LockNavigationContextProvider": {
"title": "Are you sure you want to leave?",
"description": "You have unsaved changes. If you leave, your changes will be lost.",
"continueLabel": "Leave"
},
"passwordShareConfirmationDialog": { "passwordShareConfirmationDialog": {
"title": "Share Password?", "title": "Share Password?",
"description": "An extension is asking for your password. Do you want to share it? Only continue if you initiated this action.", "description": "An extension is asking for your password. Do you want to share it? Only continue if you initiated this action.",
@ -578,85 +30,6 @@
"doNotShare": "Do not share", "doNotShare": "Do not share",
"decideLater": "Decide later", "decideLater": "Decide later",
"doNotAskAgain": "Do not ask again" "doNotAskAgain": "Do not ask again"
},
"StringPoolField": {
"addCustom": {
"label": "Add custom"
},
"forms": {
"addNew": {
"title": "Add new value",
"description": "Enter your characters you would like to include",
"label": "Characters",
"submit": "Add"
}
}
}
},
"relations": {
"alias": {
"mutations": {
"success": {
"aliasCreation": "Created Alias successfully!",
"aliasUpdated": "Updated Alias successfully!",
"notesUpdated": "Updated & encrypted notes successfully!",
"aliasChangedToEnabled": "Alias has been enabled",
"aliasChangedToDisabled": "Alias has been disabled",
"addressCopiedToClipboard": "Address has been copied to your clipboard!",
"aliasDeleted": "Alias has been deleted!"
}
},
"settings": {
"removeTrackers": {
"label": "Remove Trackers",
"helperText": "Remove single-pixel image trackers as well as url trackers."
},
"createMailReports": {
"label": "Create Mail Reports",
"helperText": "Create reports of emails sent to aliases. Reports are end-to-end encrypted. Only you can access them."
},
"proxyImages": {
"label": "Proxy Images",
"helperText": "Proxies images in your emails through this KleckRelay instance. This adds an extra layer of privacy. Images are loaded immediately after we receive the email. They then will be stored for some time (cache time). During that time, the image will be served from us. This means the sender has no idea you have opened the mail. After the cache time, the image is loaded from the sender, but it will be forwarded by us. This means the sender will not be able to access your IP address nor your browser data."
},
"imageProxyFormat": {
"label": "Image File Type",
"enumTexts": {
"jpeg": "JPEG",
"png": "PNG",
"webp": "WEBP"
}
},
"proxyUserAgent": {
"label": "Proxy User Agent",
"helperText": "An User Agent is a identifier each browser and email client sends when retrieving files, such as images. You can specify here what user agent you would like to be used when we forward it. User Agents are kept up-to-date.",
"enumTexts": {
"apple-mail": "Apple Mail",
"google-mail": "Google Mail",
"outlook-windows": "Outlook / Windows",
"outlook-macos": "Outlook / MacOS",
"firefox": "Firefox Browser",
"chrome": "Chrome Browser"
}
},
"expandUrlShorteners": {
"label": "Expand URL Shorteners",
"helperText": "Expand shortened URLs (for example bit.ly) to their original URL. This way those services can't track you."
},
"saveAction": "Save Settings"
}
},
"report": {
"mutations": {
"success": {
"reportDeleted": "Report has been deleted!"
}
},
"emailMeta": {
"flow": "{{from}} -> {{to}}",
"emptySubject": "<No Subject>"
}
} }
} }
} }

View File

@ -0,0 +1,7 @@
{
"title": "Verify your email",
"isLoading": "We are verifying your email address...",
"errors": {
"invalid": "The verification link is invalid or has expired."
}
}

View File

@ -25,32 +25,28 @@ export default function PasswordShareConfirmationDialog({
onShare, onShare,
onClose, onClose,
}: PasswordShareConfirmationDialogProps): ReactElement { }: PasswordShareConfirmationDialogProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("extension")
return ( return (
<Dialog open={open} onClose={() => onClose(false)} maxWidth="sm" fullWidth={false}> <Dialog open={open} onClose={() => onClose(false)} maxWidth="sm" fullWidth={false}>
<DialogTitle>{t("components.passwordShareConfirmationDialog.title")}</DialogTitle> <DialogTitle>{t("sharePassword.title")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>{t("sharePassword.description")}</DialogContentText>
{t("components.passwordShareConfirmationDialog.description")}
</DialogContentText>
<Box my={2}> <Box my={2}>
<Alert severity="warning"> <Alert severity="warning">{t("sharePassword.warning")}</Alert>
{t("components.passwordShareConfirmationDialog.warning")}
</Alert>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Box mr="auto"> <Box mr="auto">
<Button startIcon={<MdAccessTimeFilled />} onClick={() => onClose(false)}> <Button startIcon={<MdAccessTimeFilled />} onClick={() => onClose(false)}>
{t("components.passwordShareConfirmationDialog.decideLater")} {t("sharePassword.decideLater")}
</Button> </Button>
</Box> </Box>
<Button startIcon={<TiCancel />} onClick={() => onClose(true)}> <Button startIcon={<TiCancel />} onClick={() => onClose(true)}>
{t("components.passwordShareConfirmationDialog.doNotShare")} {t("sharePassword.doNotAskAgain")}
</Button> </Button>
<Button color="error" onClick={onShare} startIcon={<MdShield />}> <Button color="error" onClick={onShare} startIcon={<MdShield />}>
{t("components.passwordShareConfirmationDialog.continueAction")} {t("sharePassword.sharePassword")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -22,7 +22,7 @@ export interface LockNavigationContextProviderProps {
export default function LockNavigationContextProvider({ export default function LockNavigationContextProvider({
children, children,
}: LockNavigationContextProviderProps): JSX.Element { }: LockNavigationContextProviderProps): JSX.Element {
const {t} = useTranslation() const {t} = useTranslation(["components", "common"])
const navigate = useNavigate() const navigate = useNavigate()
const [isLocked, setIsLocked] = useState<boolean>(false) const [isLocked, setIsLocked] = useState<boolean>(false)
@ -97,18 +97,18 @@ export default function LockNavigationContextProvider({
{children} {children}
</LockNavigationContext.Provider> </LockNavigationContext.Provider>
<Dialog open={showDialog} onClose={cancel}> <Dialog open={showDialog} onClose={cancel}>
<DialogTitle>{t("components.LockNavigationContextProvider.title")}</DialogTitle> <DialogTitle>{t("LockNavigationContextProvider.title")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
{t("components.LockNavigationContextProvider.description")} {t("LockNavigationContextProvider.description")}
</DialogContentText> </DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button startIcon={<TiCancel />} onClick={cancel}> <Button startIcon={<TiCancel />} onClick={cancel}>
{t("general.cancelLabel")} {t("general.cancelLabel", {ns: "common"})}
</Button> </Button>
<Button startIcon={<MdLogout />} onClick={leave}> <Button startIcon={<MdLogout />} onClick={leave}>
{t("components.LockNavigationContextProvider.continueLabel")} {t("LockNavigationContextProvider.continueLabel")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -16,13 +16,10 @@ export const ALIAS_TYPE_ICON_MAP: Record<AliasType, ReactElement> = {
[AliasType.CUSTOM]: <FaHashtag />, [AliasType.CUSTOM]: <FaHashtag />,
} }
const ALIAS_TYPE_TOOLTIP_MAP = createEnumMapFromTranslation( const ALIAS_TYPE_TOOLTIP_MAP = createEnumMapFromTranslation("aliasTypeExplanation", AliasType)
"components.AliasTypeIndicator",
AliasType,
)
export default function AliasTypeIndicator({type}: AliasTypeIndicatorProps): ReactElement { export default function AliasTypeIndicator({type}: AliasTypeIndicatorProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("aliases")
return ( return (
// @ts-ignore // @ts-ignore

View File

@ -15,7 +15,7 @@ export interface WithEncryptionRequiredProps {
export default function DecryptionPasswordMissingAlert({ export default function DecryptionPasswordMissingAlert({
children = <></>, children = <></>,
}: WithEncryptionRequiredProps): JSX.Element { }: WithEncryptionRequiredProps): JSX.Element {
const {t} = useTranslation() const {t} = useTranslation("decryption")
const {handleAnchorClick} = useContext(LockNavigationContext) const {handleAnchorClick} = useContext(LockNavigationContext)
const {encryptionStatus} = useContext(AuthContext) const {encryptionStatus} = useContext(AuthContext)
const theme = useTheme() const theme = useTheme()
@ -33,12 +33,12 @@ export default function DecryptionPasswordMissingAlert({
> >
<Grid item> <Grid item>
<Typography variant="h6" component="h2"> <Typography variant="h6" component="h2">
{t("components.DecryptionPasswordMissingAlert.unavailable.title")} {t("actions.passwordMissing.unavailable.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<Typography> <Typography>
{t("components.DecryptionPasswordMissingAlert.unavailable.description")} {t("actions.passwordMissing.unavailable.description")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
@ -49,9 +49,7 @@ export default function DecryptionPasswordMissingAlert({
startIcon={<MdLock />} startIcon={<MdLock />}
onClick={handleAnchorClick} onClick={handleAnchorClick}
> >
{t( {t("actions.passwordMissing.unavailable.continueActionLabel")}
"components.DecryptionPasswordMissingAlert.unavailable.continueAction",
)}
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>
@ -70,14 +68,12 @@ export default function DecryptionPasswordMissingAlert({
> >
<Grid item> <Grid item>
<Typography variant="h6" component="h2"> <Typography variant="h6" component="h2">
{t("components.DecryptionPasswordMissingAlert.passwordRequired.title")} {t("actions.passwordMissing.passwordRequired.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<Typography> <Typography>
{t( {t("actions.passwordMissing.passwordRequired.description")}
"components.DecryptionPasswordMissingAlert.passwordRequired.description",
)}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
@ -87,9 +83,7 @@ export default function DecryptionPasswordMissingAlert({
startIcon={<MdLock />} startIcon={<MdLock />}
onClick={handleAnchorClick} onClick={handleAnchorClick}
> >
{t( {t("actions.passwordMissing.passwordRequired.continueActionLabel")}
"components.DecryptionPasswordMissingAlert.passwordRequired.continueAction",
)}
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -12,7 +12,7 @@ export default function ErrorLoadingDataMessage({
message, message,
onRetry, onRetry,
}: ErrorLoadingDataMessageProps): ReactElement { }: ErrorLoadingDataMessageProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("components")
return ( return (
<Grid container spacing={2} flexDirection="column" alignItems="center"> <Grid container spacing={2} flexDirection="column" alignItems="center">
@ -20,9 +20,7 @@ export default function ErrorLoadingDataMessage({
<Alert severity="error">{message}</Alert> <Alert severity="error">{message}</Alert>
</Grid> </Grid>
<Grid item> <Grid item>
<Button onClick={onRetry}> <Button onClick={onRetry}>{t("ErrorLoadingDataMessage.tryAgain")}</Button>
{t("components.ErrorLoadingDataMessage.tryAgain")}
</Button>
</Grid> </Grid>
</Grid> </Grid>
) )

View File

@ -5,20 +5,18 @@ import {FaQuestion} from "react-icons/fa"
import {Grid, Typography} from "@mui/material" import {Grid, Typography} from "@mui/material"
export default function NoSearchResults(): ReactElement { export default function NoSearchResults(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("common")
return ( return (
<Grid container spacing={4} direction="column" alignItems="center"> <Grid container spacing={4} direction="column" alignItems="center">
<Grid item> <Grid item>
<Typography variant="h6">{t("components.NoSearchResults.title")}</Typography> <Typography variant="h6">{t("noSearchResults.title")}</Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<FaQuestion size={40} /> <FaQuestion size={40} />
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body1"> <Typography variant="body1">{t("noSearchResults.description")}</Typography>
{t("components.NoSearchResults.description")}
</Typography>
</Grid> </Grid>
</Grid> </Grid>
) )

View File

@ -12,14 +12,14 @@ export interface OpenMailButtonProps {
} }
export default function OpenMailButton({domain}: OpenMailButtonProps): ReactElement { export default function OpenMailButton({domain}: OpenMailButtonProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("components")
const userAgent = new UAParser() const userAgent = new UAParser()
if (userAgent.getOS().name === "Android" && APP_LINK_MAP[domain]) { if (userAgent.getOS().name === "Android" && APP_LINK_MAP[domain]) {
return ( return (
<Button startIcon={<IoMdMailOpen />} variant="text" href={APP_LINK_MAP[domain].android}> <Button startIcon={<IoMdMailOpen />} variant="text" href={APP_LINK_MAP[domain].android}>
{t("components.OpenMailButton.label")} {t("OpenMailButton.label")}
</Button> </Button>
) )
} }

View File

@ -5,12 +5,13 @@ import React, {ReactElement, useEffect, useState} from "react"
import {Alert, Button, Grid, Snackbar, Typography, TypographyProps} from "@mui/material" import {Alert, Button, Grid, Snackbar, Typography, TypographyProps} from "@mui/material"
import {LoadingButton} from "@mui/lab" import {LoadingButton} from "@mui/lab"
import {OverrideProps} from "@mui/types" import {OverrideProps} from "@mui/types"
import {useTranslation} from "react-i18next"
export interface SimpleFormProps { export interface SimpleFormProps {
title: string title: string
description: string description: string
continueActionLabel: string
continueActionLabel?: string
children?: ReactElement[] children?: ReactElement[]
cancelActionLabel?: string cancelActionLabel?: string
isSubmitting?: boolean isSubmitting?: boolean
@ -32,6 +33,8 @@ export default function SimpleForm({
titleComponent = "h1", titleComponent = "h1",
isSubmitting = false, isSubmitting = false,
}: SimpleFormProps): ReactElement { }: SimpleFormProps): ReactElement {
const {t} = useTranslation("common")
const [showSnackbar, setShowSnackbar] = useState<boolean>(false) const [showSnackbar, setShowSnackbar] = useState<boolean>(false)
useEffect(() => { useEffect(() => {
@ -101,7 +104,7 @@ export default function SimpleForm({
type="submit" type="submit"
startIcon={<MdChevronRight />} startIcon={<MdChevronRight />}
> >
{continueActionLabel} {continueActionLabel || t("general.continueLabel")}
</LoadingButton> </LoadingButton>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -28,22 +28,22 @@ export default function AddNewDialog({
open = false, open = false,
onClose, onClose,
}: StringPoolFieldProps): ReactElement { }: StringPoolFieldProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["components", "common"])
const [value, setValue] = useState<string>("") const [value, setValue] = useState<string>("")
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose}>
<DialogTitle>{t("components.StringPoolField.forms.addNew.title")}</DialogTitle> <DialogTitle>{t("StringPoolField.forms.addNew.title")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
{t("components.StringPoolField.forms.addNew.description")} {t("StringPoolField.forms.addNew.description")}
</DialogContentText> </DialogContentText>
<Box my={2}> <Box my={2}>
<TextField <TextField
value={value} value={value}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
label={t("components.StringPoolField.forms.addNew.label")} label={t("StringPoolField.forms.addNew.label")}
name="addNew" name="addNew"
fullWidth fullWidth
autoFocus autoFocus
@ -53,14 +53,14 @@ export default function AddNewDialog({
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} startIcon={<TiCancel />} variant="text"> <Button onClick={onClose} startIcon={<TiCancel />} variant="text">
{t("general.cancelLabel")} {t("general.cancelLabel", {ns: "common"})}
</Button> </Button>
<Button <Button
onClick={() => onCreated(value)} onClick={() => onCreated(value)}
variant="contained" variant="contained"
startIcon={<MdCheck />} startIcon={<MdCheck />}
> >
{t("components.StringPoolField.forms.addNew.submit")} {t("StringPoolField.forms.addNew.submit")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -51,7 +51,7 @@ export default function StringPoolField({
fullWidth, fullWidth,
...props ...props
}: StringPoolFieldProps): ReactElement { }: StringPoolFieldProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("components")
const reversedPoolsMap = useMemo( const reversedPoolsMap = useMemo(
() => Object.fromEntries(Object.entries(pools).map(([key, value]) => [value, key])), () => Object.fromEntries(Object.entries(pools).map(([key, value]) => [value, key])),
@ -155,9 +155,7 @@ export default function StringPoolField({
<ListItemIcon> <ListItemIcon>
<MdAdd /> <MdAdd />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText primary={t("StringPoolField.addCustom.label")} />
primary={t("components.StringPoolField.addCustom.label")}
/>
</MenuItem> </MenuItem>
)} )}
</Select> </Select>

View File

@ -18,7 +18,7 @@ export default function TimedButton({
disabled: parentDisabled = false, disabled: parentDisabled = false,
...props ...props
}: TimedButtonProps): ReactElement { }: TimedButtonProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("components")
const [startDate, resetInterval] = useIntervalUpdate(1000) const [startDate, resetInterval] = useIntervalUpdate(1000)
@ -35,9 +35,7 @@ export default function TimedButton({
}} }}
> >
<span>{children} </span> <span>{children} </span>
{secondsLeft > 0 && ( {secondsLeft > 0 && <span>{t("TimedButton.remainingTime", {count: secondsLeft})}</span>}
<span>{t("components.TimedButton.remainingTime", {count: secondsLeft})}</span>
)}
</LoadingButton> </LoadingButton>
) )
} }

View File

@ -2,10 +2,10 @@ import {ImageProxyFormatType, ProxyUserAgentType} from "~/server-types"
import {createEnumMapFromTranslation} from "~/utils" import {createEnumMapFromTranslation} from "~/utils"
export const IMAGE_PROXY_FORMAT_TYPE_NAME_MAP = createEnumMapFromTranslation( export const IMAGE_PROXY_FORMAT_TYPE_NAME_MAP = createEnumMapFromTranslation(
"relations.alias.settings.imageProxyFormat.enumTexts", "settings.fields.imageProxyFormat.values",
ImageProxyFormatType, ImageProxyFormatType,
) )
export const PROXY_USER_AGENT_TYPE_NAME_MAP = createEnumMapFromTranslation( export const PROXY_USER_AGENT_TYPE_NAME_MAP = createEnumMapFromTranslation(
"relations.alias.settings.proxyUserAgent.enumTexts", "settings.fields.proxyUserAgent.values",
ProxyUserAgentType, ProxyUserAgentType,
) )

View File

@ -15,6 +15,7 @@ i18n.use(HttpApi)
.init({ .init({
debug: isDev, debug: isDev,
fallbackLng: "en-US", fallbackLng: "en-US",
load: "all",
backend: { backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", loadPath: "/locales/{{lng}}/{{ns}}.json",
}, },

View File

@ -18,8 +18,8 @@ import decryptCronReportData from "~/apis/helpers/decrypt-cron-report-data"
const MAX_REPORT_DAY_THRESHOLD = 5 const MAX_REPORT_DAY_THRESHOLD = 5
function ServerStatus(): ReactElement | null { function ServerStatus(): ReactElement | null {
const {t} = useTranslation("admin")
const serverSettings = useLoaderData() as ServerSettings const serverSettings = useLoaderData() as ServerSettings
const {t} = useTranslation()
const {_decryptUsingPrivateKey} = useContext(AuthContext) const {_decryptUsingPrivateKey} = useContext(AuthContext)
const query = useQuery<CronReport | null, AxiosError>(["get_latest_cron_report"], async () => { const query = useQuery<CronReport | null, AxiosError>(["get_latest_cron_report"], async () => {
@ -50,7 +50,7 @@ function ServerStatus(): ReactElement | null {
if (report.createdAt < thresholdDate) { if (report.createdAt < thresholdDate) {
return ( return (
<Alert severity="warning"> <Alert severity="warning">
{t("routes.AdminRoute.serverStatus.noRecentReports", { {t("serverStatus.noRecentReports", {
date: format(new Date(report.createdAt), "Pp"), date: format(new Date(report.createdAt), "Pp"),
})} })}
</Alert> </Alert>
@ -60,7 +60,7 @@ function ServerStatus(): ReactElement | null {
if (report.reportData.report.status === "error") { if (report.reportData.report.status === "error") {
return ( return (
<Alert severity="error"> <Alert severity="error">
{t("routes.AdminRoute.serverStatus.error", { {t("serverStatus.error", {
relativeDescription: formatRelative( relativeDescription: formatRelative(
new Date(report.createdAt), new Date(report.createdAt),
new Date(), new Date(),
@ -72,7 +72,7 @@ function ServerStatus(): ReactElement | null {
return ( return (
<Alert severity="success"> <Alert severity="success">
{t("routes.AdminRoute.serverStatus.success", { {t("serverStatus.success", {
relativeDescription: formatRelative( relativeDescription: formatRelative(
new Date(report.createdAt), new Date(report.createdAt),
new Date(), new Date(),

View File

@ -18,14 +18,14 @@ interface WebsiteForm {
url: string url: string
} }
const WEBSITE_SCHEMA = yup.object().shape({
url: yup.string().matches(URL_REGEX, "This URL is invalid."),
})
export default function AddWebsiteField({onAdd, isLoading}: AddWebsiteFieldProps): ReactElement { export default function AddWebsiteField({onAdd, isLoading}: AddWebsiteFieldProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("alias-notes")
const schema = yup.object().shape({
url: yup.string().matches(URL_REGEX, t("form.websites.error.invalid") as string),
})
const websiteFormik = useFormik<WebsiteForm>({ const websiteFormik = useFormik<WebsiteForm>({
validationSchema: WEBSITE_SCHEMA, validationSchema: schema,
initialValues: { initialValues: {
url: "", url: "",
}, },
@ -58,10 +58,8 @@ export default function AddWebsiteField({onAdd, isLoading}: AddWebsiteFieldProps
<TextField <TextField
name="url" name="url"
id="url" id="url"
label={t("routes.AliasDetailRoute.sections.notes.form.websites.label")} label={t("form.websites.label")}
placeholder={t( placeholder={t("form.websites.placeholder")}
"routes.AliasDetailRoute.sections.notes.form.websites.placeholder",
)}
variant="outlined" variant="outlined"
value={websiteFormik.values.url} value={websiteFormik.values.url}
onChange={websiteFormik.handleChange} onChange={websiteFormik.handleChange}
@ -93,7 +91,7 @@ export default function AddWebsiteField({onAdd, isLoading}: AddWebsiteFieldProps
error={websiteFormik.touched.url && Boolean(websiteFormik.errors.url)} error={websiteFormik.touched.url && Boolean(websiteFormik.errors.url)}
> >
{(websiteFormik.touched.url && websiteFormik.errors.url) || {(websiteFormik.touched.url && websiteFormik.errors.url) ||
t("routes.AliasDetailRoute.sections.notes.form.websites.helperText")} t("form.websites.helperText")}
</FormHelperText> </FormHelperText>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -12,7 +12,7 @@ export interface AliasAddressProps {
} }
export default function AliasAddress({address}: AliasAddressProps): ReactElement { export default function AliasAddress({address}: AliasAddressProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("common")
const [{value, error}, copyToClipboard] = useCopyToClipboard() const [{value, error}, copyToClipboard] = useCopyToClipboard()
return ( return (
@ -26,11 +26,8 @@ export default function AliasAddress({address}: AliasAddressProps): ReactElement
> >
{address} {address}
</Button> </Button>
<SuccessSnack <SuccessSnack key={value} message={value && t("messages.alias.addressCopied")} />
key={value} <ErrorSnack message={error && t("messages.errors.copyFailed")} />
message={value && t("relations.alias.mutations.success.addressCopiedToClipboard")}
/>
<ErrorSnack message={error && t("general.copyError")} />
</> </>
) )
} }

View File

@ -64,21 +64,19 @@ const CREATION_CONTEXT_ICON_MAP: Record<AliasNote["data"]["creationContext"], Re
const IMAGE_WIDTH = 20 const IMAGE_WIDTH = 20
export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProps): ReactElement { export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["alias-notes", "common"])
const {showError, showSuccess} = useErrorSuccessSnacks() const {showError, showSuccess} = useErrorSuccessSnacks()
const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} = useContext(AuthContext) const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} = useContext(AuthContext)
const schema = yup.object().shape({ const schema = yup.object().shape({
personalNotes: yup personalNotes: yup.string().label(t("personalNotes.label")),
.string()
.label(t("routes.AliasDetailRoute.sections.notes.form.personalNotes.label")),
websites: yup.array().of( websites: yup.array().of(
yup yup
.object() .object()
.shape({ .shape({
url: yup.string().url(), url: yup.string().url(),
}) })
.label(t("routes.AliasDetailRoute.sections.notes.form.websites.label")), .label(t("form.websites.label")),
), ),
}) })
@ -122,7 +120,7 @@ export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProp
_decryptUsingMasterPassword, _decryptUsingMasterPassword,
) )
showSuccess(t("relations.alias.mutations.success.notesUpdated")) showSuccess(t("messages.alias.updated", {ns: "common"}))
await queryClient.cancelQueries(queryKey) await queryClient.cancelQueries(queryKey)
@ -181,7 +179,7 @@ export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProp
<Grid container spacing={1} direction="row"> <Grid container spacing={1} direction="row">
<Grid item> <Grid item>
<Typography variant="h6" component="h3"> <Typography variant="h6" component="h3">
{t("routes.AliasDetailRoute.sections.notes.title")} {t("title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
@ -211,11 +209,9 @@ export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProp
{notes.data.createdAt && ( {notes.data.createdAt && (
<Grid item> <Grid item>
<SimpleOverlayInformation <SimpleOverlayInformation
emptyText={t("general.emptyUnavailableValue")} emptyText={t("general.unavailableValue")}
icon={<MdEditCalendar />} icon={<MdEditCalendar />}
label={t( label={t("form.createdAt.label")}
"routes.AliasDetailRoute.sections.notes.form.createdAt.label",
)}
> >
{notes.data.createdAt && ( {notes.data.createdAt && (
<Tooltip title={notes.data.createdAt.toISOString()}> <Tooltip title={notes.data.createdAt.toISOString()}>
@ -231,13 +227,11 @@ export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProp
<Grid item> <Grid item>
<SimpleOverlayInformation <SimpleOverlayInformation
icon={CREATION_CONTEXT_ICON_MAP[notes.data.creationContext]} icon={CREATION_CONTEXT_ICON_MAP[notes.data.creationContext]}
label={t( label={t("form.creationContext.label")}
"routes.AliasDetailRoute.sections.notes.form.creationContext.label",
)}
> >
<Typography variant="body1"> <Typography variant="body1">
{t( {t(
`routes.AliasDetailRoute.sections.notes.form.creationContext.${notes.data.creationContext}.label`, `form.creationContext.values.${notes.data.creationContext}`,
)} )}
</Typography> </Typography>
</SimpleOverlayInformation> </SimpleOverlayInformation>
@ -247,9 +241,7 @@ export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProp
<Grid item> <Grid item>
<SimpleOverlayInformation <SimpleOverlayInformation
icon={<RiLinkM />} icon={<RiLinkM />}
label={t( label={t("form.createdOn.label")}
"routes.AliasDetailRoute.sections.notes.form.createdOn.label",
)}
> >
<Link <Link
href={notes.data.createdOn} href={notes.data.createdOn}
@ -265,14 +257,10 @@ export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProp
</Grid> </Grid>
)} )}
<Grid item> <Grid item>
<SimpleOverlayInformation <SimpleOverlayInformation label={t("form.personalNotes.label")}>
label={t(
"routes.AliasDetailRoute.sections.notes.form.personalNotes.label",
)}
>
{isInEditMode ? ( {isInEditMode ? (
<TextField <TextField
label="Personal Notes" label={t("form.personalNotes.label")}
multiline multiline
fullWidth fullWidth
key="personalNotes" key="personalNotes"
@ -289,9 +277,7 @@ export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProp
helperText={ helperText={
(formik.touched.personalNotes && (formik.touched.personalNotes &&
formik.errors.personalNotes) || formik.errors.personalNotes) ||
t( t("form.personalNotes.helperText")
"routes.AliasDetailRoute.sections.notes.form.personalNotes.helperText",
)
} }
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
@ -308,12 +294,8 @@ export default function AliasNotesForm({id, notes, queryKey}: AliasNotesFormProp
</Grid> </Grid>
<Grid item> <Grid item>
<SimpleOverlayInformation <SimpleOverlayInformation
label={t( label={t("form.websites.label")}
"routes.AliasDetailRoute.sections.notes.form.websites.label", emptyText={t("form.websites.emptyText")}
)}
emptyText={t(
"routes.AliasDetailRoute.sections.notes.form.websites.emptyText",
)}
> >
{isInEditMode ? ( {isInEditMode ? (
<Grid item> <Grid item>

View File

@ -44,7 +44,7 @@ export default function AliasPreferencesForm({
alias, alias,
queryKey, queryKey,
}: AliasPreferencesFormProps): ReactElement { }: AliasPreferencesFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["aliases", "common"])
const {showSuccess, showError} = useErrorSuccessSnacks() const {showSuccess, showError} = useErrorSuccessSnacks()
const {_decryptUsingMasterPassword} = useContext(AuthContext) const {_decryptUsingMasterPassword} = useContext(AuthContext)
@ -52,31 +52,34 @@ export default function AliasPreferencesForm({
removeTrackers: yup removeTrackers: yup
.mixed<boolean | null>() .mixed<boolean | null>()
.oneOf([true, false, null]) .oneOf([true, false, null])
.label(t("relations.alias.settings.removeTrackers.label")), .label(t("settings.fields.removeTrackers.label")),
createMailReport: yup createMailReport: yup
.mixed<boolean | null>() .mixed<boolean | null>()
.oneOf([true, false, null]) .oneOf([true, false, null])
.label(t("relations.alias.settings.createMailReports.label")), .label(t("settings.fields.createMailReport.label")),
proxyImages: yup.mixed<boolean | null>().oneOf([true, false, null]), proxyImages: yup
.mixed<boolean | null>()
.oneOf([true, false, null])
.label(t("settings.fields.proxyImages.label")),
imageProxyFormat: yup imageProxyFormat: yup
.mixed<ImageProxyFormatType>() .mixed<ImageProxyFormatType>()
.oneOf([null, ...Object.values(ImageProxyFormatType)]) .oneOf([null, ...Object.values(ImageProxyFormatType)])
.label(t("relations.alias.settings.imageProxyFormat.label")), .label(t("settings.fields.imageProxyFormat.label")),
proxyUserAgent: yup proxyUserAgent: yup
.mixed<ProxyUserAgentType>() .mixed<ProxyUserAgentType>()
.oneOf([null, ...Object.values(ProxyUserAgentType)]) .oneOf([null, ...Object.values(ProxyUserAgentType)])
.label(t("relations.alias.settings.proxyUserAgent.label")), .label(t("settings.fields.proxyUserAgent.label")),
expandUrlShorteners: yup expandUrlShorteners: yup
.mixed<boolean | null>() .mixed<boolean | null>()
.oneOf([true, false, null]) .oneOf([true, false, null])
.label(t("relations.alias.settings.expandUrlShorteners.label")), .label(t("settings.fields.expandUrlShorteners.label")),
}) })
const {mutateAsync} = useMutation<Alias, AxiosError, UpdateAliasData>( const {mutateAsync} = useMutation<Alias, AxiosError, UpdateAliasData>(
data => updateAlias(alias.id, data), data => updateAlias(alias.id, data),
{ {
onSuccess: async newAlias => { onSuccess: async newAlias => {
showSuccess(t("relations.alias.mutations.success.aliasUpdated")) showSuccess(t("messages.alias.updated", {ns: "common"}))
;(newAlias as any as DecryptedAlias).notes = decryptAliasNotes( ;(newAlias as any as DecryptedAlias).notes = decryptAliasNotes(
newAlias.encryptedNotes, newAlias.encryptedNotes,
_decryptUsingMasterPassword, _decryptUsingMasterPassword,
@ -126,7 +129,7 @@ export default function AliasPreferencesForm({
<Grid container spacing={4}> <Grid container spacing={4}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<SelectField <SelectField
label={t("relations.alias.settings.removeTrackers.label")} label={t("settings.fields.removeTrackers.label")}
formik={formik} formik={formik}
icon={<BsShieldShaded />} icon={<BsShieldShaded />}
name="removeTrackers" name="removeTrackers"
@ -134,9 +137,7 @@ export default function AliasPreferencesForm({
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<SelectField <SelectField
label={t( label={t("settings.fields.createMailReport.label")}
"relations.alias.settings.createMailReports.label",
)}
formik={formik} formik={formik}
icon={<MdTextSnippet />} icon={<MdTextSnippet />}
name="createMailReport" name="createMailReport"
@ -146,9 +147,7 @@ export default function AliasPreferencesForm({
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}> <Grid item xs={12}>
<SelectField <SelectField
label={t( label={t("settings.fields.proxyImages.label")}
"relations.alias.settings.proxyImages.label",
)}
formik={formik} formik={formik}
icon={<BsImage />} icon={<BsImage />}
name="proxyImages" name="proxyImages"
@ -160,7 +159,7 @@ export default function AliasPreferencesForm({
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<SelectField <SelectField
label={t( label={t(
"relations.alias.settings.imageProxyFormat.label", "settings.fields.imageProxyFormat.label",
)} )}
formik={formik} formik={formik}
icon={<FaFile />} icon={<FaFile />}
@ -173,7 +172,7 @@ export default function AliasPreferencesForm({
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<SelectField <SelectField
label={t( label={t(
"relations.alias.settings.proxyUserAgent.label", "settings.fields.proxyUserAgent.label",
)} )}
formik={formik} formik={formik}
name="proxyUserAgent" name="proxyUserAgent"
@ -196,13 +195,11 @@ export default function AliasPreferencesForm({
type="submit" type="submit"
startIcon={<MdCheckCircle />} startIcon={<MdCheckCircle />}
> >
{t("relations.alias.settings.saveAction")} {t("settings.continueActionLabel")}
</LoadingButton> </LoadingButton>
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body2"> <Typography variant="body2">{t("settings.description")}</Typography>
{t("routes.AliasDetailRoute.sections.settings.description")}
</Typography>
</Grid> </Grid>
</Grid> </Grid>
</Box> </Box>

View File

@ -24,7 +24,7 @@ export default function ChangeAliasActivationStatusSwitch({
isActive, isActive,
queryKey, queryKey,
}: ChangeAliasActivationStatusSwitchProps): ReactElement { }: ChangeAliasActivationStatusSwitchProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("common")
const {showError, showSuccess} = useErrorSuccessSnacks() const {showError, showSuccess} = useErrorSuccessSnacks()
const {_decryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext) const {_decryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext)
@ -83,8 +83,8 @@ export default function ChangeAliasActivationStatusSwitch({
showSuccess( showSuccess(
isActive isActive
? t("relations.alias.mutations.success.aliasChangedToDisabled") ? t("messages.alias.changedToDisabled")
: t("relations.alias.mutations.success.aliasChangedToEnabled"), : t("messages.alias.changedToEnabled"),
) )
} catch {} } catch {}
}} }}

View File

@ -26,7 +26,7 @@ import {AuthContext, EncryptionStatus} from "~/components"
import CustomAliasDialog from "~/route-widgets/AliasesRoute/CustomAliasDialog" import CustomAliasDialog from "~/route-widgets/AliasesRoute/CustomAliasDialog"
export function CreateAliasButton(): ReactElement { export function CreateAliasButton(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["aliases", "common"])
const {showSuccess, showError} = useErrorSuccessSnacks() const {showSuccess, showError} = useErrorSuccessSnacks()
const {_encryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext) const {_encryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext)
@ -54,7 +54,7 @@ export function CreateAliasButton(): ReactElement {
{ {
onError: showError, onError: showError,
onSuccess: async alias => { onSuccess: async alias => {
showSuccess(t("relations.alias.mutations.success.aliasCreation")) showSuccess(t("messages.alias.created", {ns: "common"}))
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["get_aliases"], queryKey: ["get_aliases"],
@ -82,7 +82,7 @@ export function CreateAliasButton(): ReactElement {
}) })
} }
> >
{t("routes.AliasesRoute.actions.createRandomAlias.label")} {t("actions.createRandomAlias.title")}
</Button> </Button>
<Button size="small" onClick={event => setAnchorElement(event.currentTarget)}> <Button size="small" onClick={event => setAnchorElement(event.currentTarget)}>
<MdArrowDropDown /> <MdArrowDropDown />
@ -100,9 +100,7 @@ export function CreateAliasButton(): ReactElement {
<ListItemIcon> <ListItemIcon>
<FaPen /> <FaPen />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText primary={t("actions.createCustomAlias.title")} />
primary={t("routes.AliasesRoute.actions.createCustomAlias.label")}
/>
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>

View File

@ -45,7 +45,7 @@ export default function CustomAliasDialog({
onClose, onClose,
}: CustomAliasDialogProps): ReactElement { }: CustomAliasDialogProps): ReactElement {
const serverSettings = useLoaderData() as ServerSettings const serverSettings = useLoaderData() as ServerSettings
const {t} = useTranslation() const {t} = useTranslation(["aliases", "common"])
const schema = yup.object().shape({ const schema = yup.object().shape({
local: yup local: yup
@ -54,7 +54,7 @@ export default function CustomAliasDialog({
.required() .required()
.min(1) .min(1)
.max(64 - serverSettings.customAliasSuffixLength - 1) .max(64 - serverSettings.customAliasSuffixLength - 1)
.label(t("routes.AliasesRoute.actions.createCustomAlias.form.address.label")), .label(t("fields.customAliasLocal.label", {ns: "common"})),
}) })
const formik = useFormik<Form>({ const formik = useFormik<Form>({
@ -78,12 +78,10 @@ export default function CustomAliasDialog({
return ( return (
<Dialog onClose={onClose} open={visible} keepMounted={false}> <Dialog onClose={onClose} open={visible} keepMounted={false}>
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<DialogTitle> <DialogTitle>{t("actions.createCustomAlias.title")}</DialogTitle>
{t("routes.AliasesRoute.actions.createCustomAlias.label")}
</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
{t("routes.AliasesRoute.actions.createCustomAlias.description")} {t("actions.createCustomAlias.description")}
</DialogContentText> </DialogContentText>
<Box paddingY={4}> <Box paddingY={4}>
<TextField <TextField
@ -92,12 +90,8 @@ export default function CustomAliasDialog({
autoFocus autoFocus
name="local" name="local"
id="local" id="local"
label={t( label={t("fields.customAliasLocal.label", {ns: "common"})}
"routes.AliasesRoute.actions.createCustomAlias.form.address.label", placeholder={t("fields.customAliasLocal.placeholder", {ns: "common"})}
)}
placeholder={t(
"routes.AliasesRoute.actions.createCustomAlias.form.address.placeholder",
)}
value={formik.values.local} value={formik.values.local}
onChange={formik.handleChange} onChange={formik.handleChange}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
@ -122,7 +116,7 @@ export default function CustomAliasDialog({
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} startIcon={<TiCancel />}> <Button onClick={onClose} startIcon={<TiCancel />}>
{t("general.cancelLabel")} {t("general.cancelLabel", {ns: "common"})}
</Button> </Button>
<Button <Button
onClick={() => {}} onClick={() => {}}
@ -131,7 +125,7 @@ export default function CustomAliasDialog({
variant="contained" variant="contained"
type="submit" type="submit"
> >
{t("routes.AliasesRoute.actions.createCustomAlias.continueAction")} {t("actions.createCustomAlias.continueActionLabel")}
</Button> </Button>
</DialogActions> </DialogActions>
</form> </form>

View File

@ -5,7 +5,7 @@ import {FaMask} from "react-icons/fa"
import {Container, Grid, Typography} from "@mui/material" import {Container, Grid, Typography} from "@mui/material"
export default function EmptyStateScreen(): ReactElement { export default function EmptyStateScreen(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("aliases")
return ( return (
<Container maxWidth="xs"> <Container maxWidth="xs">
@ -20,16 +20,14 @@ export default function EmptyStateScreen(): ReactElement {
> >
<Grid item> <Grid item>
<Typography variant="h6" component="h2"> <Typography variant="h6" component="h2">
{t("routes.AliasesRoute.emptyState.title")} {t("emptyState.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<FaMask size={40} /> <FaMask size={40} />
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body1"> <Typography variant="body1">{t("emptyState.description")}</Typography>
{t("routes.AliasesRoute.emptyState.description")}
</Typography>
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>

View File

@ -30,11 +30,11 @@ const SECTION_ICON_MAP: Record<NavigationSection, ReactElement> = {
} }
const SECTION_TEXT_MAP: Record<NavigationSection, string> = { const SECTION_TEXT_MAP: Record<NavigationSection, string> = {
[NavigationSection.Overview]: "components.NavigationButton.overview", [NavigationSection.Overview]: "navigation.overview",
[NavigationSection.Aliases]: "components.NavigationButton.aliases", [NavigationSection.Aliases]: "navigation.aliases",
[NavigationSection.Reports]: "components.NavigationButton.reports", [NavigationSection.Reports]: "navigation.reports",
[NavigationSection.Settings]: "components.NavigationButton.settings", [NavigationSection.Settings]: "navigation.settings",
[NavigationSection.Admin]: "components.NavigationButton.admin", [NavigationSection.Admin]: "navigation.admin",
} }
const PATH_SECTION_MAP: Record<string, NavigationSection> = { const PATH_SECTION_MAP: Record<string, NavigationSection> = {
@ -46,7 +46,7 @@ const PATH_SECTION_MAP: Record<string, NavigationSection> = {
} }
export default function NavigationButton({section}: NavigationButtonProps): ReactElement { export default function NavigationButton({section}: NavigationButtonProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("common")
const {handleAnchorClick} = useContext(LockNavigationContext) const {handleAnchorClick} = useContext(LockNavigationContext)
const location = useLocation() const location = useLocation()

View File

@ -17,7 +17,7 @@ export default function GenerateEmailReportsForm({
onNo, onNo,
onYes, onYes,
}: GenerateEmailReportsFormProps): ReactElement { }: GenerateEmailReportsFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["complete-account", "common"])
return ( return (
<MultiStepFormElement> <MultiStepFormElement>
@ -37,9 +37,7 @@ export default function GenerateEmailReportsForm({
<Grid container spacing={4} direction="column"> <Grid container spacing={4} direction="column">
<Grid item> <Grid item>
<Typography variant="h6" component="h2" align="center"> <Typography variant="h6" component="h2" align="center">
{t( {t("forms.askForGeneration.title")}
"routes.CompleteAccountRoute.forms.generateReports.title",
)}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
@ -49,9 +47,7 @@ export default function GenerateEmailReportsForm({
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="subtitle1" component="p"> <Typography variant="subtitle1" component="p">
{t( {t("forms.askForGeneration.description")}
"routes.CompleteAccountRoute.forms.generateReports.description",
)}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
@ -62,9 +58,7 @@ export default function GenerateEmailReportsForm({
<Grid container spacing={2} direction="row"> <Grid container spacing={2} direction="row">
<Grid item> <Grid item>
<Button startIcon={<TiCancel />} color="secondary" onClick={onNo}> <Button startIcon={<TiCancel />} color="secondary" onClick={onNo}>
{t( {t("general.noLabel", {ns: "common"})}
"routes.CompleteAccountRoute.forms.generateReports.cancelAction",
)}
</Button> </Button>
</Grid> </Grid>
<Grid item> <Grid item>
@ -73,9 +67,7 @@ export default function GenerateEmailReportsForm({
color="primary" color="primary"
onClick={onYes} onClick={onYes}
> >
{t( {t("general.yesLabel", {ns: "common"})}
"routes.CompleteAccountRoute.forms.generateReports.continueAction",
)}
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -11,7 +11,7 @@ import {Box, InputAdornment} from "@mui/material"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {AuthContext, PasswordField, SimpleForm} from "~/components" import {AuthContext, PasswordField, SimpleForm} from "~/components"
import {setupEncryptionForUser} from "~/utils" import {parseFastAPIError, setupEncryptionForUser} from "~/utils"
import {useExtensionHandler, useNavigateToNext, useSystemPreferredTheme, useUser} from "~/hooks" import {useExtensionHandler, useNavigateToNext, useSystemPreferredTheme, useUser} from "~/hooks"
import {ServerSettings, ServerUser} from "~/server-types" import {ServerSettings, ServerUser} from "~/server-types"
import {UpdateAccountData, updateAccount} from "~/apis" import {UpdateAccountData, updateAccount} from "~/apis"
@ -27,7 +27,7 @@ interface Form {
} }
export default function PasswordForm({onDone}: PasswordFormProps): ReactElement { export default function PasswordForm({onDone}: PasswordFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["complete-account", "common"])
const user = useUser() const user = useUser()
const theme = useSystemPreferredTheme() const theme = useSystemPreferredTheme()
const serverSettings = useLoaderData() as ServerSettings const serverSettings = useLoaderData() as ServerSettings
@ -36,17 +36,18 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
const $password = useRef<HTMLInputElement | null>(null) const $password = useRef<HTMLInputElement | null>(null)
const $passwordConfirmation = useRef<HTMLInputElement | null>(null) const $passwordConfirmation = useRef<HTMLInputElement | null>(null)
const schema = yup.object().shape({ const schema = yup.object().shape({
password: yup.string().required(), password: yup
.string()
.required()
.label(t("fields.password.label", {ns: "common"})),
passwordConfirmation: yup passwordConfirmation: yup
.string() .string()
.required() .required()
.oneOf( .oneOf(
[yup.ref("password"), null], [yup.ref("password"), null],
t( t("fields.passwordConfirmation.errors.mismatch", {ns: "common"}) as string,
"routes.CompleteAccountRoute.forms.password.form.passwordConfirm.mustMatchHelperText",
) as string,
) )
.label(t("routes.CompleteAccountRoute.forms.password.form.passwordConfirm.label")), .label(t("fields.passwordConfirmation.label", {ns: "common"})),
}) })
const {_setEncryptionPassword, login} = useContext(AuthContext) const {_setEncryptionPassword, login} = useContext(AuthContext)
@ -89,7 +90,7 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
}, },
) )
} catch (error) { } catch (error) {
setErrors({detail: t("general.defaultError")}) setErrors(parseFastAPIError(error as AxiosError))
} }
}, },
}) })
@ -109,11 +110,8 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
<Box maxWidth="80vw"> <Box maxWidth="80vw">
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<SimpleForm <SimpleForm
title={t("routes.CompleteAccountRoute.forms.password.title")} title={t("forms.enterPassword.title")}
description={t("routes.CompleteAccountRoute.forms.password.description")} description={t("forms.enterPassword.description")}
continueActionLabel={t(
"routes.CompleteAccountRoute.forms.password.continueAction",
)}
nonFieldError={formik.errors.detail} nonFieldError={formik.errors.detail}
> >
{[ {[
@ -123,12 +121,8 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
autoFocus autoFocus
id="password" id="password"
name="password" name="password"
label={t( label={t("fields.password.label", {ns: "common"})}
"routes.CompleteAccountRoute.forms.password.form.password.label", placeholder={t("fields.password.placeholder", {ns: "common"})}
)}
placeholder={t(
"routes.CompleteAccountRoute.forms.password.form.password.placeholder",
)}
autoComplete="new-password" autoComplete="new-password"
value={formik.values.password} value={formik.values.password}
onChange={formik.handleChange} onChange={formik.handleChange}
@ -148,12 +142,10 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
fullWidth fullWidth
id="passwordConfirmation" id="passwordConfirmation"
name="passwordConfirmation" name="passwordConfirmation"
label={t( label={t("fields.passwordConfirmation.label", {ns: "common"})}
"routes.CompleteAccountRoute.forms.password.form.passwordConfirm.label", placeholder={t("fields.passwordConfirmation.placeholder", {
)} ns: "common",
placeholder={t( })}
"routes.CompleteAccountRoute.forms.password.form.passwordConfirm.placeholder",
)}
value={formik.values.passwordConfirmation} value={formik.values.passwordConfirmation}
onChange={formik.handleChange} onChange={formik.handleChange}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}

View File

@ -14,7 +14,7 @@ export interface AliasExplanationProps {
} }
export default function AliasExplanation({local, emails}: AliasExplanationProps): ReactElement { export default function AliasExplanation({local, emails}: AliasExplanationProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("admin-reserved-aliases")
const theme = useTheme() const theme = useTheme()
const serverSettings = useLoaderData() as ServerSettings const serverSettings = useLoaderData() as ServerSettings
@ -37,7 +37,7 @@ export default function AliasExplanation({local, emails}: AliasExplanationProps)
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="caption" textAlign="center"> <Typography variant="caption" textAlign="center">
{t("routes.AdminRoute.forms.reservedAliases.explanation.step1")} {t("createNew.explanation.step1")}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
@ -49,7 +49,7 @@ export default function AliasExplanation({local, emails}: AliasExplanationProps)
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="caption" textAlign="center"> <Typography variant="caption" textAlign="center">
{t("routes.AdminRoute.forms.reservedAliases.explanation.step2")} {t("createNew.explanation.step2")}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
@ -76,7 +76,7 @@ export default function AliasExplanation({local, emails}: AliasExplanationProps)
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="caption" textAlign="center"> <Typography variant="caption" textAlign="center">
{t("routes.AdminRoute.forms.reservedAliases.explanation.step4")} {t("createNew.explanation.step4")}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -37,7 +37,7 @@ export default function UsersSelectField({
error, error,
...props ...props
}: UsersSelectFieldProps): ReactElement { }: UsersSelectFieldProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("admin-reserved-aliases")
const meUser = useUser() const meUser = useUser()
const {data: {users} = {}} = useQuery<GetAdminUsersResponse, AxiosError>( const {data: {users} = {}} = useQuery<GetAdminUsersResponse, AxiosError>(
["getAdminUsers"], ["getAdminUsers"],
@ -49,7 +49,7 @@ export default function UsersSelectField({
return ( return (
<FormControl sx={{minWidth: 180}}> <FormControl sx={{minWidth: 180}}>
<InputLabel id="users-select" error={error}> <InputLabel id="users-select" error={error}>
{t("routes.AdminRoute.forms.reservedAliases.fields.users.label")} {t("fields.users.label")}
</InputLabel> </InputLabel>
<Select<string[]> <Select<string[]>
{...props} {...props}
@ -98,7 +98,7 @@ export default function UsersSelectField({
name="users" name="users"
id="users" id="users"
error={error} error={error}
label={t("routes.AdminRoute.forms.reservedAliases.fields.users.label")} label={t("fields.users.label")}
> >
{users ? ( {users ? (
users.map(user => ( users.map(user => (
@ -108,12 +108,9 @@ export default function UsersSelectField({
primary={(() => { primary={(() => {
// Check if user is me // Check if user is me
if (user.id === meUser.id) { if (user.id === meUser.id) {
return t( return t("fields.users.me", {
"routes.AdminRoute.forms.reservedAliases.fields.users.me",
{
email: user.email.address, email: user.email.address,
}, })
)
} }
return user.email.address return user.email.address
@ -122,7 +119,7 @@ export default function UsersSelectField({
</MenuItem> </MenuItem>
)) ))
) : ( ) : (
<MenuItem value={""}>{t("general.loading")}</MenuItem> <MenuItem value={""}>{t("general.loading", {ns: "common"})}</MenuItem>
)} )}
</Select> </Select>
{helperText ? <FormHelperText error={error}>{helperText}</FormHelperText> : null} {helperText ? <FormHelperText error={error}>{helperText}</FormHelperText> : null}

View File

@ -14,14 +14,14 @@ export default function AliasesPercentageAmount({
length, length,
percentage, percentage,
}: AliasPercentageAmountProps): ReactElement { }: AliasPercentageAmountProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("admin-global-settings")
const amount = Math.floor(Math.pow(characters.length, length) * percentage) const amount = Math.floor(Math.pow(characters.length, length) * percentage)
return ( return (
<Alert severity="info" variant="standard"> <Alert severity="info" variant="standard">
<Typography variant="subtitle1" component="h5"> <Typography variant="subtitle1" component="h5">
{t("routes.AdminRoute.settings.randomAliasesIncreaseExplanation", { {t("randomAliasesIncreaseExplanation", {
originalLength: length, originalLength: length,
increasedLength: length + 1, increasedLength: length + 1,
amount, amount,

View File

@ -18,7 +18,7 @@ export default function RandomAliasGenerator({
length, length,
}: RandomAliasGeneratorProps): ReactElement { }: RandomAliasGeneratorProps): ReactElement {
const serverSettings = useLoaderData() as ServerSettings const serverSettings = useLoaderData() as ServerSettings
const {t} = useTranslation() const {t} = useTranslation("admin-global-settings")
const theme = useTheme() const theme = useTheme()
const generateLocal = useCallback( const generateLocal = useCallback(
@ -39,7 +39,7 @@ export default function RandomAliasGenerator({
return ( return (
<Alert severity="info" variant="standard"> <Alert severity="info" variant="standard">
<Typography variant="subtitle1" component="h5"> <Typography variant="subtitle1" component="h5">
{t("routes.AdminRoute.settings.randomAliasesPreview.title")} {t("randomAliasesPreview.title")}
</Typography> </Typography>
<Grid container spacing={2} direction="row" alignItems="center"> <Grid container spacing={2} direction="row" alignItems="center">
<Grid item> <Grid item>
@ -51,9 +51,7 @@ export default function RandomAliasGenerator({
</IconButton> </IconButton>
</Grid> </Grid>
</Grid> </Grid>
<FormHelperText> <FormHelperText>{t("randomAliasesPreview.helperText")}</FormHelperText>
{t("routes.AdminRoute.settings.randomAliasesPreview.helperText")}
</FormHelperText>
</Alert> </Alert>
) )
} }

View File

@ -5,8 +5,7 @@ import {useTranslation} from "react-i18next"
import {Container, Grid, Typography} from "@mui/material" import {Container, Grid, Typography} from "@mui/material"
export default function SettingsDisabled(): ReactElement { export default function SettingsDisabled(): ReactElement {
console.log("asdas") const {t} = useTranslation("admin-global-settings")
const {t} = useTranslation()
return ( return (
<Container maxWidth="xs"> <Container maxWidth="xs">
@ -21,16 +20,14 @@ export default function SettingsDisabled(): ReactElement {
> >
<Grid item> <Grid item>
<Typography variant="h6" component="h2"> <Typography variant="h6" component="h2">
{t("routes.AdminRoute.settings.disabled.title")} {t("disabled.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<RiAlertFill size={40} /> <RiAlertFill size={40} />
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body1"> <Typography variant="body1">{t("disabled.description")}</Typography>
{t("routes.AdminRoute.settings.disabled.description")}
</Typography>
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>

View File

@ -44,7 +44,7 @@ const DEFAULT_POOLS = createPool({
}) })
export default function SettingsForm({settings, queryKey}: SettingsFormProps) { export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
const {t} = useTranslation() const {t} = useTranslation(["admin-global-settings", "common"])
const {showSuccess, showError} = useErrorSuccessSnacks() const {showSuccess, showError} = useErrorSuccessSnacks()
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
@ -52,50 +52,33 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
.number() .number()
.min(1) .min(1)
.max(1_023) .max(1_023)
.label(t("routes.AdminRoute.forms.settings.randomEmailIdMinLength.label")), .label(t("fields.randomEmailIdMinLength.label")),
randomEmailIdChars: yup randomEmailIdChars: yup.string().label(t("fields.randomEmailIdChars.label")),
.string()
.label(t("routes.AdminRoute.forms.settings.randomEmailIdChars.label")),
randomEmailLengthIncreaseOnPercentage: yup randomEmailLengthIncreaseOnPercentage: yup
.number() .number()
.min(0) .min(0)
.max(1) .max(1)
.label( .label(t("fields.randomEmailLengthIncreaseOnPercentage.label")),
t("routes.AdminRoute.forms.settings.randomEmailLengthIncreaseOnPercentage.label"),
),
imageProxyStorageLifeTimeInHours: yup imageProxyStorageLifeTimeInHours: yup
.number() .number()
.label(t("routes.AdminRoute.forms.settings.imageProxyStorageLifeTimeInHours.label")), .label(t("fields.imageProxyStorageLifeTimeInHours.label")),
customEmailSuffixLength: yup customEmailSuffixLength: yup
.number() .number()
.min(1) .min(1)
.max(1_023) .max(1_023)
.label(t("routes.AdminRoute.forms.settings.customEmailSuffixLength-label")), .label(t("fields.customEmailSuffixLength.label")),
customEmailSuffixChars: yup customEmailSuffixChars: yup.string().label(t("fields.customEmailSuffixChars.label")),
.string()
.label(t("routes.AdminRoute.forms.settings.customEmailSuffixChars.label")),
userEmailEnableDisposableEmails: yup userEmailEnableDisposableEmails: yup
.boolean() .boolean()
.label(t("routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label")), .label(t("fields.userEmailEnableDisposableEmails.label")),
userEmailEnableOtherRelays: yup userEmailEnableOtherRelays: yup
.boolean() .boolean()
.label(t("routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label")), .label(t("fields.userEmailEnableOtherRelays.label")),
enableImageProxy: yup enableImageProxy: yup.boolean().label(t("fields.enableImageProxy.label")),
.boolean() enableImageProxyStorage: yup.boolean().label(t("fields.enableImageProxyStorage.label")),
.label(t("routes.AdminRoute.forms.settings.enableImageProxy.label")), allowStatistics: yup.boolean().label(t("fields.allowStatistics.label")),
enableImageProxyStorage: yup allowAliasDeletion: yup.boolean().label(t("fields.allowAliasDeletion.label")),
.boolean() maxAliasesPerUser: yup.number().label(t("fields.maxAliasesPerUser.label")).min(0),
.label(t("routes.AdminRoute.forms.settings.enableImageProxyStorage.label")),
allowStatistics: yup
.boolean()
.label(t("routes.AdminRoute.forms.settings.allowStatistics.label")),
allowAliasDeletion: yup
.boolean()
.label(t("routes.AdminRoute.forms.settings.allowAliasDeletion.label")),
maxAliasesPerUser: yup
.number()
.label(t("routes.AdminRoute.forms.settings.maxAliasesPerUser.label"))
.min(0),
} as Record<keyof AdminSettings, any>) } as Record<keyof AdminSettings, any>)
const {mutateAsync} = useMutation< const {mutateAsync} = useMutation<
@ -109,7 +92,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
return return
} }
showSuccess(t("routes.AdminRoute.settings.successMessage")) showSuccess(t("updatedSuccessfullyMessage"))
queryClient.setQueryData<Partial<AdminSettings>>(queryKey, newSettings) queryClient.setQueryData<Partial<AdminSettings>>(queryKey, newSettings)
}, },
@ -144,12 +127,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
<Grid container spacing={2} direction="column"> <Grid container spacing={2} direction="column">
<Grid item> <Grid item>
<Typography variant="h4" component="h1" align="center"> <Typography variant="h4" component="h1" align="center">
{t("routes.AdminRoute.settings.title")} {t("title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="subtitle1" component="p"> <Typography variant="subtitle1" component="p">
{t("routes.AdminRoute.settings.description")} {t("description")}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
@ -160,9 +143,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
<TextField <TextField
key="max_aliases_per_user" key="max_aliases_per_user"
fullWidth fullWidth
label={t( label={t("fields.maxAliasesPerUser.label")}
"routes.AdminRoute.forms.settings.maxAliasesPerUser.label",
)}
name="maxAliasesPerUser" name="maxAliasesPerUser"
value={formik.values.maxAliasesPerUser} value={formik.values.maxAliasesPerUser}
onChange={formik.handleChange} onChange={formik.handleChange}
@ -173,9 +154,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
helperText={ helperText={
(formik.touched.maxAliasesPerUser && (formik.touched.maxAliasesPerUser &&
formik.errors.maxAliasesPerUser) || formik.errors.maxAliasesPerUser) ||
t( t("fields.maxAliasesPerUser.description")
"routes.AdminRoute.forms.settings.maxAliasesPerUser.description",
)
} }
type="number" type="number"
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
@ -193,9 +172,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
<TextField <TextField
key="random_email_id_min_length" key="random_email_id_min_length"
fullWidth fullWidth
label={t( label={t("fields.randomEmailIdMinLength.label")}
"routes.AdminRoute.forms.settings.randomEmailIdMinLength.label",
)}
name="randomEmailIdMinLength" name="randomEmailIdMinLength"
value={formik.values.randomEmailIdMinLength} value={formik.values.randomEmailIdMinLength}
onChange={formik.handleChange} onChange={formik.handleChange}
@ -206,9 +183,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
helperText={ helperText={
(formik.touched.randomEmailIdMinLength && (formik.touched.randomEmailIdMinLength &&
formik.errors.randomEmailIdMinLength) || formik.errors.randomEmailIdMinLength) ||
t( t("fields.randomEmailIdMinLength.description")
"routes.AdminRoute.forms.settings.randomEmailIdMinLength.description",
)
} }
type="number" type="number"
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
@ -229,9 +204,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
key="random_email_id_chars" key="random_email_id_chars"
pools={DEFAULT_POOLS} pools={DEFAULT_POOLS}
id="random_email_id_chars" id="random_email_id_chars"
label={t( label={t("fields.randomEmailIdChars.label")}
"routes.AdminRoute.forms.settings.randomEmailIdChars.label",
)}
name="randomEmailIdChars" name="randomEmailIdChars"
value={formik.values.randomEmailIdChars!} value={formik.values.randomEmailIdChars!}
onChange={formik.handleChange} onChange={formik.handleChange}
@ -242,9 +215,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
helperText={ helperText={
(formik.touched.randomEmailIdChars && (formik.touched.randomEmailIdChars &&
formik.errors.randomEmailIdChars) || formik.errors.randomEmailIdChars) ||
(t( (t("fields.randomEmailIdChars.description") as string)
"routes.AdminRoute.forms.settings.randomEmailIdChars.description",
) as string)
} }
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
startAdornment={ startAdornment={
@ -264,9 +235,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
<TextField <TextField
key="random_email_length_increase_on_percentage" key="random_email_length_increase_on_percentage"
fullWidth fullWidth
label={t( label={t("fields.randomEmailLengthIncreaseOnPercentage.label")}
"routes.AdminRoute.forms.settings.randomEmailLengthIncreaseOnPercentage.label",
)}
name="randomEmailLengthIncreaseOnPercentage" name="randomEmailLengthIncreaseOnPercentage"
value={formik.values.randomEmailLengthIncreaseOnPercentage} value={formik.values.randomEmailLengthIncreaseOnPercentage}
onChange={formik.handleChange} onChange={formik.handleChange}
@ -277,9 +246,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
helperText={ helperText={
(formik.touched.randomEmailLengthIncreaseOnPercentage && (formik.touched.randomEmailLengthIncreaseOnPercentage &&
formik.errors.randomEmailLengthIncreaseOnPercentage) || formik.errors.randomEmailLengthIncreaseOnPercentage) ||
t( t("fields.randomEmailLengthIncreaseOnPercentage.description")
"routes.AdminRoute.forms.settings.randomEmailLengthIncreaseOnPercentage.description",
)
} }
type="number" type="number"
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
@ -304,9 +271,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
<TextField <TextField
key="custom_email_suffix_length" key="custom_email_suffix_length"
fullWidth fullWidth
label={t( label={t("fields.customEmailSuffixLength.label")}
"routes.AdminRoute.forms.settings.customEmailSuffixLength.label",
)}
name="customEmailSuffixLength" name="customEmailSuffixLength"
value={formik.values.customEmailSuffixLength} value={formik.values.customEmailSuffixLength}
onChange={formik.handleChange} onChange={formik.handleChange}
@ -317,9 +282,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
helperText={ helperText={
(formik.touched.customEmailSuffixLength && (formik.touched.customEmailSuffixLength &&
formik.errors.customEmailSuffixLength) || formik.errors.customEmailSuffixLength) ||
t( t("fields.customEmailSuffixLength.description")
"routes.AdminRoute.forms.settings.customEmailSuffixLength.description",
)
} }
type="number" type="number"
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
@ -340,9 +303,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
fullWidth fullWidth
pools={DEFAULT_POOLS} pools={DEFAULT_POOLS}
id="custom_email_suffix_chars" id="custom_email_suffix_chars"
label={t( label={t("fields.customEmailSuffixChars.label")}
"routes.AdminRoute.forms.settings.customEmailSuffixChars.label",
)}
name="customEmailSuffixChars" name="customEmailSuffixChars"
value={formik.values.customEmailSuffixChars!} value={formik.values.customEmailSuffixChars!}
onChange={formik.handleChange} onChange={formik.handleChange}
@ -353,9 +314,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
helperText={ helperText={
(formik.touched.customEmailSuffixChars && (formik.touched.customEmailSuffixChars &&
formik.errors.customEmailSuffixChars) || formik.errors.customEmailSuffixChars) ||
(t( (t("fields.customEmailSuffixChars.description") as string)
"routes.AdminRoute.forms.settings.customEmailSuffixChars.description",
) as string)
} }
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
startAdornment={ startAdornment={
@ -369,9 +328,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
<TextField <TextField
key="image_proxy_storage_life_time_in_hours" key="image_proxy_storage_life_time_in_hours"
fullWidth fullWidth
label={t( label={t("fields.imageProxyStorageLifeTimeInHours.label")}
"routes.AdminRoute.forms.settings.imageProxyStorageLifeTimeInHours.label",
)}
name="imageProxyStorageLifeTimeInHours" name="imageProxyStorageLifeTimeInHours"
value={formik.values.imageProxyStorageLifeTimeInHours} value={formik.values.imageProxyStorageLifeTimeInHours}
onChange={formik.handleChange} onChange={formik.handleChange}
@ -382,9 +339,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
helperText={ helperText={
(formik.touched.imageProxyStorageLifeTimeInHours && (formik.touched.imageProxyStorageLifeTimeInHours &&
formik.errors.imageProxyStorageLifeTimeInHours) || formik.errors.imageProxyStorageLifeTimeInHours) ||
t( t("fields.imageProxyStorageLifeTimeInHours.description")
"routes.AdminRoute.forms.settings.imageProxyStorageLifeTimeInHours.description",
)
} }
type="number" type="number"
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
@ -397,14 +352,11 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
), ),
endAdornment: ( endAdornment: (
<InputAdornment position="end"> <InputAdornment position="end">
{t( {t("fields.imageProxyStorageLifeTimeInHours.unit", {
"routes.AdminRoute.forms.settings.imageProxyStorageLifeTimeInHours.unit",
{
count: count:
formik.values formik.values
.imageProxyStorageLifeTimeInHours || 0, .imageProxyStorageLifeTimeInHours || 0,
}, })}
)}
</InputAdornment> </InputAdornment>
), ),
}} }}
@ -421,9 +373,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
/> />
} }
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
label={t( label={t("fields.userEmailEnableDisposableEmails.label")}
"routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label",
)}
/> />
<FormHelperText <FormHelperText
error={ error={
@ -433,9 +383,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
> >
{(formik.touched.userEmailEnableDisposableEmails && {(formik.touched.userEmailEnableDisposableEmails &&
formik.errors.userEmailEnableDisposableEmails) || formik.errors.userEmailEnableDisposableEmails) ||
t( t("fields.userEmailEnableDisposableEmails.description")}
"routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.description",
)}
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
@ -450,9 +398,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
/> />
} }
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
label={t( label={t("fields.userEmailEnableOtherRelays.label")}
"routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label",
)}
/> />
<FormHelperText <FormHelperText
error={ error={
@ -462,9 +408,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
> >
{(formik.touched.userEmailEnableOtherRelays && {(formik.touched.userEmailEnableOtherRelays &&
formik.errors.userEmailEnableOtherRelays) || formik.errors.userEmailEnableOtherRelays) ||
t( t("fields.userEmailEnableOtherRelays.description")}
"routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.description",
)}
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
@ -481,9 +425,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
/> />
} }
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
label={t( label={t("fields.enableImageProxy.label")}
"routes.AdminRoute.forms.settings.enableImageProxy.label",
)}
/> />
<FormHelperText <FormHelperText
error={ error={
@ -493,9 +435,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
> >
{(formik.touched.enableImageProxy && {(formik.touched.enableImageProxy &&
formik.errors.enableImageProxy) || formik.errors.enableImageProxy) ||
t( t("fields.enableImageProxy.description")}
"routes.AdminRoute.forms.settings.enableImageProxy.description",
)}
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
@ -513,9 +453,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
/> />
} }
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
label={t( label={t("fields.enableImageProxyStorage.label")}
"routes.AdminRoute.forms.settings.enableImageProxyStorage.label",
)}
/> />
<FormHelperText <FormHelperText
error={ error={
@ -525,9 +463,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
> >
{(formik.touched.enableImageProxyStorage && {(formik.touched.enableImageProxyStorage &&
formik.errors.enableImageProxyStorage) || formik.errors.enableImageProxyStorage) ||
t( t("fields.enableImageProxyStorage.description")}
"routes.AdminRoute.forms.settings.enableImageProxyStorage.description",
)}
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Collapse> </Collapse>
@ -545,9 +481,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
/> />
} }
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
label={t( label={t("fields.allowStatistics.label")}
"routes.AdminRoute.forms.settings.allowStatistics.label",
)}
/> />
<FormHelperText <FormHelperText
error={ error={
@ -557,9 +491,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
> >
{(formik.touched.allowStatistics && {(formik.touched.allowStatistics &&
formik.errors.allowStatistics) || formik.errors.allowStatistics) ||
t( t("fields.allowStatistics.description")}
"routes.AdminRoute.forms.settings.allowStatistics.description",
)}
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
@ -574,9 +506,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
/> />
} }
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
label={t( label={t("fields.allowAliasDeletion.label")}
"routes.AdminRoute.forms.settings.allowAliasDeletion.label",
)}
/> />
<FormHelperText <FormHelperText
error={ error={
@ -586,9 +516,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
> >
{(formik.touched.allowAliasDeletion && {(formik.touched.allowAliasDeletion &&
formik.errors.allowAliasDeletion) || formik.errors.allowAliasDeletion) ||
t( t("fields.allowAliasDeletion.description")}
"routes.AdminRoute.forms.settings.allowAliasDeletion.description",
)}
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
@ -608,7 +536,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
formik.submitForm() formik.submitForm()
}} }}
> >
{t("routes.AdminRoute.settings.resetLabel")} {t("general.resetLabel", {ns: "common"})}
</LoadingButton> </LoadingButton>
</Grid> </Grid>
<Grid item> <Grid item>
@ -618,7 +546,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
type="submit" type="submit"
startIcon={<MdCheck />} startIcon={<MdCheck />}
> >
{t("general.saveLabel")} {t("general.saveLabel", {ns: "common"})}
</LoadingButton> </LoadingButton>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -55,7 +55,7 @@ export default function ConfirmCodeForm({
}: ConfirmCodeFormProps): ReactElement { }: ConfirmCodeFormProps): ReactElement {
const settings = useLoaderData() as ServerSettings const settings = useLoaderData() as ServerSettings
const expirationTime = isDev ? 70 : settings.emailLoginExpirationInSeconds const expirationTime = isDev ? 70 : settings.emailLoginExpirationInSeconds
const {t} = useTranslation() const {t} = useTranslation(["login", "common"])
const requestDate = useMemo(() => new Date(), []) const requestDate = useMemo(() => new Date(), [])
const [isExpiringSoon, setIsExpiringSoon] = useState<boolean>(false) const [isExpiringSoon, setIsExpiringSoon] = useState<boolean>(false)
@ -67,7 +67,7 @@ export default function ConfirmCodeForm({
.max(settings.emailLoginTokenLength) .max(settings.emailLoginTokenLength)
.test( .test(
"chars", "chars",
t("routes.LoginRoute.forms.confirmCode.form.code.errors.invalidChars") as string, t("forms.confirmCode.fields.code.errors.invalidChars") as string,
code => { code => {
if (!code) { if (!code) {
return false return false
@ -78,7 +78,7 @@ export default function ConfirmCodeForm({
return code.split("").every(char => chars.includes(char)) return code.split("").every(char => chars.includes(char))
}, },
) )
.label(t("routes.LoginRoute.forms.confirmCode.form.code.label")), .label(t("forms.confirmCode.fields.code.label")),
}) })
const {mutateAsync} = useMutation< const {mutateAsync} = useMutation<
@ -165,7 +165,7 @@ export default function ConfirmCodeForm({
> >
<Grid item> <Grid item>
<Typography variant="h6" component="h1" align="center"> <Typography variant="h6" component="h1" align="center">
{t("routes.LoginRoute.forms.confirmCode.title")} {t("forms.confirmCode.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
@ -175,7 +175,7 @@ export default function ConfirmCodeForm({
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="subtitle1" component="p" align="center"> <Typography variant="subtitle1" component="p" align="center">
{t("routes.LoginRoute.forms.confirmCode.description")} {t("forms.confirmCode.description")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
@ -196,7 +196,7 @@ export default function ConfirmCodeForm({
} }
labelPlacement="end" labelPlacement="end"
label={t( label={t(
"routes.LoginRoute.forms.confirmCode.allowLoginFromDifferentDevices", "forms.confirmCode.allowLoginFromDifferentDevices",
)} )}
/> />
</Grid> </Grid>
@ -206,9 +206,7 @@ export default function ConfirmCodeForm({
fullWidth fullWidth
name="code" name="code"
id="code" id="code"
label={t( label={t("forms.confirmCode.fields.code.label")}
"routes.LoginRoute.forms.confirmCode.form.code.label",
)}
value={formik.values.code} value={formik.values.code}
onChange={event => { onChange={event => {
formik.setFieldValue( formik.setFieldValue(
@ -250,7 +248,7 @@ export default function ConfirmCodeForm({
type="submit" type="submit"
startIcon={<MdChevronRight />} startIcon={<MdChevronRight />}
> >
{t("routes.LoginRoute.forms.confirmCode.continueAction")} {t("forms.confirmCode.continueActionLabel")}
</LoadingButton> </LoadingButton>
</Grid> </Grid>
</Grid> </Grid>
@ -260,7 +258,7 @@ export default function ConfirmCodeForm({
</MultiStepFormElement> </MultiStepFormElement>
<Snackbar open={isExpiringSoon}> <Snackbar open={isExpiringSoon}>
<Alert severity="warning" variant="filled"> <Alert severity="warning" variant="filled">
{t("routes.LoginRoute.forms.confirmCode.expiringSoon")} {t("forms.confirmCode.expiringSoonWarning")}
</Alert> </Alert>
</Snackbar> </Snackbar>
</> </>

View File

@ -19,8 +19,8 @@ export default function ResendMailButton({
email, email,
sameRequestToken, sameRequestToken,
}: ResendMailButtonProps): ReactElement { }: ResendMailButtonProps): ReactElement {
const {t} = useTranslation("components")
const settings = useLoaderData() as ServerSettings const settings = useLoaderData() as ServerSettings
const {t} = useTranslation()
const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() => const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() =>
resendEmailLoginCode({ resendEmailLoginCode({
@ -37,7 +37,7 @@ export default function ResendMailButton({
startIcon={<MdMail />} startIcon={<MdMail />}
onClick={() => mutate()} onClick={() => mutate()}
> >
{t("components.ResendMailButton.label")} {t("ResendMailButton.label")}
</TimedButton> </TimedButton>
<MutationStatusSnackbar mutation={mutation} /> <MutationStatusSnackbar mutation={mutation} />
</> </>

View File

@ -22,7 +22,7 @@ export default function ConfirmFromDifferentDevice({
token, token,
onConfirm, onConfirm,
}: ConfirmFromDifferentDeviceProps): ReactElement { }: ConfirmFromDifferentDeviceProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["login"])
const {mutate, isLoading, isError} = useMutation<ServerUser, AxiosError, void>( const {mutate, isLoading, isError} = useMutation<ServerUser, AxiosError, void>(
() => () =>
verifyLoginWithEmail({ verifyLoginWithEmail({
@ -51,14 +51,12 @@ export default function ConfirmFromDifferentDevice({
<Grid container spacing={2} direction="column" alignItems="center"> <Grid container spacing={2} direction="column" alignItems="center">
<Grid item> <Grid item>
<Typography variant="h6" component="h1"> <Typography variant="h6" component="h1">
{t("routes.LoginRoute.forms.confirmFromDifferentDevice.title")} {t("forms.confirmFromDifferentDevice.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body1"> <Typography variant="body1">
{t( {t("forms.confirmFromDifferentDevice.description")}
"routes.LoginRoute.forms.confirmFromDifferentDevice.description",
)}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -24,7 +24,7 @@ interface Form {
} }
export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormProps): ReactElement { export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["login", "common"])
const $password = useRef<HTMLInputElement | null>(null) const $password = useRef<HTMLInputElement | null>(null)
const schema = yup.object().shape({ const schema = yup.object().shape({
@ -32,7 +32,7 @@ export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormPro
.string() .string()
.email() .email()
.required() .required()
.label(t("routes.LoginRoute.forms.email.form.email.label")), .label(t("fields.email.label", {ns: "common"})),
}) })
const {mutateAsync} = useMutation<LoginWithEmailResult, AxiosError, string>(loginWithEmail, { const {mutateAsync} = useMutation<LoginWithEmailResult, AxiosError, string>(loginWithEmail, {
@ -63,9 +63,9 @@ export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormPro
<MultiStepFormElement> <MultiStepFormElement>
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<SimpleForm <SimpleForm
title={t("routes.LoginRoute.forms.email.title")} title={t("title", {ns: "login"})}
description={t("routes.LoginRoute.forms.email.description")} description={t("forms.email.description", {ns: "login"})}
continueActionLabel={t("routes.LoginRoute.forms.email.continueAction")} continueActionLabel={t("forms.email.continueActionLabel", {ns: "login"})}
nonFieldError={formik.errors.detail} nonFieldError={formik.errors.detail}
isSubmitting={formik.isSubmitting} isSubmitting={formik.isSubmitting}
> >
@ -77,7 +77,7 @@ export default function EmailForm({onLogin, email: preFilledEmail}: EmailFormPro
name="email" name="email"
id="email" id="email"
label="Email" label="Email"
placeholder={t("routes.LoginRoute.forms.email.form.email.placeholder")} placeholder={t("fields.email.placeholder", {ns: "common"})}
inputRef={$password} inputRef={$password}
inputMode="email" inputMode="email"
value={formik.values.email} value={formik.values.email}

View File

@ -1,19 +1,21 @@
import * as yup from "yup" import * as yup from "yup"
import {ReactElement} from "react" import {ReactElement} from "react"
import {useMutation} from "@tanstack/react-query"
import {ServerUser} from "~/server-types"
import {AxiosError} from "axios"
import {verifyOTP} from "~/apis"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import {useFormik} from "formik" import {useFormik} from "formik"
import {BsPhone, BsShieldLockFill} from "react-icons/bs"
import {MdChevronRight} from "react-icons/md"
import {Link as RouterLink} from "react-router-dom"
import {AxiosError} from "axios"
import {useMutation} from "@tanstack/react-query"
import {LoadingButton} from "@mui/lab"
import {Box, Button, Grid, InputAdornment, TextField, Typography} from "@mui/material"
import {verifyOTP} from "~/apis"
import {parseFastAPIError} from "~/utils" import {parseFastAPIError} from "~/utils"
import {MultiStepFormElement} from "~/components" import {MultiStepFormElement} from "~/components"
import {Box, Button, Grid, InputAdornment, TextField, Typography} from "@mui/material"
import {BsPhone, BsShieldLockFill} from "react-icons/bs"
import {LoadingButton} from "@mui/lab"
import {MdChevronRight} from "react-icons/md"
import {useErrorSuccessSnacks} from "~/hooks" import {useErrorSuccessSnacks} from "~/hooks"
import {Link as RouterLink} from "react-router-dom" import {ServerUser} from "~/server-types"
interface Form { interface Form {
code: string code: string
@ -30,7 +32,7 @@ export default function OTPForm({
onConfirm, onConfirm,
onCodeUnavailable, onCodeUnavailable,
}: OTPFormProps): ReactElement { }: OTPFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["login", "common"])
const {showError} = useErrorSuccessSnacks() const {showError} = useErrorSuccessSnacks()
const {mutateAsync} = useMutation<ServerUser, AxiosError, string>( const {mutateAsync} = useMutation<ServerUser, AxiosError, string>(
code => code =>
@ -42,7 +44,7 @@ export default function OTPForm({
onSuccess: onConfirm, onSuccess: onConfirm,
onError: error => { onError: error => {
if (error.response?.status === 410 || error.response?.status === 404) { if (error.response?.status === 410 || error.response?.status === 404) {
showError(t("routes.LoginRoute.forms.otp.unavailable").toString()) showError(t("forms.otp.isUnavailable").toString())
onCodeUnavailable() onCodeUnavailable()
} }
}, },
@ -50,7 +52,10 @@ export default function OTPForm({
) )
const schema = yup.object().shape({ const schema = yup.object().shape({
code: yup.string().required().label(t("routes.LoginRoute.forms.otp.code.label")), code: yup
.string()
.required()
.label(t("fields.2faCode.label", {ns: "common"})),
}) })
const formik = useFormik<Form>({ const formik = useFormik<Form>({
@ -81,7 +86,7 @@ export default function OTPForm({
> >
<Grid item> <Grid item>
<Typography variant="h6" component="h1" align="center"> <Typography variant="h6" component="h1" align="center">
{t("routes.LoginRoute.forms.otp.title")} {t("forms.otp.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
@ -91,7 +96,7 @@ export default function OTPForm({
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="subtitle1" component="p" align="center"> <Typography variant="subtitle1" component="p" align="center">
{t("routes.LoginRoute.forms.otp.description")} {t("forms.otp.description")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
@ -101,7 +106,8 @@ export default function OTPForm({
fullWidth fullWidth
name="code" name="code"
id="code" id="code"
label={t("routes.LoginRoute.forms.otp.code.label")} placeholder={t("fields.2faCode.placeholder", {ns: "common"})}
label={t("fields.2faCode.label", {ns: "common"})}
value={formik.values.code} value={formik.values.code}
onChange={formik.handleChange} onChange={formik.handleChange}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
@ -125,12 +131,12 @@ export default function OTPForm({
type="submit" type="submit"
startIcon={<MdChevronRight />} startIcon={<MdChevronRight />}
> >
{t("routes.LoginRoute.forms.otp.submit")} {t("forms.otp.continueActionLabel")}
</LoadingButton> </LoadingButton>
</Grid> </Grid>
<Grid item> <Grid item>
<Button component={RouterLink} to="/auth/recover-2fa"> <Button component={RouterLink} to="/auth/recover-2fa">
{t("routes.LoginRoute.forms.otp.lost")} {t("forms.otp.codesLostActionLabel")}
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -12,13 +12,13 @@ export interface ExpandedUrlsListItemProps {
} }
export default function ExpandedUrlsListItem({urls}: ExpandedUrlsListItemProps): ReactElement { export default function ExpandedUrlsListItem({urls}: ExpandedUrlsListItemProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("reports")
return ( return (
<ExpandableListItem <ExpandableListItem
data={urls} data={urls}
icon={<BsArrowsAngleExpand />} icon={<BsArrowsAngleExpand />}
title={t("routes.ReportDetailRoute.sections.trackers.results.expandedUrls.text", { title={t("sections.trackers.results.expandedUrls.text", {
count: urls.length, count: urls.length,
})} })}
> >

View File

@ -17,14 +17,14 @@ export interface ProxiedImagesListItemProps {
} }
export default function ProxiedImagesListItem({images}: ProxiedImagesListItemProps): ReactElement { export default function ProxiedImagesListItem({images}: ProxiedImagesListItemProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("reports")
const serverSettings = useLoaderData() as ServerSettings const serverSettings = useLoaderData() as ServerSettings
return ( return (
<ExpandableListItem <ExpandableListItem
data={images} data={images}
icon={<BsImage />} icon={<BsImage />}
title={t("routes.ReportDetailRoute.sections.trackers.results.proxiedImages.text", { title={t("sections.trackers.results.proxiedImages.text", {
count: images.length, count: images.length,
})} })}
> >
@ -62,11 +62,11 @@ export default function ProxiedImagesListItem({images}: ProxiedImagesListItemPro
) )
) { ) {
return t( return t(
"routes.ReportDetailRoute.sections.trackers.results.proxiedImages.status.isStored", "sections.trackers.results.proxiedImages.status.isStored",
) )
} else { } else {
return t( return t(
"routes.ReportDetailRoute.sections.trackers.results.proxiedImages.status.isProxying", "sections.trackers.results.proxiedImages.status.isProxying",
) )
} }
})()} })()}

View File

@ -14,7 +14,7 @@ export interface SinglePixelImageTrackersListItemProps {
export default function SinglePixelImageTrackersListItem({ export default function SinglePixelImageTrackersListItem({
images, images,
}: SinglePixelImageTrackersListItemProps): ReactElement { }: SinglePixelImageTrackersListItemProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("reports")
const imagesPerTracker = images.reduce((acc, value) => { const imagesPerTracker = images.reduce((acc, value) => {
acc[value.trackerName] = [...(acc[value.trackerName] || []), value] acc[value.trackerName] = [...(acc[value.trackerName] || []), value]
@ -26,7 +26,7 @@ export default function SinglePixelImageTrackersListItem({
<ExpandableListItem <ExpandableListItem
data={images} data={images}
icon={<BsShieldShaded />} icon={<BsShieldShaded />}
title={t("routes.ReportDetailRoute.sections.trackers.results.imageTrackers.text", { title={t("sections.trackers.results.imageTrackers.text", {
count: images.length, count: images.length,
})} })}
> >

View File

@ -5,23 +5,21 @@ import {MdTextSnippet} from "react-icons/md"
import {Container, Grid, Typography} from "@mui/material" import {Container, Grid, Typography} from "@mui/material"
export default function EmptyStateScreen(): ReactElement { export default function EmptyStateScreen(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("reports")
return ( return (
<Container maxWidth="xs"> <Container maxWidth="xs">
<Grid container spacing={4} direction="column" alignItems="center" alignSelf="center"> <Grid container spacing={4} direction="column" alignItems="center" alignSelf="center">
<Grid item> <Grid item>
<Typography variant="h6" component="h2"> <Typography variant="h6" component="h2">
{t("routes.ReportsRoute.emptyState.title")} {t("emptyState.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<MdTextSnippet size={64} /> <MdTextSnippet size={64} />
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body1"> <Typography variant="body1">{t("emptyState.description")}</Typography>
{t("routes.ReportsRoute.emptyState.description")}
</Typography>
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>

View File

@ -11,18 +11,16 @@ export interface ReportInformationItemProps {
} }
export default function ReportInformationItem({report}: ReportInformationItemProps): ReactElement { export default function ReportInformationItem({report}: ReportInformationItemProps): ReactElement {
const {t} = useTranslation("reports")
const navigate = useNavigate() const navigate = useNavigate()
const {t} = useTranslation()
return ( return (
<ListItemButton onClick={() => navigate(`/reports/${report.id}`)}> <ListItemButton onClick={() => navigate(`/reports/${report.id}`)}>
<ListItemText <ListItemText
primary={ primary={
report.messageDetails.content.subject || ( report.messageDetails.content.subject || <i>{t("emailMeta.emptySubject")}</i>
<i>{t("relations.report.emailMeta.emptySubject")}</i>
)
} }
secondary={t("relations.report.emailMeta.flow", { secondary={t("emailMeta.flow", {
from: report.messageDetails.meta.from, from: report.messageDetails.meta.from,
to: report.messageDetails.meta.to, to: report.messageDetails.meta.to,
})} })}

View File

@ -17,7 +17,7 @@ export default function AdminUserPicker({
onPick, onPick,
alreadyPicked, alreadyPicked,
}: AdminUserPickerProps): ReactElement { }: AdminUserPickerProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("admin-reserved-aliases")
const meUser = useUser() const meUser = useUser()
const {data: {users: availableUsers} = {}} = useQuery<GetAdminUsersResponse, AxiosError>( const {data: {users: availableUsers} = {}} = useQuery<GetAdminUsersResponse, AxiosError>(
["getAdminUsers"], ["getAdminUsers"],
@ -54,7 +54,7 @@ export default function AdminUserPicker({
{users.map(user => ( {users.map(user => (
<MenuItem key={user.id} value={user.id}> <MenuItem key={user.id} value={user.id}>
{user.id === meUser?.id {user.id === meUser?.id
? t("routes.AdminRoute.forms.reservedAliases.fields.users.me", { ? t("fields.users.me", {
email: user.email.address, email: user.email.address,
}) })
: user.email.address} : user.email.address}

View File

@ -22,7 +22,7 @@ export default function AliasActivationSwitch({
isActive, isActive,
queryKey, queryKey,
}: AliasActivationSwitch): ReactElement { }: AliasActivationSwitch): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("common")
const {showError, showSuccess} = useErrorSuccessSnacks() const {showError, showSuccess} = useErrorSuccessSnacks()
const {isLoading, mutateAsync} = useMutation< const {isLoading, mutateAsync} = useMutation<
ReservedAlias, ReservedAlias,
@ -76,8 +76,8 @@ export default function AliasActivationSwitch({
showSuccess( showSuccess(
isActive isActive
? t("relations.alias.mutations.success.aliasChangedToDisabled") ? t("messages.alias.changedToDisabled")
: t("relations.alias.mutations.success.aliasChangedToEnabled"), : t("messages.alias.changedToEnabled"),
) )
} catch {} } catch {}
}} }}

View File

@ -40,7 +40,7 @@ interface Form {
} }
export default function AliasUsersList({users, queryKey, id}: AliasUsersListProps): ReactElement { export default function AliasUsersList({users, queryKey, id}: AliasUsersListProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["admin-reserved-aliases", "common"])
const {showError, showSuccess} = useErrorSuccessSnacks() const {showError, showSuccess} = useErrorSuccessSnacks()
const {mutateAsync} = useMutation< const {mutateAsync} = useMutation<
ReservedAlias, ReservedAlias,
@ -73,7 +73,7 @@ export default function AliasUsersList({users, queryKey, id}: AliasUsersListProp
} }
}, },
onSuccess: async newAlias => { onSuccess: async newAlias => {
showSuccess(t("relations.alias.mutations.success.aliasUpdated")) showSuccess(t("messages.alias.updated", {ns: "common"}))
await queryClient.cancelQueries(queryKey) await queryClient.cancelQueries(queryKey)
@ -102,7 +102,7 @@ export default function AliasUsersList({users, queryKey, id}: AliasUsersListProp
}), }),
}), }),
) )
.label(t("routes.AliasDetailRoute.sections.users.fields.users.label")), .label(t("fields.users.label")),
}) })
const initialValues: Form = { const initialValues: Form = {
users: users, users: users,
@ -126,7 +126,7 @@ export default function AliasUsersList({users, queryKey, id}: AliasUsersListProp
<Grid container spacing={1} direction="row"> <Grid container spacing={1} direction="row">
<Grid item> <Grid item>
<Typography variant="h6" component="h3"> <Typography variant="h6" component="h3">
{t("routes.ReservedAliasDetailRoute.sections.users.title")} {t("fields.users.label")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>

View File

@ -1,11 +1,11 @@
import {ReactElement} from "react" import {ReactElement} from "react"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import {Container, Grid, Typography} from "@mui/material"
import {BsStarFill} from "react-icons/bs" import {BsStarFill} from "react-icons/bs"
import {Container, Grid, Typography} from "@mui/material"
export default function EmptyStateScreen(): ReactElement { export default function EmptyStateScreen(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("admin-reserved-aliases")
return ( return (
<Container maxWidth="xs"> <Container maxWidth="xs">
@ -20,16 +20,14 @@ export default function EmptyStateScreen(): ReactElement {
> >
<Grid item> <Grid item>
<Typography variant="h6" component="h2"> <Typography variant="h6" component="h2">
{t("routes.ReservedAliasesRoute.emptyState.title")} {t("emptyState.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<BsStarFill size={40} /> <BsStarFill size={40} />
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body1"> <Typography variant="body1">{t("emptyState.description")}</Typography>
{t("routes.ReservedAliasesRoute.emptyState.description")}
</Typography>
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>

View File

@ -17,11 +17,11 @@ export interface Delete2FAProps {
} }
export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement { export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("settings-2fa")
const {showSuccess, showError} = useErrorSuccessSnacks() const {showSuccess, showError} = useErrorSuccessSnacks()
const {mutate} = useMutation<SimpleDetailResponse, AxiosError, Delete2FAData>(delete2FA, { const {mutate} = useMutation<SimpleDetailResponse, AxiosError, Delete2FAData>(delete2FA, {
onSuccess: () => { onSuccess: () => {
showSuccess(t("routes.SettingsRoute.2fa.delete.success")) showSuccess(t("delete.success"))
onSuccess() onSuccess()
}, },
onError: showError, onError: showError,
@ -36,7 +36,7 @@ export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement {
case "showAction": case "showAction":
return ( return (
<Button onClick={() => setView("askType")} startIcon={<BsShieldLockFill />}> <Button onClick={() => setView("askType")} startIcon={<BsShieldLockFill />}>
{t("routes.SettingsRoute.2fa.delete.showAction")} {t("delete.title")}
</Button> </Button>
) )
@ -45,7 +45,7 @@ export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement {
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item> <Grid item>
<Button onClick={() => setView("askCode")} startIcon={<BsPhone />}> <Button onClick={() => setView("askCode")} startIcon={<BsPhone />}>
{t("routes.SettingsRoute.2fa.delete.askType.code")} {t("delete.steps.askType.code")}
</Button> </Button>
</Grid> </Grid>
<Grid item> <Grid item>
@ -53,7 +53,7 @@ export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement {
onClick={() => setView("askRecoveryCode")} onClick={() => setView("askRecoveryCode")}
startIcon={<MdSettingsBackupRestore />} startIcon={<MdSettingsBackupRestore />}
> >
{t("routes.SettingsRoute.2fa.delete.askType.recoveryCode")} {t("delete.steps.askType.recoveryCode")}
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>
@ -65,7 +65,7 @@ export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement {
<Grid item> <Grid item>
<TextField <TextField
fullWidth fullWidth
label={t("routes.SettingsRoute.2fa.delete.askCode.label")} label={t("delete.steps.askCode.label")}
value={value} value={value}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
/> />
@ -76,7 +76,7 @@ export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement {
variant="contained" variant="contained"
startIcon={<BsShieldLockFill />} startIcon={<BsShieldLockFill />}
> >
{t("routes.SettingsRoute.2fa.delete.submit")} {t("delete.continueActionLabel")}
</LoadingButton> </LoadingButton>
</Grid> </Grid>
</Grid> </Grid>
@ -88,7 +88,7 @@ export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement {
<Grid item> <Grid item>
<TextField <TextField
fullWidth fullWidth
label={t("routes.SettingsRoute.2fa.delete.askRecoveryCode.label")} label={t("delete.steps.askRecoveryCode.label")}
value={value} value={value}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
/> />
@ -99,7 +99,7 @@ export default function Delete2FA({onSuccess}: Delete2FAProps): ReactElement {
variant="contained" variant="contained"
startIcon={<BsShieldLockFill />} startIcon={<BsShieldLockFill />}
> >
{t("routes.SettingsRoute.2fa.delete.submit")} {t("delete.continueActionLabel")}
</LoadingButton> </LoadingButton>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -18,7 +18,7 @@ export interface Setup2FAProps {
} }
export default function Setup2FA({onSuccess}: Setup2FAProps): ReactElement { export default function Setup2FA({onSuccess}: Setup2FAProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("settings-2fa")
const {showError} = useErrorSuccessSnacks() const {showError} = useErrorSuccessSnacks()
const { const {
@ -33,9 +33,7 @@ export default function Setup2FA({onSuccess}: Setup2FAProps): ReactElement {
return ( return (
<Grid container spacing={4} direction="column"> <Grid container spacing={4} direction="column">
<Grid item> <Grid item>
<Typography variant="body1"> <Typography variant="body1">{t("setup.description")}</Typography>
{t("routes.SettingsRoute.2fa.setup.description")}
</Typography>
</Grid> </Grid>
<Grid item alignSelf="center"> <Grid item alignSelf="center">
{secret ? ( {secret ? (
@ -55,7 +53,7 @@ export default function Setup2FA({onSuccess}: Setup2FAProps): ReactElement {
variant="contained" variant="contained"
startIcon={<BsShieldLockFill />} startIcon={<BsShieldLockFill />}
> >
{t("routes.SettingsRoute.2fa.setup.setupLabel")} {t("setup.setupLabel")}
</LoadingButton> </LoadingButton>
)} )}
</Grid> </Grid>

View File

@ -41,7 +41,7 @@ export default function Settings2FARoute({
onRecreateRequired, onRecreateRequired,
secret, secret,
}: VerifyOTPFormProps): ReactElement { }: VerifyOTPFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["settings-2fa", "common"])
const {showSuccess, showError} = useErrorSuccessSnacks() const {showSuccess, showError} = useErrorSuccessSnacks()
const user = useUser() const user = useUser()
const theme = useTheme() const theme = useTheme()
@ -52,16 +52,19 @@ export default function Settings2FARoute({
code: yup code: yup
.string() .string()
.required() .required()
.matches(
/^[0-9]+$/,
t("fields.2faCode.errors.shouldOnlyBeDigits", {ns: "common"}) as string,
)
.length(6) .length(6)
.matches(/^[0-9]+$/, t("routes.SettingsRoute.2fa.setup.code.onlyDigits").toString()) .label(t("fields.2faCode.label", {ns: "common"})),
.label(t("routes.SettingsRoute.2fa.setup.code.label")),
}) })
const {mutateAsync} = useMutation<void, AxiosError, Verify2FASetupData>(verify2FASetup, { const {mutateAsync} = useMutation<void, AxiosError, Verify2FASetupData>(verify2FASetup, {
onSuccess: () => setShowRecoveryCodes(true), onSuccess: () => setShowRecoveryCodes(true),
onError: error => { onError: error => {
if (error.response?.status === 409 || error.response?.status === 410) { if (error.response?.status === 409 || error.response?.status === 410) {
showError(t("routes.SettingsRoute.2fa.setup.expired").toString()) showError(t("setup.codeExpired").toString())
onRecreateRequired() onRecreateRequired()
} else { } else {
showError(error) showError(error)
@ -107,7 +110,7 @@ export default function Settings2FARoute({
error={!!formik.errors.code} error={!!formik.errors.code}
helperText={formik.errors.code} helperText={formik.errors.code}
name="code" name="code"
label={t("routes.SettingsRoute.2fa.setup.code.label")} label={t("fields.2faCode.label", {ns: "common"})}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
@ -125,7 +128,7 @@ export default function Settings2FARoute({
variant="contained" variant="contained"
loading={formik.isSubmitting} loading={formik.isSubmitting}
> >
{t("routes.SettingsRoute.2fa.setup.submit")} {t("setup.continueActionLabel")}
</LoadingButton> </LoadingButton>
</Grid> </Grid>
</Grid> </Grid>
@ -133,7 +136,7 @@ export default function Settings2FARoute({
</Grid> </Grid>
</form> </form>
<Dialog open={showRecoveryCodes}> <Dialog open={showRecoveryCodes}>
<DialogTitle>{t("routes.SettingsRoute.2fa.setup.recoveryCodes.title")}</DialogTitle> <DialogTitle>{t("setup.recoveryCodes.title")}</DialogTitle>
<DialogContent <DialogContent
sx={{ sx={{
background: theme.palette.background.default, background: theme.palette.background.default,
@ -144,20 +147,18 @@ export default function Settings2FARoute({
<p key={code}>{code}</p> <p key={code}>{code}</p>
))} ))}
</code> </code>
<Alert severity="warning"> <Alert severity="warning">{t("setup.recoveryCodes.description")}</Alert>
{t("routes.SettingsRoute.2fa.setup.recoveryCodes.description")}
</Alert>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
variant="contained" variant="contained"
onClick={() => { onClick={() => {
showSuccess(t("routes.SettingsRoute.2fa.setup.success")) showSuccess(t("setup.success"))
setShowRecoveryCodes(false) setShowRecoveryCodes(false)
onSuccess() onSuccess()
}} }}
> >
{t("routes.SettingsRoute.2fa.setup.recoveryCodes.submit")} {t("setup.recoveryCodes.continueActionLabel")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -12,6 +12,7 @@ import {
DialogTitle, DialogTitle,
Grid, Grid,
} from "@mui/material" } from "@mui/material"
import {useTranslation} from "react-i18next"
export interface DetectEmailAutofillServiceProps { export interface DetectEmailAutofillServiceProps {
domains: string[] domains: string[]
@ -32,6 +33,8 @@ const STORAGE_KEY = "has-shown-email-autofill-service"
export default function DetectEmailAutofillService({ export default function DetectEmailAutofillService({
domains, domains,
}: DetectEmailAutofillServiceProps): ReactElement { }: DetectEmailAutofillServiceProps): ReactElement {
const {t} = useTranslation("relay-service-detected")
const $hasDetected = useRef<boolean>(false) const $hasDetected = useRef<boolean>(false)
const [type, setType] = useState<AliasType | null>(null) const [type, setType] = useState<AliasType | null>(null)
@ -100,19 +103,14 @@ export default function DetectEmailAutofillService({
return ( return (
<Dialog open={type !== null} onClose={() => setType(null)}> <Dialog open={type !== null} onClose={() => setType(null)}>
<DialogTitle>Email relay service detected</DialogTitle> <DialogTitle>{t("title")}</DialogTitle>
<DialogContent> <DialogContent>
<Grid container spacing={2} justifyContent="center"> <Grid container spacing={2} justifyContent="center">
<Grid item> <Grid item>
<DialogContentText> <DialogContentText>{t("description")}</DialogContentText>
We detected that you are using an email relay service to sign up. This
KleckRelay instance does not support relaying to another email relay
service. You can either choose a different instance or sign up with a
different email address.
</DialogContentText>
</Grid> </Grid>
<Grid item> <Grid item>
<DialogContentText>Detected email relay:</DialogContentText> <DialogContentText>{t("detectedExplanation")}</DialogContentText>
</Grid> </Grid>
<Grid item> <Grid item>
<Alert severity="info">{TYPE_NAME_MAP[type!]}</Alert> <Alert severity="info">{TYPE_NAME_MAP[type!]}</Alert>
@ -121,7 +119,7 @@ export default function DetectEmailAutofillService({
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button autoFocus startIcon={<MdCheck />} onClick={() => setType(null)}> <Button autoFocus startIcon={<MdCheck />} onClick={() => setType(null)}>
Got it {t("closeActionLabel")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -27,7 +27,7 @@ interface Form {
} }
export default function EmailForm({onSignUp, serverSettings}: EmailFormProps): ReactElement { export default function EmailForm({onSignUp, serverSettings}: EmailFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["signup", "common"])
const $password = useRef<HTMLInputElement | null>(null) const $password = useRef<HTMLInputElement | null>(null)
const schema = yup.object().shape({ const schema = yup.object().shape({
@ -35,7 +35,7 @@ export default function EmailForm({onSignUp, serverSettings}: EmailFormProps): R
.string() .string()
.email() .email()
.required() .required()
.label(t("routes.SignupRoute.forms.email.form.email.label")), .label(t("fields.email.label", {ns: "common"})),
}) })
const {mutateAsync} = useMutation<SignupResult, AxiosError, string>(signup, { const {mutateAsync} = useMutation<SignupResult, AxiosError, string>(signup, {
@ -53,13 +53,13 @@ export default function EmailForm({onSignUp, serverSettings}: EmailFormProps): R
if (isDisposable) { if (isDisposable) {
setErrors({ setErrors({
email: "Disposable email addresses are not allowed", email: t("fields.email.errors.disposable", {ns: "common"}),
}) })
return return
} }
} catch { } catch {
setErrors({ setErrors({
detail: "An error occurred", detail: t("messages.errors.unknown", {ns: "common"}),
}) })
return return
} }
@ -82,9 +82,9 @@ export default function EmailForm({onSignUp, serverSettings}: EmailFormProps): R
<MultiStepFormElement> <MultiStepFormElement>
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<SimpleForm <SimpleForm
title={t("routes.SignupRoute.forms.email.title")} title={t("forms.email.title")}
description={t("routes.SignupRoute.forms.email.description")} description={t("forms.email.description")}
continueActionLabel={t("routes.SignupRoute.forms.email.continueAction")} continueActionLabel={t("forms.email.continueActionLabel")}
nonFieldError={formik.errors.detail} nonFieldError={formik.errors.detail}
isSubmitting={formik.isSubmitting} isSubmitting={formik.isSubmitting}
> >
@ -95,10 +95,8 @@ export default function EmailForm({onSignUp, serverSettings}: EmailFormProps): R
autoFocus autoFocus
name="email" name="email"
id="email" id="email"
label={t("routes.SignupRoute.forms.email.form.email.label")} label={t("fields.email.label", {ns: "common"})}
placeholder={t( placeholder={t("fields.email.placeholder", {ns: "common"})}
"routes.SignupRoute.forms.email.form.email.placeholder",
)}
inputMode="email" inputMode="email"
inputRef={$password} inputRef={$password}
value={formik.values.email} value={formik.values.email}

View File

@ -19,7 +19,7 @@ export default function ResendMailButton({
email, email,
onEmailAlreadyVerified, onEmailAlreadyVerified,
}: ResendMailButtonProps): ReactElement { }: ResendMailButtonProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("components")
const settings = useLoaderData() as ServerSettings const settings = useLoaderData() as ServerSettings
const mutation = useMutation<ResendEmailVerificationCodeResponse, AxiosError, void>( const mutation = useMutation<ResendEmailVerificationCodeResponse, AxiosError, void>(
@ -41,7 +41,7 @@ export default function ResendMailButton({
startIcon={<MdMail />} startIcon={<MdMail />}
onClick={() => mutate()} onClick={() => mutate()}
> >
{t("components.ResendMailButton.label")} {t("ResendMailButton.label")}
</TimedButton> </TimedButton>
<MutationStatusSnackbar mutation={mutation} /> <MutationStatusSnackbar mutation={mutation} />
</> </>

View File

@ -23,7 +23,7 @@ export interface YouGotMailProps {
} }
export default function YouGotMail({email, onGoBack}: YouGotMailProps): ReactElement { export default function YouGotMail({email, onGoBack}: YouGotMailProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["signup", "common"])
const [askToEditEmail, setAskToEditEmail] = useState<boolean>(false) const [askToEditEmail, setAskToEditEmail] = useState<boolean>(false)
@ -43,12 +43,12 @@ export default function YouGotMail({email, onGoBack}: YouGotMailProps): ReactEle
> >
<Grid item> <Grid item>
<Typography variant="h6" component="h2" align="center"> <Typography variant="h6" component="h2" align="center">
{t("routes.SignupRoute.forms.mailVerification.title")} {t("forms.mailVerification.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="subtitle1" component="p"> <Typography variant="subtitle1" component="p">
{t("routes.SignupRoute.forms.mailVerification.description")} {t("forms.mailVerification.description")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
@ -72,20 +72,18 @@ export default function YouGotMail({email, onGoBack}: YouGotMailProps): ReactEle
</Grid> </Grid>
</MultiStepFormElement> </MultiStepFormElement>
<Dialog open={askToEditEmail}> <Dialog open={askToEditEmail}>
<DialogTitle> <DialogTitle>{t("forms.mailVerification.editEmail.title")}</DialogTitle>
{t("routes.SignupRoute.forms.mailVerification.editEmail.title")}
</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
{t("routes.SignupRoute.forms.mailVerification.editEmail.description")} {t("forms.mailVerification.editEmail.description")}
</DialogContentText> </DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button startIcon={<MdCancel />} onClick={() => setAskToEditEmail(false)}> <Button startIcon={<MdCancel />} onClick={() => setAskToEditEmail(false)}>
{t("general.cancelLabel")} {t("general.cancelLabel", {ns: "common"})}
</Button> </Button>
<Button onClick={onGoBack}> <Button onClick={onGoBack}>
{t("routes.SignupRoute.forms.mailVerification.editEmail.continueAction")} {t("forms.mailVerification.editEmail.continueActionLabel")}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -11,7 +11,7 @@ import {useNavigateToNext, useUser} from "~/hooks"
import ServerStatus from "~/route-widgets/AdminRoute/ServerStatus" import ServerStatus from "~/route-widgets/AdminRoute/ServerStatus"
export default function AdminRoute(): ReactElement { export default function AdminRoute(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("admin")
const navigateToNext = useNavigateToNext() const navigateToNext = useNavigateToNext()
const user = useUser() const user = useUser()
@ -22,20 +22,20 @@ export default function AdminRoute(): ReactElement {
}, [user.isAdmin, navigateToNext]) }, [user.isAdmin, navigateToNext])
return ( return (
<SimplePageBuilder.Page title={t("routes.AdminRoute.title")}> <SimplePageBuilder.Page title={t("title")}>
<ServerStatus /> <ServerStatus />
<List> <List>
<ListItemButton component={Link} to="/admin/reserved-aliases"> <ListItemButton component={Link} to="/admin/reserved-aliases">
<ListItemIcon> <ListItemIcon>
<BsStarFill /> <BsStarFill />
</ListItemIcon> </ListItemIcon>
<ListItemText primary={t("routes.AdminRoute.routes.reservedAliases")} /> <ListItemText primary={t("routes.reservedAliases")} />
</ListItemButton> </ListItemButton>
<ListItemButton component={Link} to="/admin/settings"> <ListItemButton component={Link} to="/admin/settings">
<ListItemIcon> <ListItemIcon>
<AiFillEdit /> <AiFillEdit />
</ListItemIcon> </ListItemIcon>
<ListItemText primary={t("routes.AdminRoute.routes.settings")} /> <ListItemText primary={t("routes.settings")} />
</ListItemButton> </ListItemButton>
</List> </List>
</SimplePageBuilder.Page> </SimplePageBuilder.Page>

View File

@ -25,7 +25,7 @@ import ChangeAliasActivationStatusSwitch from "~/route-widgets/AliasDetailRoute/
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes" import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
export default function AliasDetailRoute(): ReactElement { export default function AliasDetailRoute(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["aliases", "common"])
const serverSettings = useLoaderData() as ServerSettings const serverSettings = useLoaderData() as ServerSettings
const {id: aliasID} = useParams() const {id: aliasID} = useParams()
const {_decryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext) const {_decryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext)
@ -46,17 +46,17 @@ export default function AliasDetailRoute(): ReactElement {
return ( return (
<SimplePage <SimplePage
title={t("routes.AliasDetailRoute.title")} title={t("detailsTitle")}
actions={ actions={
serverSettings.allowAliasDeletion && serverSettings.allowAliasDeletion &&
query.data && ( query.data && (
<DeleteButton <DeleteButton
onDelete={() => deleteAlias(aliasID!)} onDelete={() => deleteAlias(aliasID!)}
label={t("routes.AliasDetailRoute.actions.delete.label")} label={t("actions.delete.label")}
description={t("routes.AliasDetailRoute.actions.delete.description")} description={t("actions.delete.description")}
continueLabel={t("routes.AliasDetailRoute.actions.delete.continueAction")} continueLabel={t("actions.delete.continueActionLabel")}
navigateTo={"/aliases"} navigateTo={"/aliases"}
successMessage={t("relations.alias.mutations.success.aliasDeleted")} successMessage={t("messages.alias.deleted", {ns: "common"})}
/> />
) )
} }
@ -97,10 +97,7 @@ export default function AliasDetailRoute(): ReactElement {
<DecryptionPasswordMissingAlert /> <DecryptionPasswordMissingAlert />
)} )}
</div>, </div>,
<SimplePageBuilder.Section <SimplePageBuilder.Section label={t("settings.title")} key="settings">
label={t("routes.AliasDetailRoute.sections.settings.title")}
key="settings"
>
<AliasPreferencesForm alias={alias} queryKey={queryKey} /> <AliasPreferencesForm alias={alias} queryKey={queryKey} />
</SimplePageBuilder.Section>, </SimplePageBuilder.Section>,
]} ]}

View File

@ -36,7 +36,7 @@ enum TypeFilter {
} }
export default function AliasesRoute(): ReactElement { export default function AliasesRoute(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation(["aliases", "common"])
const [searchValue, setSearchValue] = useState<string>("") const [searchValue, setSearchValue] = useState<string>("")
const [queryValue, setQueryValue] = useState<string>("") const [queryValue, setQueryValue] = useState<string>("")
@ -131,7 +131,7 @@ export default function AliasesRoute(): ReactElement {
return ( return (
<SimplePage <SimplePage
title={t("routes.AliasesRoute.title")} title={t("title")}
pageOptionsActions={ pageOptionsActions={
showSearch && ( showSearch && (
<Grid container spacing={2} direction="column"> <Grid container spacing={2} direction="column">
@ -146,10 +146,8 @@ export default function AliasesRoute(): ReactElement {
setQueryValue(event.target.value) setQueryValue(event.target.value)
}) })
}} }}
label={t("routes.AliasesRoute.pageActions.search.label")} label={t("fields.search.label", {ns: "common"})}
placeholder={t( placeholder={t("pageActions.search.placeholder")}
"routes.AliasesRoute.pageActions.search.placeholder",
)}
id="search" id="search"
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
@ -166,9 +164,7 @@ export default function AliasesRoute(): ReactElement {
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item> <Grid item>
<Chip <Chip
label={t( label={t("pageActions.searchFilter.active")}
"routes.AliasesRoute.pageActions.searchFilter.active",
)}
variant={ variant={
searchFilter === SearchFilter.Active searchFilter === SearchFilter.Active
? "filled" ? "filled"
@ -187,9 +183,7 @@ export default function AliasesRoute(): ReactElement {
</Grid> </Grid>
<Grid item> <Grid item>
<Chip <Chip
label={t( label={t("pageActions.searchFilter.inactive")}
"routes.AliasesRoute.pageActions.searchFilter.inactive",
)}
variant={ variant={
searchFilter === SearchFilter.Inactive searchFilter === SearchFilter.Inactive
? "filled" ? "filled"
@ -213,9 +207,7 @@ export default function AliasesRoute(): ReactElement {
<Grid item> <Grid item>
<Chip <Chip
icon={ALIAS_TYPE_ICON_MAP[AliasType.CUSTOM]} icon={ALIAS_TYPE_ICON_MAP[AliasType.CUSTOM]}
label={t( label={t("pageActions.typeFilter.custom")}
"routes.AliasesRoute.pageActions.typeFilter.custom",
)}
variant={ variant={
typeFilter === TypeFilter.Custom typeFilter === TypeFilter.Custom
? "filled" ? "filled"
@ -235,9 +227,7 @@ export default function AliasesRoute(): ReactElement {
<Grid item> <Grid item>
<Chip <Chip
icon={ALIAS_TYPE_ICON_MAP[AliasType.RANDOM]} icon={ALIAS_TYPE_ICON_MAP[AliasType.RANDOM]}
label={t( label={t("pageActions.typeFilter.random")}
"routes.AliasesRoute.pageActions.typeFilter.random",
)}
variant={ variant={
typeFilter === TypeFilter.Random typeFilter === TypeFilter.Random
? "filled" ? "filled"
@ -307,15 +297,14 @@ export default function AliasesRoute(): ReactElement {
</Grid> </Grid>
<SuccessSnack <SuccessSnack
key={value} key={value}
message={ message={value && t("messages.alias.addressCopied", {ns: "common"})}
value && />
t("relations.alias.mutations.success.addressCopiedToClipboard") <ErrorSnack
} message={error && t("messages.errors.unknown", {ns: "common"})}
/> />
<ErrorSnack message={error && t("general.copyError")} />
<Snackbar open={isInCopyAddressMode} autoHideDuration={null}> <Snackbar open={isInCopyAddressMode} autoHideDuration={null}>
<Alert variant="standard" severity="info"> <Alert variant="standard" severity="info">
{t("routes.AliasesRoute.isInCopyMode")} {t("isInCopyMode")}
</Alert> </Alert>
</Snackbar> </Snackbar>
</> </>

View File

@ -8,7 +8,7 @@ import {Box, Button, Grid} from "@mui/material"
import {LanguageButton} from "~/components" import {LanguageButton} from "~/components"
export default function AuthenticateRoute(): ReactElement { export default function AuthenticateRoute(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("common")
return ( return (
<Box <Box
@ -35,7 +35,7 @@ export default function AuthenticateRoute(): ReactElement {
size="small" size="small"
startIcon={<MdAdd />} startIcon={<MdAdd />}
> >
{t("components.AuthenticateRoute.signup")} {t("routes.signup")}
</Button> </Button>
</Grid> </Grid>
<Grid item> <Grid item>
@ -46,7 +46,7 @@ export default function AuthenticateRoute(): ReactElement {
size="small" size="small"
startIcon={<MdLogin />} startIcon={<MdLogin />}
> >
{t("components.AuthenticateRoute.login")} {t("routes.login")}
</Button> </Button>
</Grid> </Grid>
<Grid item> <Grid item>

View File

@ -12,7 +12,7 @@ import NavigationButton, {
} from "~/route-widgets/AuthenticateRoute/NavigationButton" } from "~/route-widgets/AuthenticateRoute/NavigationButton"
export default function AuthenticatedRoute(): ReactElement { export default function AuthenticatedRoute(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("common")
const theme = useTheme() const theme = useTheme()
const user = useUser() const user = useUser()
@ -90,7 +90,7 @@ export default function AuthenticatedRoute(): ReactElement {
to="/auth/logout" to="/auth/logout"
startIcon={<MdLogout />} startIcon={<MdLogout />}
> >
{t("components.AuthenticatedRoute.logout")} {t("routes.logout")}
</Button> </Button>
</Grid> </Grid>
<Grid item> <Grid item>

View File

@ -9,7 +9,7 @@ import GenerateEmailReportsForm from "~/route-widgets/CompleteAccountRoute/Gener
import PasswordForm from "~/route-widgets/CompleteAccountRoute/PasswordForm" import PasswordForm from "~/route-widgets/CompleteAccountRoute/PasswordForm"
export default function CompleteAccountRoute(): ReactElement { export default function CompleteAccountRoute(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation("complete-account")
const {encryptionStatus} = useContext(AuthContext) const {encryptionStatus} = useContext(AuthContext)
const navigateToNext = useNavigateToNext() const navigateToNext = useNavigateToNext()
@ -50,13 +50,11 @@ export default function CompleteAccountRoute(): ReactElement {
> >
<Grid item> <Grid item>
<Typography variant="h6" component="h1" align="center"> <Typography variant="h6" component="h1" align="center">
{t("routes.CompleteAccountRoute.forms.available.title")} {t("alreadyCompleted.title")}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body1"> <Typography variant="body1">{t("alreadyCompleted.description")}</Typography>
{t("routes.CompleteAccountRoute.forms.available.description")}
</Typography>
</Grid> </Grid>
</Grid> </Grid>
</Paper> </Paper>

Some files were not shown because too many files have changed in this diff Show More