diff --git a/src/resources/js/app.js b/src/resources/js/app.js
index 09aa686a..31a6c847 100644
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -1,522 +1,516 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap')
import AppComponent from '../vue/App'
import MenuComponent from '../vue/Widgets/Menu'
import SupportForm from '../vue/Widgets/SupportForm'
import store from './store'
import { loadLangAsync, i18n } from './locale'
const loader = '
Loading
'
let isLoading = 0
// Lock the UI with the 'loading...' element
const startLoading = () => {
isLoading++
let loading = $('#app > .app-loader').removeClass('fadeOut')
if (!loading.length) {
$('#app').append($(loader))
}
}
// Hide "loading" overlay
const stopLoading = () => {
if (isLoading > 0) {
$('#app > .app-loader').addClass('fadeOut')
isLoading--;
}
}
let loadingRoute
// Note: This has to be before the app is created
// Note: You cannot use app inside of the function
window.router.beforeEach((to, from, next) => {
// check if the route requires authentication and user is not logged in
if (to.meta.requiresAuth && !store.state.isLoggedIn) {
// remember the original request, to use after login
store.state.afterLogin = to;
// redirect to login page
next({ name: 'login' })
return
}
if (to.meta.loading) {
startLoading()
loadingRoute = to.name
}
next()
})
window.router.afterEach((to, from) => {
if (to.name && loadingRoute === to.name) {
stopLoading()
loadingRoute = null
}
// When changing a page remove old:
// - error page
// - modal backdrop
$('#error-page,.modal-backdrop.show').remove()
})
const app = new Vue({
components: {
AppComponent,
MenuComponent,
},
i18n,
store,
router: window.router,
data() {
return {
isUser: !window.isAdmin && !window.isReseller,
appName: window.config['app.name'],
appUrl: window.config['app.url'],
themeDir: '/themes/' + window.config['app.theme']
}
},
methods: {
// Clear (bootstrap) form validation state
clearFormValidation(form) {
$(form).find('.is-invalid').removeClass('is-invalid')
$(form).find('.invalid-feedback').remove()
},
hasPermission(type) {
const authInfo = store.state.authInfo
const key = 'enable' + type.charAt(0).toUpperCase() + type.slice(1)
return !!(authInfo && authInfo.statusInfo[key])
},
hasRoute(name) {
return this.$router.resolve({ name: name }).resolved.matched.length > 0
},
hasSKU(name) {
const authInfo = store.state.authInfo
return authInfo.statusInfo.skus && authInfo.statusInfo.skus.indexOf(name) != -1
},
isController(wallet_id) {
if (wallet_id && store.state.authInfo) {
let i
for (i = 0; i < store.state.authInfo.wallets.length; i++) {
if (wallet_id == store.state.authInfo.wallets[i].id) {
return true
}
}
for (i = 0; i < store.state.authInfo.accounts.length; i++) {
if (wallet_id == store.state.authInfo.accounts[i].id) {
return true
}
}
}
return false
},
// Set user state to "logged in"
loginUser(response, dashboard, update) {
if (!update) {
store.commit('logoutUser') // destroy old state data
store.commit('loginUser')
}
localStorage.setItem('token', response.access_token)
axios.defaults.headers.common.Authorization = 'Bearer ' + response.access_token
if (response.email) {
store.state.authInfo = response
}
if (dashboard !== false) {
this.$router.push(store.state.afterLogin || { name: 'dashboard' })
}
store.state.afterLogin = null
// Refresh the token before it expires
let timeout = response.expires_in || 0
// We'll refresh 60 seconds before the token expires
if (timeout > 60) {
timeout -= 60
}
// TODO: We probably should try a few times in case of an error
// TODO: We probably should prevent axios from doing any requests
// while the token is being refreshed
this.refreshTimeout = setTimeout(() => {
axios.post('/api/auth/refresh').then(response => {
this.loginUser(response.data, false, true)
})
}, timeout * 1000)
},
// Set user state to "not logged in"
logoutUser(redirect) {
store.commit('logoutUser')
localStorage.setItem('token', '')
delete axios.defaults.headers.common.Authorization
if (redirect !== false) {
this.$router.push({ name: 'login' })
}
clearTimeout(this.refreshTimeout)
},
logo(mode) {
let src = this.appUrl + this.themeDir + '/images/logo_' + (mode || 'header') + '.png'
return ``
},
// Display "loading" overlay inside of the specified element
addLoader(elem, small = true) {
$(elem).css({position: 'relative'}).append(small ? $(loader).addClass('small') : $(loader))
},
// Remove loader element added in addLoader()
removeLoader(elem) {
$(elem).find('.app-loader').remove()
},
startLoading,
stopLoading,
isLoading() {
return isLoading > 0
},
errorPage(code, msg, hint) {
// Until https://github.com/vuejs/vue-router/issues/977 is implemented
// 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 = '
'
+ `
${code}
${msg}
${hint}
`
+ '
'
$('#error-page').remove()
$('#app').append(error_page)
app.updateBodyClass('error')
},
errorHandler(error) {
this.stopLoading()
if (!error.response) {
// TODO: probably network connection error
} else if (error.response.status === 401) {
// Remember requested route to come back to it after log in
if (this.$route.meta.requiresAuth) {
store.state.afterLogin = this.$route
this.logoutUser()
} else {
this.logoutUser(false)
}
} else {
this.errorPage(error.response.status, error.response.statusText)
}
},
downloadFile(url) {
// TODO: This might not be a best way for big files as the content
// will be stored (temporarily) in browser memory
// TODO: This method does not show the download progress in the browser
// but it could be implemented in the UI, axios has 'progress' property
axios.get(url, { responseType: 'blob' })
.then(response => {
const link = document.createElement('a')
const contentDisposition = response.headers['content-disposition']
let filename = 'unknown'
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+)"/);
if (match.length === 2) {
filename = match[1];
}
}
link.href = window.URL.createObjectURL(response.data)
link.download = filename
link.click()
})
},
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) {
let index = ''
if (discount) {
cost = Math.floor(cost * ((100 - discount) / 100))
index = '\u00B9'
}
return this.price(cost) + '/month' + index
},
clickRecord(event) {
if (!/^(a|button|svg|path)$/i.test(event.target.nodeName)) {
let link = $(event.target).closest('tr').find('a')[0]
if (link) {
link.click()
}
}
},
domainStatusClass(domain) {
if (domain.isDeleted) {
return 'text-muted'
}
if (domain.isSuspended) {
return 'text-warning'
}
if (!domain.isVerified || !domain.isLdapReady || !domain.isConfirmed) {
return 'text-danger'
}
return 'text-success'
},
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) {
return 'text-muted'
}
if (list.isSuspended) {
return 'text-warning'
}
if (!list.isLdapReady) {
return 'text-danger'
}
return 'text-success'
},
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
// check if it is a "menu page", find the page name
// otherwise we'll use the real path as page name
window.config.menu.every(item => {
if (item.location == page && item.page) {
page = item.page
return false
}
})
page = page.replace(/^\//, '')
return page ? page : '404'
},
supportDialog(container) {
let dialog = $('#support-dialog')
// 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
form.$toast = this.$toast
dialog = $(form.$el)
}
dialog.on('shown.bs.modal', () => {
dialog.find('input').first().focus()
}).modal()
},
userStatusClass(user) {
if (user.isDeleted) {
return 'text-muted'
}
if (user.isSuspended) {
return 'text-warning'
}
if (!user.isImapReady || !user.isLdapReady) {
return 'text-danger'
}
return 'text-success'
},
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
// so, we can apply page-specific styles
document.body.className = 'page-' + (name || this.pageName()).replace(/\/.*$/, '')
}
}
})
// Fetch the locale file and the start the app
loadLangAsync().then(() => app.$mount('#app'))
// Add a axios request interceptor
window.axios.interceptors.request.use(
config => {
// This is the only way I found to change configuration options
// on a running application. We need this for browser testing.
config.headers['X-Test-Payment-Provider'] = window.config.paymentProvider
return config
},
error => {
// Do something with request error
return Promise.reject(error)
}
)
// Add a axios response interceptor for general/validation error handler
window.axios.interceptors.response.use(
response => {
if (response.config.onFinish) {
response.config.onFinish()
}
return response
},
error => {
let error_msg
let status = error.response ? error.response.status : 200
// Do not display the error in a toast message, pass the error as-is
if (error.config.ignoreErrors) {
return Promise.reject(error)
}
if (error.config.onFinish) {
error.config.onFinish()
}
if (error.response && status == 422) {
error_msg = "Form validation error"
const modal = $('div.modal.show')
$(modal.length ? modal : 'form').each((i, form) => {
form = $(form)
$.each(error.response.data.errors || {}, (idx, msg) => {
const input_name = (form.data('validation-prefix') || form.find('form').first().data('validation-prefix') || '') + idx
let input = form.find('#' + input_name)
if (!input.length) {
input = form.find('[name="' + input_name + '"]');
}
if (input.length) {
// Create an error message
// API responses can use a string, array or object
let msg_text = ''
if ($.type(msg) !== 'string') {
$.each(msg, (index, str) => {
msg_text += str + ' '
})
}
else {
msg_text = msg
}
let feedback = $('
').text(msg_text)
if (input.is('.list-input')) {
// List input widget
let controls = input.children(':not(:first-child)')
if (!controls.length && typeof msg == 'string') {
// this is an empty list (the main input only)
// and the error message is not an array
input.find('.main-input').addClass('is-invalid')
} else {
controls.each((index, element) => {
if (msg[index]) {
$(element).find('input').addClass('is-invalid')
}
})
}
input.addClass('is-invalid').next('.invalid-feedback').remove()
input.after(feedback)
}
else {
// Standard form element
input.addClass('is-invalid')
input.parent().find('.invalid-feedback').remove()
input.parent().append(feedback)
}
}
})
form.find('.is-invalid:not(.listinput-widget)').first().focus()
})
}
else if (error.response && error.response.data) {
error_msg = error.response.data.message
}
else {
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
index c012bf82..aac060b9 100644
--- a/src/resources/lang/de/ui.php
+++ b/src/resources/lang/de/ui.php
@@ -1,30 +1,30 @@
[
+ 'btn' => [
'cancel' => "Stornieren",
'save' => "Speichern",
],
'lang' => [
'en' => "Englisch",
'de' => "Deutsch",
'fr' => "Französisch",
],
'login' => [
'forgot_password' => "Passwort vergessen?",
'sign_in' => "Anmelden",
'webmail' => "Webmail",
],
'menu' => [
'cockpit' => "Cockpit",
'login' => "Einloggen",
'logout' => "Ausloggen",
'signup' => "Signup",
'toggle' => "Navigation umschalten",
],
];
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
index 4c83ea23..4b9fecec 100644
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -1,178 +1,407 @@
[
+ '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' => [
'list-title' => "Distribution list | Distribution lists",
'create' => "Create list",
'delete' => "Delete list",
'email' => "Email",
'list-empty' => "There are no distribution lists in this account.",
'new' => "New distribution list",
'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' => [
'en' => "English",
'de' => "German",
'fr' => "French",
'it' => "Italian",
],
'login' => [
'2fa' => "Second factor code",
'2fa_desc' => "Second factor code is optional for users with no 2-Factor Authentication setup.",
'forgot_password' => "Forgot password?",
'header' => "Please sign in",
'sign_in' => "Sign in",
'webmail' => "Webmail"
],
'meet' => [
'title' => "Voice & Video Conferencing",
'welcome' => "Welcome to our beta program for Voice & Video Conferencing.",
'url' => "You have a room of your own at the URL below. This room is only open when you yourself are in attendance. Use this URL to invite people to join you.",
'notice' => "This is a work in progress and more features will be added over time. Current features include:",
'sharing' => "Screen Sharing",
'sharing-text' => "Share your screen for presentations or show-and-tell.",
'security' => "Room Security",
'security-text' => "Increase the room security by setting a password that attendees will need to know"
. " before they can enter, or lock the door so attendees will have to knock, and a moderator can accept or deny those requests.",
'qa' => "Raise Hand (Q&A)",
'qa-text' => "Silent audience members can raise their hand to facilitate a Question & Answer session with the panel members.",
'moderation' => "Moderator Delegation",
'moderation-text' => "Delegate moderator authority for the session, so that a speaker is not needlessly"
. " interrupted with attendees knocking and other moderator duties.",
'eject' => "Eject Attendees",
'eject-text' => "Eject attendees from the session in order to force them to reconnect, or address policy"
. " violations. Click the user icon for effective dismissal.",
'silent' => "Silent Audience Members",
'silent-text' => "For a webinar-style session, configure the room to force all new attendees to be silent audience members.",
'interpreters' => "Language Specific Audio Channels",
'interpreters-text' => "Designate a participant to interpret the original audio to a target language, for sessions"
. " with multi-lingual attendees. The interpreter is expected to be able to relay the original audio, and override it.",
'beta-notice' => "Keep in mind that this is still in beta and might come with some issues."
. " Should you encounter any on your way, let us know by contacting support.",
// Room options dialog
'options' => "Room options",
'password' => "Password",
'password-none' => "none",
'password-clear' => "Clear password",
'password-set' => "Set password",
'password-text' => "You can add a password to your meeting. Participants will have to provide the password before they are allowed to join the meeting.",
'lock' => "Locked room",
'lock-text' => "When the room is locked participants have to be approved by a moderator before they could join the meeting.",
'nomedia' => "Subscribers only",
'nomedia-text' => "Forces all participants to join as subscribers (with camera and microphone turned off)."
. " Moderators will be able to promote them to publishers throughout the session.",
// Room menu
'partcnt' => "Number of participants",
'menu-audio-mute' => "Mute audio",
'menu-audio-unmute' => "Unmute audio",
'menu-video-mute' => "Mute video",
'menu-video-unmute' => "Unmute video",
'menu-screen' => "Share screen",
'menu-hand-lower' => "Lower hand",
'menu-hand-raise' => "Raise hand",
'menu-channel' => "Interpreted language channel",
'menu-chat' => "Chat",
'menu-fullscreen' => "Full screen",
'menu-fullscreen-exit' => "Exit full screen",
'menu-leave' => "Leave session",
// Room setup screen
'setup-title' => "Set up your session",
'mic' => "Microphone",
'cam' => "Camera",
'nick' => "Nickname",
'nick-placeholder' => "Your name",
'join' => "JOIN",
'joinnow' => "JOIN NOW",
'imaowner' => "I'm the owner",
// Room
'qa' => "Q & A",
'leave-title' => "Room closed",
'leave-body' => "The session has been closed by the room owner.",
'media-title' => "Media setup",
'join-request' => "Join request",
'join-requested' => "{user} requested to join.",
// Status messages
'status-init' => "Checking the room...",
'status-323' => "The room is closed. Please, wait for the owner to start the session.",
'status-324' => "The room is closed. It will be open for others after you join.",
'status-325' => "The room is ready. Please, provide a valid password.",
'status-326' => "The room is locked. Please, enter your name and try again.",
'status-327' => "Waiting for permission to join the room.",
'status-404' => "The room does not exist.",
'status-429' => "Too many requests. Please, wait.",
'status-500' => "Failed to connect to the room. Server error.",
// Other menus
'media-setup' => "Media setup",
'perm' => "Permissions",
'perm-av' => "Audio & Video publishing",
'perm-mod' => "Moderation",
'lang-int' => "Language interpreter",
'menu-options' => "Options",
],
'menu' => [
'cockpit' => "Cockpit",
'login' => "Login",
'logout' => "Logout",
'signup' => "Signup",
'toggle' => "Toggle navigation",
],
'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}",
],
'password' => [
'reset' => "Password Reset",
'reset-step1' => "Enter your email address to reset your password.",
'reset-step1-hint' => "You may need to check your spam folder or unblock {email}.",
'reset-step2' => "We sent out a confirmation code to your external email address."
. " 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
index 39bf9208..b0d8d65c 100644
--- a/src/resources/vue/Admin/Dashboard.vue
+++ b/src/resources/vue/Admin/Dashboard.vue
@@ -1,24 +1,24 @@
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.
-
The domain must have one of the following entries in DNS:
+
{{ $t('domain.verify-intro') }}
+
+
-
TXT entry with value: {{ domain.hash_text }}
-
or CNAME entry: {{ domain.hash_cname }}.{{ domain.namespace }}. IN CNAME {{ domain.hash_code }}.{{ domain.namespace }}.
- When this is done press the button below to start the verification.
-
Here's a sample zone file for your domain:
{{ domain.dns.join("\n") }}
- Verify
+ {{ $t('domain.verify-outro') }}
+
+
{{ $t('domain.verify-sample') }}
{{ domain.dns.join("\n") }}
+ {{ $t('btn.verify') }}
-
Domain configuration
+
{{ $t('domain.config') }}
-
In order to let {{ $root.appName }} receive email traffic for your domain you need to adjust
- the DNS settings, more precisely the MX entries, accordingly.
-
Edit your domain's zone file and replace existing MX
- entries with the following values:
{{ domain.config.join("\n") }}
-
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.
{{ $t('user.title') }}
- Delete user
+ {{ $t('user.delete') }}
-
New user account
+
{{ $t('user.new') }}
-
+ ×
-
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.
This will delete the account as well as all domains, users and aliases associated with this account.
- This operation is irreversible.
-
As you will not be able to recover anything after this point, please make sure
- that you have migrated all data before proceeding.
-
- 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 {{ supportEmail }}.
-
-
Also feel free to contact {{ $root.appName }} Support with any questions
- or concerns that you may have in this context.
- Here you can download receipts (in PDF format) for payments in specified period.
- Select the period and press the Download button.
+ {{ $t('wallet.receipts-hint') }}
- Download
+ {{ $t('btn.download') }}
- There are no receipts for payments in this account. Please, note that you can download
- receipts after the month ends.
+ {{ $t('wallet.receipts-none') }}
{{ paymentDialogTitle }}
-
+ ×
- Here is how it works: You specify the amount by which you want to to up your wallet in {{ wallet.currency }}.
- We will then convert this to {{ selectedPaymentMethod.currency }}, and on the next page you will be provided with the bank-details
- to transfer the amount in {{ selectedPaymentMethod.currency }}.
+ {{ $t('wallet.currency-conv', { wc: wallet.currency, pc: selectedPaymentMethod.currency }) }}
- Please note that a bank transfer can take several days to complete.
+ {{ $t('wallet.banktransfer-hint') }}
+
+
+ {{ $t('wallet.payment-amount-hint') }}
-
Choose the amount by which you want to top up your wallet.
- We are preparing your account.
- We are preparing the domain.
- We are preparing the distribution list.
- We are preparing the user account.
+ {{ $t('status.prepare-account') }}
+ {{ $t('status.prepare-domain') }}
+ {{ $t('status.prepare-distlist') }}
+ {{ $t('status.prepare-user') }}
- Some features may be missing or readonly at the moment.
- The process never ends? Press the "Refresh" button, please.
+ {{ $t('status.prepare-hint') }}
+
+ {{ $t('status.prepare-refresh') }}
- Refresh
+ {{ $t('btn.refresh') }}
- Your account is almost ready.
- The domain is almost ready.
- The distribution list is almost ready.
- The user account is almost ready.
+ {{ $t('status.ready-account') }}
+ {{ $t('status.ready-domain') }}
+ {{ $t('status.ready-distlist') }}
+ {{ $t('status.ready-user') }}
- Verify your domain to finish the setup process.
+ {{ $t('status.verify') }}