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": {
"cancelLabel": "Abbrechen",
"cancelLabel": "Cancel",
"emptyValue": "-",
"emptyUnavailableValue": "Nicht verfügbar",
"emptyUnavailableValue": "Unavailable",
"saveLabel": "Save",
"defaultValueSelection": "Standard <{{value}}>",
"defaultValueSelection": "Default <{{value}}>",
"defaultValueSelectionRaw": "<{{value}}>",
"booleanSelection": {
"true": "Ja",
"false": "Nein"
"true": "Yes",
"false": "No"
},
"defaultError": "Ein Fehler ist aufgetreten.",
"defaultSuccess": "Erfolgreich übernommen!",
"loading": "Lädt...",
"actionNotUndoable": "Diese Aktion kann nicht rückgängig gemacht werden!",
"copyError": "Konnte nicht in Zwischenable kopieren. Bitte kopiere den Text manuell.",
"experimentalFeature": "Diese Funktion ist experimentell und kann Fehler verursachen."
},
"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..."
}
"defaultError": "An error occurred.",
"defaultSuccess": "Success!",
"loading": "Loading...",
"actionNotUndoable": "This action cannot be undone!",
"copyError": "Copying to clipboard did not work. Please copy the text manually.",
"experimentalFeature": "This is an experimental feature.",
"deletedSuccessfully": "Deleted successfully!",
"appError": "We are sorry but there was an error. Please try again later."
},
"components": {
"NavigationButton": {
"overview": "Überblick",
"aliases": "Aliase",
"reports": "Berichte",
"settings": "Einstellungen"
},
"AuthenticateRoute": {
"signup": "Registrieren",
"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>"
}
"passwordShareConfirmationDialog": {
"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.",
"continueAction": "Share Password",
"doNotShare": "Do not share",
"decideLater": "Decide later",
"doNotAskAgain": "Do not ask again"
}
}
}

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."
},
"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": {
"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": {
"title": "Share Password?",
"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",
"decideLater": "Decide later",
"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,
onClose,
}: PasswordShareConfirmationDialogProps): ReactElement {
const {t} = useTranslation()
const {t} = useTranslation("extension")
return (
<Dialog open={open} onClose={() => onClose(false)} maxWidth="sm" fullWidth={false}>
<DialogTitle>{t("components.passwordShareConfirmationDialog.title")}</DialogTitle>
<DialogTitle>{t("sharePassword.title")}</DialogTitle>
<DialogContent>
<DialogContentText>
{t("components.passwordShareConfirmationDialog.description")}
</DialogContentText>
<DialogContentText>{t("sharePassword.description")}</DialogContentText>
<Box my={2}>
<Alert severity="warning">
{t("components.passwordShareConfirmationDialog.warning")}
</Alert>
<Alert severity="warning">{t("sharePassword.warning")}</Alert>
</Box>
</DialogContent>
<DialogActions>
<Box mr="auto">
<Button startIcon={<MdAccessTimeFilled />} onClick={() => onClose(false)}>
{t("components.passwordShareConfirmationDialog.decideLater")}
{t("sharePassword.decideLater")}
</Button>
</Box>
<Button startIcon={<TiCancel />} onClick={() => onClose(true)}>
{t("components.passwordShareConfirmationDialog.doNotShare")}
{t("sharePassword.doNotAskAgain")}
</Button>
<Button color="error" onClick={onShare} startIcon={<MdShield />}>
{t("components.passwordShareConfirmationDialog.continueAction")}
{t("sharePassword.sharePassword")}
</Button>
</DialogActions>
</Dialog>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,14 +12,14 @@ export interface OpenMailButtonProps {
}
export default function OpenMailButton({domain}: OpenMailButtonProps): ReactElement {
const {t} = useTranslation()
const {t} = useTranslation("components")
const userAgent = new UAParser()
if (userAgent.getOS().name === "Android" && APP_LINK_MAP[domain]) {
return (
<Button startIcon={<IoMdMailOpen />} variant="text" href={APP_LINK_MAP[domain].android}>
{t("components.OpenMailButton.label")}
{t("OpenMailButton.label")}
</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 {LoadingButton} from "@mui/lab"
import {OverrideProps} from "@mui/types"
import {useTranslation} from "react-i18next"
export interface SimpleFormProps {
title: string
description: string
continueActionLabel: string
continueActionLabel?: string
children?: ReactElement[]
cancelActionLabel?: string
isSubmitting?: boolean
@ -32,6 +33,8 @@ export default function SimpleForm({
titleComponent = "h1",
isSubmitting = false,
}: SimpleFormProps): ReactElement {
const {t} = useTranslation("common")
const [showSnackbar, setShowSnackbar] = useState<boolean>(false)
useEffect(() => {
@ -101,7 +104,7 @@ export default function SimpleForm({
type="submit"
startIcon={<MdChevronRight />}
>
{continueActionLabel}
{continueActionLabel || t("general.continueLabel")}
</LoadingButton>
</Grid>
</Grid>

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ i18n.use(HttpApi)
.init({
debug: isDev,
fallbackLng: "en-US",
load: "all",
backend: {
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
function ServerStatus(): ReactElement | null {
const {t} = useTranslation("admin")
const serverSettings = useLoaderData() as ServerSettings
const {t} = useTranslation()
const {_decryptUsingPrivateKey} = useContext(AuthContext)
const query = useQuery<CronReport | null, AxiosError>(["get_latest_cron_report"], async () => {
@ -50,7 +50,7 @@ function ServerStatus(): ReactElement | null {
if (report.createdAt < thresholdDate) {
return (
<Alert severity="warning">
{t("routes.AdminRoute.serverStatus.noRecentReports", {
{t("serverStatus.noRecentReports", {
date: format(new Date(report.createdAt), "Pp"),
})}
</Alert>
@ -60,7 +60,7 @@ function ServerStatus(): ReactElement | null {
if (report.reportData.report.status === "error") {
return (
<Alert severity="error">
{t("routes.AdminRoute.serverStatus.error", {
{t("serverStatus.error", {
relativeDescription: formatRelative(
new Date(report.createdAt),
new Date(),
@ -72,7 +72,7 @@ function ServerStatus(): ReactElement | null {
return (
<Alert severity="success">
{t("routes.AdminRoute.serverStatus.success", {
{t("serverStatus.success", {
relativeDescription: formatRelative(
new Date(report.createdAt),
new Date(),

View File

@ -18,14 +18,14 @@ interface WebsiteForm {
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 {
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>({
validationSchema: WEBSITE_SCHEMA,
validationSchema: schema,
initialValues: {
url: "",
},
@ -58,10 +58,8 @@ export default function AddWebsiteField({onAdd, isLoading}: AddWebsiteFieldProps
<TextField
name="url"
id="url"
label={t("routes.AliasDetailRoute.sections.notes.form.websites.label")}
placeholder={t(
"routes.AliasDetailRoute.sections.notes.form.websites.placeholder",
)}
label={t("form.websites.label")}
placeholder={t("form.websites.placeholder")}
variant="outlined"
value={websiteFormik.values.url}
onChange={websiteFormik.handleChange}
@ -93,7 +91,7 @@ export default function AddWebsiteField({onAdd, isLoading}: AddWebsiteFieldProps
error={websiteFormik.touched.url && Boolean(websiteFormik.errors.url)}
>
{(websiteFormik.touched.url && websiteFormik.errors.url) ||
t("routes.AliasDetailRoute.sections.notes.form.websites.helperText")}
t("form.websites.helperText")}
</FormHelperText>
</Grid>
</Grid>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,21 @@
import * as yup from "yup"
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 {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 {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 {Link as RouterLink} from "react-router-dom"
import {ServerUser} from "~/server-types"
interface Form {
code: string
@ -30,7 +32,7 @@ export default function OTPForm({
onConfirm,
onCodeUnavailable,
}: OTPFormProps): ReactElement {
const {t} = useTranslation()
const {t} = useTranslation(["login", "common"])
const {showError} = useErrorSuccessSnacks()
const {mutateAsync} = useMutation<ServerUser, AxiosError, string>(
code =>
@ -42,7 +44,7 @@ export default function OTPForm({
onSuccess: onConfirm,
onError: error => {
if (error.response?.status === 410 || error.response?.status === 404) {
showError(t("routes.LoginRoute.forms.otp.unavailable").toString())
showError(t("forms.otp.isUnavailable").toString())
onCodeUnavailable()
}
},
@ -50,7 +52,10 @@ export default function OTPForm({
)
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>({
@ -81,7 +86,7 @@ export default function OTPForm({
>
<Grid item>
<Typography variant="h6" component="h1" align="center">
{t("routes.LoginRoute.forms.otp.title")}
{t("forms.otp.title")}
</Typography>
</Grid>
<Grid item>
@ -91,7 +96,7 @@ export default function OTPForm({
</Grid>
<Grid item>
<Typography variant="subtitle1" component="p" align="center">
{t("routes.LoginRoute.forms.otp.description")}
{t("forms.otp.description")}
</Typography>
</Grid>
<Grid item>
@ -101,7 +106,8 @@ export default function OTPForm({
fullWidth
name="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}
onChange={formik.handleChange}
disabled={formik.isSubmitting}
@ -125,12 +131,12 @@ export default function OTPForm({
type="submit"
startIcon={<MdChevronRight />}
>
{t("routes.LoginRoute.forms.otp.submit")}
{t("forms.otp.continueActionLabel")}
</LoadingButton>
</Grid>
<Grid item>
<Button component={RouterLink} to="/auth/recover-2fa">
{t("routes.LoginRoute.forms.otp.lost")}
{t("forms.otp.codesLostActionLabel")}
</Button>
</Grid>
</Grid>

View File

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

View File

@ -17,14 +17,14 @@ export interface ProxiedImagesListItemProps {
}
export default function ProxiedImagesListItem({images}: ProxiedImagesListItemProps): ReactElement {
const {t} = useTranslation()
const {t} = useTranslation("reports")
const serverSettings = useLoaderData() as ServerSettings
return (
<ExpandableListItem
data={images}
icon={<BsImage />}
title={t("routes.ReportDetailRoute.sections.trackers.results.proxiedImages.text", {
title={t("sections.trackers.results.proxiedImages.text", {
count: images.length,
})}
>
@ -62,11 +62,11 @@ export default function ProxiedImagesListItem({images}: ProxiedImagesListItemPro
)
) {
return t(
"routes.ReportDetailRoute.sections.trackers.results.proxiedImages.status.isStored",
"sections.trackers.results.proxiedImages.status.isStored",
)
} else {
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({
images,
}: SinglePixelImageTrackersListItemProps): ReactElement {
const {t} = useTranslation()
const {t} = useTranslation("reports")
const imagesPerTracker = images.reduce((acc, value) => {
acc[value.trackerName] = [...(acc[value.trackerName] || []), value]
@ -26,7 +26,7 @@ export default function SinglePixelImageTrackersListItem({
<ExpandableListItem
data={images}
icon={<BsShieldShaded />}
title={t("routes.ReportDetailRoute.sections.trackers.results.imageTrackers.text", {
title={t("sections.trackers.results.imageTrackers.text", {
count: images.length,
})}
>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import {
DialogTitle,
Grid,
} from "@mui/material"
import {useTranslation} from "react-i18next"
export interface DetectEmailAutofillServiceProps {
domains: string[]
@ -32,6 +33,8 @@ const STORAGE_KEY = "has-shown-email-autofill-service"
export default function DetectEmailAutofillService({
domains,
}: DetectEmailAutofillServiceProps): ReactElement {
const {t} = useTranslation("relay-service-detected")
const $hasDetected = useRef<boolean>(false)
const [type, setType] = useState<AliasType | null>(null)
@ -100,19 +103,14 @@ export default function DetectEmailAutofillService({
return (
<Dialog open={type !== null} onClose={() => setType(null)}>
<DialogTitle>Email relay service detected</DialogTitle>
<DialogTitle>{t("title")}</DialogTitle>
<DialogContent>
<Grid container spacing={2} justifyContent="center">
<Grid item>
<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>
<DialogContentText>{t("description")}</DialogContentText>
</Grid>
<Grid item>
<DialogContentText>Detected email relay:</DialogContentText>
<DialogContentText>{t("detectedExplanation")}</DialogContentText>
</Grid>
<Grid item>
<Alert severity="info">{TYPE_NAME_MAP[type!]}</Alert>
@ -121,7 +119,7 @@ export default function DetectEmailAutofillService({
</DialogContent>
<DialogActions>
<Button autoFocus startIcon={<MdCheck />} onClick={() => setType(null)}>
Got it
{t("closeActionLabel")}
</Button>
</DialogActions>
</Dialog>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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