diff --git a/src/resources/js/app.js b/src/resources/js/app.js --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -193,16 +193,8 @@ // we can't really use router to display error page as it has two side // effects: it changes the URL and adds the error page to browser history. // For now we'll be replacing current view with error page "manually". - const map = { - 400: "Bad request", - 401: "Unauthorized", - 403: "Access denied", - 404: "Not found", - 405: "Method not allowed", - 500: "Internal server error" - } - if (!msg) msg = map[code] || "Unknown Error" + if (!msg) msg = this.$te('error.' + code) ? this.$t('error.' + code) : this.$t('error.unknown') if (!hint) hint = '' const error_page = '
' @@ -255,6 +247,7 @@ }) }, price(price, currency) { + // TODO: Set locale argument according to the currently used locale return ((price || 0) / 100).toLocaleString('de-DE', { style: 'currency', currency: currency || 'CHF' }) }, priceLabel(cost, discount) { @@ -292,18 +285,18 @@ }, domainStatusText(domain) { if (domain.isDeleted) { - return 'Deleted' + return this.$t('status.deleted') } if (domain.isSuspended) { - return 'Suspended' + return this.$t('status.suspended') } if (!domain.isVerified || !domain.isLdapReady || !domain.isConfirmed) { - return 'Not Ready' + return this.$t('status.notready') } - return 'Active' + return this.$t('status.active') }, distlistStatusClass(list) { if (list.isDeleted) { @@ -322,18 +315,18 @@ }, distlistStatusText(list) { if (list.isDeleted) { - return 'Deleted' + return this.$t('status.deleted') } if (list.isSuspended) { - return 'Suspended' + return this.$t('status.suspended') } if (!list.isLdapReady) { - return 'Not Ready' + return this.$t('status.notready') } - return 'Active' + return this.$t('status.active') }, pageName(path) { let page = this.$route.path @@ -356,6 +349,7 @@ // FIXME: Find a nicer way of doing this if (!dialog.length) { + SupportForm.i18n = i18n let form = new Vue(SupportForm) form.$mount($('
').appendTo(container)[0]) form.$root = this @@ -384,18 +378,18 @@ }, userStatusText(user) { if (user.isDeleted) { - return 'Deleted' + return this.$t('status.deleted') } if (user.isSuspended) { - return 'Suspended' + return this.$t('status.suspended') } if (!user.isImapReady || !user.isLdapReady) { - return 'Not Ready' + return this.$t('status.notready') } - return 'Active' + return this.$t('status.active') }, updateBodyClass(name) { // Add 'class' attribute to the body, different for each page @@ -514,7 +508,7 @@ error_msg = error.request ? error.request.statusText : error.message } - app.$toast.error(error_msg || "Server Error") + app.$toast.error(error_msg || app.$t('error.server')) // Pass the error as-is return Promise.reject(error) diff --git a/src/resources/lang/de/ui.php b/src/resources/lang/de/ui.php --- a/src/resources/lang/de/ui.php +++ b/src/resources/lang/de/ui.php @@ -2,7 +2,7 @@ return [ - 'button' => [ + 'btn' => [ 'cancel' => "Stornieren", 'save' => "Speichern", ], diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -8,19 +8,46 @@ return [ - 'button' => [ + 'app' => [ + 'faq' => "FAQ", + ], + + 'btn' => [ + 'add' => "Add", 'accept' => "Accept", 'back' => "Back", 'cancel' => "Cancel", 'close' => "Close", 'continue' => "Continue", + 'delete' => "Delete", 'deny' => "Deny", + 'download' => "Download", + 'edit' => "Edit", + 'file' => "Choose file...", + 'moreinfo' => "More information", + 'refresh' => "Refresh", + 'reset' => "Reset", + 'resend' => "Resend", 'save' => "Save", + 'search' => "Search", + 'signup' => "Sign Up", 'submit' => "Submit", + 'suspend' => "Suspend", + 'unsuspend' => "Unsuspend", + 'verify' => "Verify", ], 'dashboard' => [ 'beta' => "beta", + 'distlists' => "Distribution lists", + 'chat' => "Video chat", + 'domains' => "Domains", + 'invitations' => "Invitations", + 'profile' => "Your profile", + 'users' => "User accounts", + 'wallet' => "Wallet", + 'webmail' => "Webmail", + 'stats' => "Stats", ], 'distlist' => [ @@ -33,13 +60,72 @@ 'recipients' => "Recipients", ], + 'domain' => [ + 'dns-verify' => "Domain DNS verification sample:", + 'dns-config' => "Domain DNS configuration sample:", + 'namespace' => "Namespace", + 'verify' => "Domain verification", + 'verify-intro' => "In order to confirm that you're the actual holder of the domain, we need to run a verification process before finally activating it for email delivery.", + 'verify-dns' => "The domain must have one of the following entries in DNS:", + 'verify-dns-txt' => "TXT entry with value:", + 'verify-dns-cname' => "or CNAME entry:", + 'verify-outro' => "When this is done press the button below to start the verification.", + 'verify-sample' => "Here's a sample zone file for your domain:", + 'config' => "Domain configuration", + 'config-intro' => "In order to let {app} receive email traffic for your domain you need to adjust the DNS settings, more precisely the MX entries, accordingly.", + 'config-sample' => "Edit your domain's zone file and replace existing MX entries with the following values:", + 'config-hint' => "If you don't know how to set DNS entries for your domain, please contact the registration service where you registered the domain or your web hosting provider.", + ], + + 'error' => [ + '400' => "Bad request", + '401' => "Unauthorized", + '403' => "Access denied", + '404' => "Not found", + '405' => "Method not allowed", + '500' => "Internal server error", + 'unknown' => "Unknown Error", + 'server' => "Server Error", + ], + 'form' => [ + 'amount' => "Amount", 'code' => "Confirmation Code", + 'config' => "Configuration", + 'date' => "Date", + 'description' => "Description", + 'details' => "Details", + 'domain' => "Domain", 'email' => "Email Address", + 'firstname' => "First Name", + 'lastname' => "Last Name", 'none' => "none", + 'or' => "or", 'password' => "Password", 'password-confirm' => "Confirm Password", + 'phone' => "Phone", 'status' => "Status", + 'surname' => "Surname", + 'user' => "User", + 'primary-email' => "Primary Email", + 'id' => "ID", + 'created' => "Created", + 'deleted' => "Deleted", + ], + + 'invitation' => [ + 'create' => "Create invite(s)", + 'create-title' => "Invite for a signup", + 'create-email' => "Enter an email address of the person you want to invite.", + 'create-csv' => "To send multiple invitations at once, provide a CSV (comma separated) file, or alternatively a plain-text file, containing one email address per line.", + 'empty-list' => "There are no invitations in the database.", + 'title' => "Signup invitations", + 'search' => "Email address or domain", + 'send' => "Send invite(s)", + 'status-completed' => "User signed up", + 'status-failed' => "Sending failed", + 'status-sent' => "Sent", + 'status-new' => "Not sent yet", ], 'lang' => [ @@ -159,11 +245,18 @@ ], 'msg' => [ + 'initializing' => "Initializing...", 'loading' => "Loading...", + 'loading-failed' => "Failed to load data.", 'notfound' => "Resource not found.", + 'info' => "Information", + 'error' => "Error", + 'warning' => "Warning", + 'success' => "Success", ], 'nav' => [ + 'more' => "Load more", 'step' => "Step {i}/{n}", ], @@ -175,4 +268,140 @@ . " Enter the code we sent you, or click the link in the message.", ], + 'signup' => [ + 'email' => "Existing Email Address", + 'login' => "Login", + 'title' => "Sign Up", + 'step1' => "Sign up to start your free month.", + 'step2' => "We sent out a confirmation code to your email address. Enter the code we sent you, or click the link in the message.", + 'step3' => "Create your Kolab identity (you can choose additional addresses later).", + 'voucher' => "Voucher Code", + ], + + 'status' => [ + 'prepare-account' => "We are preparing your account.", + 'prepare-domain' => "We are preparing the domain.", + 'prepare-distlist' => "We are preparing the distribution list.", + 'prepare-user' => "We are preparing the user account.", + 'prepare-hint' => "Some features may be missing or readonly at the moment.", + 'prepare-refresh' => "The process never ends? Press the \"Refresh\" button, please.", + 'ready-account' => "Your account is almost ready.", + 'ready-domain' => "The domain is almost ready.", + 'ready-distlist' => "The distribution list is almost ready.", + 'ready-user' => "The user account is almost ready.", + 'verify' => "Verify your domain to finish the setup process.", + 'verify-domain' => "Verify domain", + 'deleted' => "Deleted", + 'suspended' => "Suspended", + 'notready' => "Not Ready", + 'active' => "Active", + ], + + 'support' => [ + 'title' => "Contact Support", + 'id' => "Customer number or email address you have with us", + 'id-pl' => "e.g. 12345678 or john@kolab.org", + 'id-hint' => "Leave blank if you are not a customer yet", + 'name' => "Name", + 'name-pl' => "how we should call you in our reply", + 'email' => "Working email address", + 'email-pl' => "make sure we can reach you at this address", + 'summary' => "Issue Summary", + 'summary-pl' => "one sentence that summarizes your issue", + 'expl' => "Issue Explanation", + ], + + 'user' => [ + '2fa-hint1' => "This will remove 2-Factor Authentication entitlement as well as the user-configured factors.", + '2fa-hint2' => "Please, make sure to confirm the user identity properly.", + 'address' => "Address", + 'aliases' => "Aliases", + 'aliases-email' => "Email Aliases", + 'aliases-none' => "This user has no email aliases.", + 'add-bonus' => "Add bonus", + 'add-bonus-title' => "Add a bonus to the wallet", + 'add-penalty' => "Add penalty", + 'add-penalty-title' => "Add a penalty to the wallet", + 'auto-payment' => "Auto-payment", + 'auto-payment-text' => "Fill up by {amount} CHF when under {balance} CHF using {method}", + 'country' => "Country", + 'create' => "Create user", + 'custno' => "Customer No.", + 'delete' => "Delete user", + 'delete-email' => "Delete {email}", + 'delete-text' => "Do you really want to delete this user permanently?" + . " This will delete all account data and withdraw the permission to access the email account." + . " Please note that this action cannot be undone.", + 'discount' => "Discount", + 'discount-hint' => "applied discount", + 'discount-title' => "Account discount", + 'distlists' => "Distribution lists", + 'distlists-none' => "There are no distribution lists in this account.", + 'domains' => "Domains", + 'domains-none' => "There are no domains in this account.", + 'ext-email' => "External Email", + 'finances' => "Finances", + 'list-title' => "User accounts", + 'managed-by' => "Managed by", + 'new' => "New user account", + 'org' => "Organization", + 'package' => "Package", + 'price' => "Price", + 'profile-title' => "Your profile", + 'profile-delete' => "Delete account", + 'profile-delete-title' => "Delete this account?", + 'profile-delete-text1' => "This will delete the account as well as all domains, users and aliases associated with this account.", + 'profile-delete-warning' => "This operation is irreversible", + 'profile-delete-text2' => "As you will not be able to recover anything after this point, please make sure that you have migrated all data before proceeding.", + 'profile-delete-support' => "As we always strive to improve, we would like to ask for 2 minutes of your time. " + . "The best tool for improvement is feedback from users, and we would like to ask " + . "for a few words about your reasons for leaving our service. Please send your feedback to {email}.", + 'profile-delete-contact' => "Also feel free to contact {app} Support with any questions or concerns that you may have in this context.", + 'reset-2fa' => "Reset 2-Factor Auth", + 'reset-2fa-title' => "2-Factor Authentication Reset", + 'title' => "User account", + 'search-pl' => "User ID, email or domain", + 'skureq' => "{sku} requires {list}.", + 'subscription' => "Subscription", + 'subscriptions' => "Subscriptions", + 'subscriptions-none' => "This user has no subscriptions.", + 'users' => "Users", + 'users-none' => "There are no users in this account.", + ], + + 'wallet' => [ + 'add-credit' => "Add credit", + 'auto-payment-cancel' => "Cancel auto-payment", + 'auto-payment-change' => "Change auto-payment", + 'auto-payment-failed' => "The setup of automatic payments failed. Restart the process to enable automatic top-ups.", + 'auto-payment-hint' => "Here is how it works: Every time your account runs low, we will charge your preferred payment method for an amount you choose." + . " You can cancel or change the auto-payment option at any time.", + 'auto-payment-setup' => "Set up auto-payment", + 'auto-payment-disabled' => "The configured auto-payment has been disabled. Top up your wallet or raise the auto-payment amount.", + 'auto-payment-info' => "Auto-payment is set to fill up your account by {amount} CHF every time your account balance gets under {balance} CHF.", + 'auto-payment-inprogress' => "The setup of the automatic payment is still in progress.", + 'auto-payment-next' => "Next, you will be redirected to the checkout page, where you can provide your credit card details.", + 'auto-payment-disabled-next' => "The auto-payment is disabled. Immediately after you submit new settings we'll enable it and attempt to top up your wallet.", + 'auto-payment-update' => "Update auto-payment", + 'banktransfer-hint' => "Please note that a bank transfer can take several days to complete.", + 'currency-conv' => "Here is how it works: You specify the amount by which you want to top up your wallet in {wc}." + . " We will then convert this to {pc}, and on the next page you will be provided with the bank-details to transfer the amount in {pc}.", + 'fill-up' => "Fill up by", + 'history' => "History", + 'noperm' => "Only account owners can access a wallet.", + 'payment-amount-hint' => "Choose the amount by which you want to top up your wallet.", + 'payment-method' => "Method of payment: {method}", + 'payment-warning' => "You will be charged for {price}.", + 'pending-payments' => "Pending Payments", + 'pending-payments-warning' => "You have payments that are still in progress. See the \"Pending Payments\" tab below.", + 'pending-payments-none' => "There are no pending payments for this account.", + 'receipts' => "Receipts", + 'receipts-hint' => "Here you can download receipts (in PDF format) for payments in specified period. Select the period and press the Download button.", + 'receipts-none' => "There are no receipts for payments in this account. Please, note that you can download receipts after the month ends.", + 'title' => "Account balance", + 'top-up' => "Top up your wallet", + 'transactions' => "Transactions", + 'transactions-none' => "There are no transactions for this account.", + 'when-below' => "when account balance is below", + ], ]; diff --git a/src/resources/vue/Admin/Dashboard.vue b/src/resources/vue/Admin/Dashboard.vue --- a/src/resources/vue/Admin/Dashboard.vue +++ b/src/resources/vue/Admin/Dashboard.vue @@ -3,7 +3,7 @@
- Stats + {{ $t('dashboard.stats') }}
diff --git a/src/resources/vue/Admin/Distlist.vue b/src/resources/vue/Admin/Distlist.vue --- a/src/resources/vue/Admin/Distlist.vue +++ b/src/resources/vue/Admin/Distlist.vue @@ -6,7 +6,9 @@
- +
{{ list.id }} ({{ list.created_at }}) @@ -14,13 +16,13 @@
- +
{{ $root.distlistStatusText(list) }}
- +
{{ member }}
@@ -29,8 +31,12 @@
- - + +
diff --git a/src/resources/vue/Admin/Domain.vue b/src/resources/vue/Admin/Domain.vue --- a/src/resources/vue/Admin/Domain.vue +++ b/src/resources/vue/Admin/Domain.vue @@ -6,7 +6,9 @@
- +
{{ domain.id }} ({{ domain.created_at }}) @@ -14,7 +16,7 @@
- +
{{ $root.domainStatusText(domain) }} @@ -23,8 +25,12 @@
- - + +
@@ -32,7 +38,7 @@ @@ -40,9 +46,9 @@
-

Domain DNS verification sample:

+

{{ $t('domain.dns-verify') }}

{{ domain.dns.join("\n") }}

-

Domain DNS configuration sample:

+

{{ $t('domain.dns-config') }}

{{ domain.config.join("\n") }}

diff --git a/src/resources/vue/Admin/Stats.vue b/src/resources/vue/Admin/Stats.vue --- a/src/resources/vue/Admin/Stats.vue +++ b/src/resources/vue/Admin/Stats.vue @@ -1,6 +1,5 @@