diff --git a/src/app/Http/Controllers/ContentController.php b/src/app/Http/Controllers/ContentController.php index e86d07b6..93df377e 100644 --- a/src/app/Http/Controllers/ContentController.php +++ b/src/app/Http/Controllers/ContentController.php @@ -1,173 +1,173 @@ with('env', \App\Utils::uiEnv()); } /** * Get the list of FAQ entries for the specified page * * @param string $page Page path * * @return \Illuminate\Http\JsonResponse JSON response */ public function faqContent(string $page) { if (empty($page)) { return $this->errorResponse(404); } $faq = []; $theme_name = \config('app.theme'); $theme_file = resource_path("themes/{$theme_name}/theme.json"); if (file_exists($theme_file)) { $theme = json_decode(file_get_contents($theme_file), true); if (json_last_error() != JSON_ERROR_NONE) { \Log::error("Failed to parse $theme_file: " . json_last_error_msg()); } elseif (!empty($theme['faq']) && !empty($theme['faq'][$page])) { $faq = $theme['faq'][$page]; } // TODO: Support pages with variables, e.g. users/ } // Localization if (!empty($faq)) { self::loadLocale($theme_name); foreach ($faq as $idx => $item) { if (!empty($item['label'])) { $faq[$idx]['title'] = \trans('theme::faq.' . $item['label']); } } } return response()->json(['status' => 'success', 'faq' => $faq]); } /** * Returns list of enabled locales * * @return array List of two-letter language codes */ public static function locales(): array { if ($locales = \env('APP_LOCALES')) { return preg_split('/\s*,\s*/', strtolower(trim($locales))); } - return ['en', 'de']; + return ['en', 'de', 'fr']; } /** * Get menu definition from the theme * * @return array */ public static function menu(): array { $theme_name = \config('app.theme'); $theme_file = resource_path("themes/{$theme_name}/theme.json"); $menu = []; if (file_exists($theme_file)) { $theme = json_decode(file_get_contents($theme_file), true); if (json_last_error() != JSON_ERROR_NONE) { \Log::error("Failed to parse $theme_file: " . json_last_error_msg()); } elseif (!empty($theme['menu'])) { $menu = $theme['menu']; } } // TODO: These 2-3 lines could become a utility function somewhere $req_domain = preg_replace('/:[0-9]+$/', '', request()->getHttpHost()); $sys_domain = \config('app.domain'); $isAdmin = $req_domain == "admin.$sys_domain"; $filter = function ($item) use ($isAdmin) { if ($isAdmin && empty($item['admin'])) { return false; } if (!$isAdmin && !empty($item['admin']) && $item['admin'] === 'only') { return false; } return true; }; $menu = array_values(array_filter($menu, $filter)); // Load localization files for all supported languages $lang_path = resource_path("themes/{$theme_name}/lang"); $locales = []; foreach (self::locales() as $lang) { $file = "{$lang_path}/{$lang}/menu.php"; if (file_exists($file)) { $locales[$lang] = include $file; } } foreach ($menu as $idx => $item) { // Handle menu localization if (!empty($item['label'])) { $label = $item['label']; foreach ($locales as $lang => $labels) { if (!empty($labels[$label])) { $item["title-{$lang}"] = $labels[$label]; } } } // Unset properties that we don't need on the client side unset($item['admin'], $item['label']); $menu[$idx] = $item; } return $menu; } /** * Register localization files from the theme. * * @param string $theme Theme name */ protected static function loadLocale(string $theme): void { $path = resource_path(sprintf('themes/%s/lang', $theme)); \app('translator')->addNamespace('theme', $path); } } diff --git a/src/resources/js/app.js b/src/resources/js/app.js index e41d9739..1b57460a 100644 --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -1,523 +1,523 @@ /** * 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 { Tab } from 'bootstrap' 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() $('body').css('padding', 0) // remove padding added by unclosed modal }) 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 `${this.appName}` }, // Display "loading" overlay inside of the specified element addLoader(elem, small = true, style = null) { if (style) { $(elem).css(style) } else { $(elem).css('position', 'relative') } $(elem).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 }, tab(e) { e.preventDefault() new Tab(e.target).show() }, 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". 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 + return this.price(cost) + '/' + this.$t('wallet.month') + index }, clickRecord(event) { if (!/^(a|button|svg|path)$/i.test(event.target.nodeName)) { $(event.target).closest('tr').find('a').trigger('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 this.$t('status.deleted') } if (domain.isSuspended) { return this.$t('status.suspended') } if (!domain.isVerified || !domain.isLdapReady || !domain.isConfirmed) { return this.$t('status.notready') } 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 this.$t('status.deleted') } if (list.isSuspended) { return this.$t('status.suspended') } if (!list.isLdapReady) { return this.$t('status.notready') } 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')[0] if (!dialog) { // FIXME: Find a nicer way of doing this SupportForm.i18n = i18n let form = new Vue(SupportForm) form.$mount($('
').appendTo(container)[0]) form.$root = this form.$toast = this.$toast dialog = form.$el } dialog.__vue__.showDialog() }, 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 this.$t('status.deleted') } if (user.isSuspended) { return this.$t('status.suspended') } if (!user.isImapReady || !user.isLdapReady) { return this.$t('status.notready') } 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 (typeof(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 || app.$t('error.server')) // Pass the error as-is return Promise.reject(error) } ) diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php index e80e8b5a..92486eaf 100644 --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -1,420 +1,421 @@ [ '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", 'spf-whitelist' => "SPF Whitelist", 'spf-whitelist-text' => "The Sender Policy Framework allows a sender domain to disclose, through DNS, " . "which systems are allowed to send emails with an envelope sender address within said domain.", 'spf-whitelist-ex' => "Here you can specify a list of allowed servers, for example: .ess.barracuda.com.", '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", 'disabled' => "disabled", 'domain' => "Domain", 'email' => "Email Address", 'enabled' => "enabled", 'firstname' => "First Name", 'general' => "General", 'lastname' => "Last Name", 'none' => "none", 'or' => "or", 'password' => "Password", 'password-confirm' => "Confirm Password", 'phone' => "Phone", 'settings' => "Settings", '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} when under {balance} using {method}", 'country' => "Country", 'create' => "Create user", 'custno' => "Customer No.", 'delete' => "Delete user", 'delete-account' => "Delete this account?", '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", 'greylisting' => "Greylisting", 'greylisting-text' => "Greylisting is a method of defending users against spam. Any incoming mail from an unrecognized sender " . "is temporarily rejected. The originating server should try again after a delay. " . "This time the email will be accepted. Spammers usually do not reattempt mail delivery.", '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} every time your account balance gets under {balance}.", '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", + 'month' => "month", '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/lang/fr/app.php b/src/resources/lang/fr/app.php new file mode 100644 index 00000000..f7b2b3c4 --- /dev/null +++ b/src/resources/lang/fr/app.php @@ -0,0 +1,81 @@ + "L'auto-paiement a été supprimé.", + 'mandate-update-success' => "L'auto-paiement a été mis-à-jour.", + + 'planbutton' => "Choisir :plan", + 'siteuser' => "Utilisateur du :site", + 'domain-setconfig-success' => "Les paramètres du domaine sont mis à jour avec succès.", + 'user-setconfig-success' => "Les paramètres d'utilisateur sont mis à jour avec succès.", + + 'process-async' => "Le processus d'installation a été poussé. Veuillez patienter.", + 'process-user-new' => "Enregistrement d'un utilisateur...", + 'process-user-ldap-ready' => "Création d'un utilisateur...", + 'process-user-imap-ready' => "Création d'une boîte aux lettres...", + 'process-distlist-new' => "Enregistrement d'une liste de distribution...", + 'process-distlist-ldap-ready' => "Création d'une liste de distribution...", + 'process-domain-new' => "Enregistrement d'un domaine personnalisé...", + 'process-domain-ldap-ready' => "Création d'un domaine personnalisé...", + 'process-domain-verified' => "Vérification d'un domaine personnalisé...", + 'process-domain-confirmed' => "vérification de la propriété d'un domaine personnalisé...", + 'process-success' => "Le processus d'installation s'est terminé avec succès.", + 'process-error-user-ldap-ready' => "Échec de créar un utilisateur.", + 'process-error-user-imap-ready' => "Échec de la vérification de l'existence d'une boîte aux lettres.", + 'process-error-domain-ldap-ready' => "Échec de créer un domaine.", + 'process-error-domain-verified' => "Échec de vérifier un domaine.", + 'process-error-domain-confirmed' => "Échec de la vérification de la propriété d'un domaine.", + 'process-distlist-new' => "Enregistrement d'une liste de distribution...", + 'process-distlist-ldap-ready' => "Création d'une liste de distribution...", + 'process-error-distlist-ldap-ready' => "Échec de créer une liste de distrubion.", + + 'distlist-update-success' => "Liste de distribution mis-à-jour avec succès.", + 'distlist-create-success' => "Liste de distribution créer avec succès.", + 'distlist-delete-success' => "Liste de distribution suppriméee avec succès.", + 'distlist-suspend-success' => "Liste de distribution à été suspendue avec succès.", + 'distlist-unsuspend-success' => "Liste de distribution à été débloquée avec succès.", + + 'domain-verify-success' => "Domaine vérifié avec succès.", + 'domain-verify-error' => "Vérification de propriété de domaine à échoué.", + 'domain-suspend-success' => "Domaine suspendue avec succès.", + 'domain-unsuspend-success' => "Domaine debloqué avec succès.", + + 'user-update-success' => "Mis-à-jour des données de l'utilsateur effectué avec succès.", + 'user-create-success' => "Utilisateur a été crée avec succès.", + 'user-delete-success' => "Utilisateur a été supprimé avec succès.", + 'user-suspend-success' => "Utilisateur a été suspendu avec succès.", + 'user-unsuspend-success' => "Utilisateur a été debloqué avec succès.", + 'user-reset-2fa-success' => "Réinstallation de l'authentification à 2-Facteur avec succès.", + + 'search-foundxdomains' => "Les domaines :x ont été trouvés.", + 'search-foundxgroups' => "Les listes de distribution :x ont été trouvées.", + 'search-foundxusers' => "Les comptes d'utilisateurs :x ont été trouvés.", + + 'signup-invitations-created' => "L'invitation à été crée.|:count nombre d'invitations ont été crée.", + 'signup-invitations-csv-empty' => "Aucune adresses email valides ont été trouvées dans le fichier téléchargé.", + 'signup-invitations-csv-invalid-email' => "Une adresse email invalide a été trouvée (:email) on line :line.", + 'signup-invitation-delete-success' => "Invitation supprimée avec succès.", + 'signup-invitation-resend-success' => "Invitation ajoutée à la file d'attente d'envoi avec succès.", + + 'support-request-success' => "Demande de soutien soumise avec succès.", + 'support-request-error' => "La soumission de demande de soutien a échoué.", + + 'wallet-award-success' => "Le bonus a été ajouté au portefeuille avec succès.", + 'wallet-penalty-success' => "La pénalité a été ajoutée au portefeuille avec succès.", + 'wallet-update-success' => "Portefeuille d'utilisateur a été mis-à-jour avec succès.", + + 'wallet-notice-date' => "Avec vos abonnements actuels, le solde de votre compte durera jusqu'à environ :date (:days).", + 'wallet-notice-nocredit' => "Votre crédit a été epuisé, veuillez recharger immédiatement votre solde.", + 'wallet-notice-today' => "Votre reste crédit sera épuisé aujourd'hui, veuillez recharger immédiatement.", + 'wallet-notice-trial' => "Vous êtes dans votre période d'essai gratuite.", + 'wallet-notice-trial-end' => "Vous approchez de la fin de votre période d'essai gratuite, veuillez recharger pour continuer.", +]; diff --git a/src/resources/lang/fr/auth.php b/src/resources/lang/fr/auth.php new file mode 100644 index 00000000..32488ee4 --- /dev/null +++ b/src/resources/lang/fr/auth.php @@ -0,0 +1,20 @@ + "Nom d'utilisateur et mot de passe invalide.", + 'throttle' => "Trop de tentatives de connexion. Veuillez ré-essayer dans :seconds secondes.", + 'logoutsuccess' => "Déconnecté avec succès.", + +]; \ No newline at end of file diff --git a/src/resources/lang/fr/documents.php b/src/resources/lang/fr/documents.php new file mode 100644 index 00000000..aeeaf5ea --- /dev/null +++ b/src/resources/lang/fr/documents.php @@ -0,0 +1,40 @@ + "ID de Compte", + 'amount' => "Montant", + 'customer-no' => "No° de Client.", + 'date' => "Date", + 'description' => "Description", + 'period' => "Période", + 'total' => "Total", + + 'month1' => "Janvier", + 'month2' => "Février", + 'month3' => "Mars", + 'month4' => "Avril", + 'month5' => "Mai", + 'month6' => "Juin", + 'month7' => "Juillet", + 'month8' => "Août", + 'month9' => "Septembre", + 'month10' => "Octobre", + 'month11' => "Novembre", + 'month12' => "Décembre", + + 'receipt-filename' => ":site Receipt for :id", + 'receipt-title' => "Reçu pour :month :year", + 'receipt-item-desc' => ":site Services", + 'receipt-refund' => "Remboursement", + 'receipt-chargeback' => "Refacturation", + + 'subtotal' => "Sous-Total", + 'vat' => "VAT (:rate%)", +]; diff --git a/src/resources/lang/fr/mail.php b/src/resources/lang/fr/mail.php new file mode 100644 index 00000000..268eba96 --- /dev/null +++ b/src/resources/lang/fr/mail.php @@ -0,0 +1,90 @@ + "Salut :name,", + 'footer1' => "Meilleures salutations,", + 'footer2' => "Votre :site Équipe", + + 'more-info-html' => "Cliquez ici pour plus d'information.", + 'more-info-text' => "Cliquez :href pour plus d'information.", + + 'negativebalance-subject' => ":site Paiement Requis", + 'negativebalance-body' => "C'est une notification pour vous informer que votre :site le solde du compte est en négatif et nécessite votre attention." + . " Veillez à mettre en place un auto-paiement pour éviter de tel avertissement comme celui-ci dans le future.", + 'negativebalance-body-ext' => "Régler votre compte pour le maintenir en fontion:", + + 'negativebalancereminder-subject' => ":site Rappel de Paiement", + 'negativebalancereminder-body' => "Vous n'avez peut-être pas rendu compte que vous êtes en retard avec votre paiement pour :site compte." + . " Veillez à mettre en place un auto-paiement pour éviter de tel avertissement comme celui-ci dans le future.", + 'negativebalancereminder-body-ext' => "Régler votre compte pour le maintenir en fontion:", + 'negativebalancereminder-body-warning' => "Soyez conscient que votre compte sera suspendu si le" + . " solde de votre compte n'est réglé avant le :date.", + + 'negativebalancesuspended-subject' => ":site Compte Suspendu", + 'negativebalancesuspended-body' => "Votre :site compte a été suspendu à la suite d'un solde négatif pendant trop longtemps." + . " Veillez nvisager de mettre en place un auto-paiement pour éviter de tel avertissement comme celui-ci dans le future.", + 'negativebalancesuspended-body-ext' => "Régler votre compte pour le maintenir en fontion:", + 'negativebalancesuspended-body-warning' => "Veuillez vous assurer que votre compte et toutes ses données seront supprimés" + . " si le solde de votre compte n'est pas réglé avant le :date.", + + 'negativebalancebeforedelete-subject' => ":site Dernier Avertissement", + 'negativebalancebeforedelete-body' => "Ceci-ci est le dernier rappel pour régler votre :site solde de compte." + . " votre compte et toutes ses données seront supprimés si le solde de votre compte nest pas régler avant le :date.", + 'negativebalancebeforedelete-body-ext' => "Régler votre compte immédiatement:", + + 'passwordreset-subject' => ":site Réinitialisation du mot de passe", + 'passwordreset-body1' => "Quelqu'un a récemment demandé de changer votre :site mot de passe.", + 'passwordreset-body2' => "Si vous êtes dans ce cas, veuillez utiliser ce code de vérification pour terminer le processus:", + 'passwordreset-body3' => "Vous pourrez également cliquer sur le lien ci-dessous:", + 'passwordreset-body4' => "si vous n'avez pas fait une telle demande, vous pouvez soit ignorer ce message, soit prendre contact avec nous au sujet de cet incident.", + + 'paymentmandatedisabled-subject' => ":site Problème d'auto-paiement", + 'paymentmandatedisabled-body' => "Votre :site solde du compte est négatif" + . " et le montant configuré pour le rechargement automatique du solde ne suffit pas" + . " le coût des abonnements consommés.", + 'paymentmandatedisabled-body-ext' => "En vous facturant plusieurs fois le même monant dans un court laps de temps" + . " peut entraîner des problêmes avec le fournisseur du service de paiement." + . " Pour éviter tout problème, nous avons suspendu l'auto-paiement pour votre compte." + . " Pour resourdre le problème,veuillez vous connecter aux paramètres de votre compte et modifier le montant d'auto-paiement.", + + 'paymentfailure-subject' => ":site Paiement Echoué", + 'paymentfailure-body' => "Un problème est survenu avec l'auto-paiement pour votre :site account.\n" + . "Nous avons tenté de vous facturer via votre méthode de paiement choisie, mais le chargement n'a pas été effectué.", + 'paymentfailure-body-ext' => "Pour éviter tout problème supplémentaire, nous avons suspendu l'auto-paiement sur votre compte." + . " Pour resourdre le problème,veuillez vous connecter aux paramètres de votre compte au", + 'paymentfailure-body-rest' => "Vous y trouverez la possibilité de payer manuellement votre compte et" + . " de modifier vos paramètres d'auto-paiement.", + + 'paymentsuccess-subject' => ":site Paiement Effectué", + 'paymentsuccess-body' => "L'auto-paiement pour votre :site le compte s'est exécuté sans problème. " + . "Vous pouvez contrôler le solde de votre nouveau compte et obtenir plus de détails ici:", + + 'support' => "Cas particulier? Il y a un probléme avec une charge?\n" + . ":site Le support reste à votre disposition.", + + 'signupcode-subject' => ":site Enregistrement", + 'signupcode-body1' => "Voici votre code de vérification pour le :site registration process:", + 'signupcode-body2' => "Vous pouvez également continuer avec le processus d'enregistrement en cliquant sur le lien ci-dessous:", + + 'signupinvitation-subject' => ":site Invitation", + 'signupinvitation-header' => "Salut,", + 'signupinvitation-body1' => "Vous êtes invité à joindre :site. Cliquez sur le lien ci-dessous pour vous inscrire.", + 'signupinvitation-body2' => "", + + 'suspendeddebtor-subject' => ":site Compte Suspendu", + 'suspendeddebtor-body' => "Vous êtes en retard avec le paiement de votre :site compte" + . " pour plus de :days jours. Votre compte est suspendu.", + 'suspendeddebtor-middle' => "Réglez immédiatement pour réactiver votre compte.", + 'suspendeddebtor-cancel' => "Vous ne souhaitez plus être notre client?" + . " Voici la démarche à suivre pour annuler votre compte:", + +]; diff --git a/src/resources/lang/fr/meet.php b/src/resources/lang/fr/meet.php new file mode 100644 index 00000000..a544a396 --- /dev/null +++ b/src/resources/lang/fr/meet.php @@ -0,0 +1,30 @@ + 'La connexion n´existe pas.', + 'connection-dismiss-error' => 'Échec du rejet de la connexion.', + 'room-not-found' => 'La salle n´existe pas.', + 'room-setconfig-success' => 'La configuration de la salle a été actualisée avec succès.', + 'room-unsupported-option-error' => 'Option de configuration de la salle invalide.', + 'session-not-found' => 'La session n\'existe pas.', + 'session-create-error' => 'Échec de la création de la session.', + 'session-join-error' => 'Échec de se joindre à la session.', + 'session-close-error' => 'Échec de fermer la session.', + 'session-close-success' => 'La session a été terminée avec succès.', + 'session-password-error' => 'Échec de se joindre à la session. Mot de pas invalide.', + 'session-request-accept-error' => 'Echec d\'accepter la demande d\'adhésion', + 'session-request-deny-error' => 'Echec de refuser la demande d\'adhésion.', + 'session-room-locked-error' => 'Échec de se joindre à la session. Salle verrouillée.', +]; diff --git a/src/resources/lang/fr/transactions.php b/src/resources/lang/fr/transactions.php new file mode 100644 index 00000000..df4210b4 --- /dev/null +++ b/src/resources/lang/fr/transactions.php @@ -0,0 +1,26 @@ + ':user_email a créé :sku_title pour :object', + 'entitlement-billed' => ':sku_title for :object est facturé à :amount', + 'entitlement-deleted' => ':user_email supprimé :sku_title pour :object', + + 'entitlement-created-short' => 'Ajoutée :sku_title pour :object', + 'entitlement-billed-short' => 'Facturé :sku_title pour :object', + 'entitlement-deleted-short' => 'Supprimé :sku_title pour :object', + + 'wallet-award' => 'bonus de :amount attribué à :wallet; :description', + 'wallet-chargeback' => ':amount été refacturé par :wallet', + 'wallet-credit' => ':amount a été ajouté au solde de :wallet', + 'wallet-debit' => ':amount a été déduit du solde de :wallet', + 'wallet-penalty' => 'Le solde de :wallet été réduit de :amount; :description', + 'wallet-refund' => ':amount a été remboursé sur le solde de :wallet', + 'wallet-refund' => ':amount a été remboursé par :wallet', + + 'wallet-award-short' => 'Prime: :description', + 'wallet-chargeback-short' => 'Rétrofacturation', + 'wallet-credit-short' => 'Paiement', + 'wallet-debit-short' => 'Déduction', + 'wallet-penalty-short' => 'Charger: :description', + 'wallet-refund-short' => 'Remboursement: :description', +]; diff --git a/src/resources/lang/fr/ui.php b/src/resources/lang/fr/ui.php new file mode 100644 index 00000000..b504323e --- /dev/null +++ b/src/resources/lang/fr/ui.php @@ -0,0 +1,421 @@ + [ + 'faq' => "FAQ", + ], + + 'btn' => [ + 'add' => "Ajouter", + 'accept' => "Accepter", + 'back' => "Back", + 'cancel' => "Annuler", + 'close' => "Fermer", + 'continue' => "Continuer", + 'delete' => "Supprimer", + 'deny' => "Refuser", + 'download' => "Télécharger", + 'edit' => "Modifier", + 'file' => "Choisir le ficher...", + 'moreinfo' => "Plus d'information", + 'refresh' => "Actualiser", + 'reset' => "Réinitialiser", + 'resend' => "Envoyer à nouveau", + 'save' => "Sauvegarder", + 'search' => "Chercher", + 'signup' => "S'inscrire", + 'submit' => "Soumettre", + 'suspend' => "Suspendre", + 'unsuspend' => "Débloquer", + 'verify' => "Vérifier", + ], + + 'dashboard' => [ + 'beta' => "bêta", + 'distlists' => "Listes de distribution", + 'chat' => "Chat Vidéo", + 'domains' => "Domaines", + 'invitations' => "Invitations", + 'profile' => "Votre profil", + 'users' => "D'utilisateurs", + 'wallet' => "Portefeuille", + 'webmail' => "Webmail", + 'stats' => "Statistiques", + ], + + 'distlist' => [ + 'list-title' => "Liste de distribution | Listes de Distribution", + 'create' => "Créer une liste", + 'delete' => "Suprimmer une list", + 'email' => "Courriel", + 'list-empty' => "il n'y a pas de listes de distribution dans ce compte.", + 'new' => "Nouvelle liste de distribution", + 'recipients' => "Destinataires", + ], + + 'domain' => [ + 'dns-verify' => "Exemple de vérification du DNS d'un domaine:", + 'dns-config' => "Exemple de configuration du DNS d'un domaine:", + 'namespace' => "Espace de noms", + 'verify' => "Vérification du domaine", + 'verify-intro' => "Afin de confirmer que vous êtes bien le titulaire du domaine, nous devons exécuter un processus de vérification avant de l'activer définitivement pour la livraison d'e-mails.", + 'verify-dns' => "Le domaine doit avoir l'une des entrées suivantes dans le DNS:", + 'verify-dns-txt' => "Entrée TXT avec valeur:", + 'verify-dns-cname' => "ou entrée CNAME:", + 'verify-outro' => "Lorsque cela est fait, appuyez sur le bouton ci-dessous pour lancer la vérification.", + 'verify-sample' => "Voici un fichier de zone simple pour votre domaine:", + 'config' => "Configuration du domaine", + 'config-intro' => "Afin de permettre à {app} de recevoir le trafic de messagerie pour votre domaine, vous devez ajuster les paramètres DNS, plus précisément les entrées MX, en conséquence.", + 'config-sample' => "Modifiez le fichier de zone de votre domaine et remplacez les entrées MX existantes par les valeurs suivantes:", + 'config-hint' => "Si vous ne savez pas comment définir les entrées DNS pour votre domaine, veuillez contacter le service d'enregistrement auprès duquel vous avez enregistré le domaine ou votre fournisseur d'hébergement Web.", + 'spf-whitelist' => "SPF Whitelist", + 'spf-whitelist-text' => "Le Sender Policy Framework permet à un domaine expéditeur de dévoiler, par le biais de DNS," + . " quels systèmes sont autorisés à envoyer des e-mails avec une adresse d'expéditeur d'enveloppe dans le domaine en question.", + 'spf-whitelist-ex' => "Vous pouvez ici spécifier une liste de serveurs autorisés, par exemple: .ess.barracuda.com.", + ], + + 'error' => [ + '400' => "Mauvaide demande", + '401' => "Non autorisé", + '403' => "Accès refusé", + '404' => "Pas trouvé", + '405' => "Méthode non autorisée", + '500' => "Erreur de serveur interne", + 'unknown' => "Erreur inconnu", + 'server' => "Erreur de serveur", + ], + + 'form' => [ + 'amount' => "Montant", + 'code' => "Le code de confirmation", + 'config' => "Configuration", + 'date' => "Date", + 'description' => "Description", + 'details' => "Détails", + 'domain' => "Domaine", + 'email' => "Adresse e-mail", + 'firstname' => "Prénom", + 'lastname' => "Nom de famille", + 'none' => "aucun", + 'or' => "ou", + 'password' => "Mot de passe", + 'password-confirm' => "Confirmer le mot de passe", + 'phone' => "Téléphone", + 'status' => "État", + 'surname' => "Nom de famille", + 'user' => "Utilisateur", + 'primary-email' => "Email principal", + 'id' => "ID", + 'created' => "Créé", + 'deleted' => "Supprimé", + 'disabled' => "Désactivé", + 'enabled' => "Activé", + 'general' => "Général", + 'settings' => "Paramètres", + ], + + 'invitation' => [ + 'create' => "Créez des invitation(s)", + 'create-title' => "Invitation à une inscription", + 'create-email' => "Saisissez l'adresse électronique de la personne que vous souhaitez inviter.", + 'create-csv' => "Pour envoyer plusieurs invitations à la fois, fournissez un fichier CSV (séparé par des virgules) ou un fichier en texte brut, contenant une adresse e-mail par ligne.", + 'empty-list' => "Il y a aucune invitation dans la mémoire de données.", + 'title' => "Invitation d'inscription", + 'search' => "Adresse E-mail ou domaine", + 'send' => "Envoyer invitation(s)", + 'status-completed' => "Utilisateur s'est inscrit", + 'status-failed' => "L'envoi a échoué", + 'status-sent' => "Envoyé", + 'status-new' => "Pas encore envoyé", + ], + + 'lang' => [ + 'en' => "Anglais", + 'de' => "Allemand", + 'fr' => "Français", + 'it' => "Italien", + ], + + 'login' => [ + '2fa' => "Code du 2ème facteur", + '2fa_desc' => "Le code du 2ème facteur est facultatif pour les utilisateurs qui n'ont pas configuré l'authentification à deux facteurs.", + 'forgot_password' => "Mot de passe oublié?", + 'header' => "Veuillez vous connecter", + 'sign_in' => "Se connecter", + 'webmail' => "Webmail" + ], + + 'meet' => [ + 'title' => "Voix et vidéo-conférence", + 'welcome' => "Bienvenue dans notre programme bêta pour les conférences vocales et vidéo.", + 'url' => "Vous disposez d'une salle avec l'URL ci-dessous. Cette salle ouvre uniquement quand vous y êtes vous-même. Utilisez cette URL pour inviter des personnes à vous rejoindre.", + 'notice' => "Il s'agit d'un travail en évolution et d'autres fonctions seront ajoutées au fil du temps. Les fonctions actuelles sont les suivantes:", + 'sharing' => "Partage d'écran", + 'sharing-text' => "Partagez votre écran pour des présentations ou des exposés.", + 'security' => "sécurité de chambre", + 'security-text' => "Renforcez la sécurité de la salle en définissant un mot de passe que les participants devront connaître." + . " avant de pouvoir entrer, ou verrouiller la porte afin que les participants doivent frapper, et un modérateur peut accepter ou refuser ces demandes.", + 'qa' => "Lever la main (Q&A)", + 'qa-text' => "Les membres du public silencieux peuvent lever la main pour animer une séance de questions-réponses avec les membres du panel.", + 'moderation' => "Délégation des Modérateurs", + 'moderation-text' => "Déléguer l'autorité du modérateur pour la séance, afin qu'un orateur ne soit pas inutilement" + . " interrompu par l'arrivée des participants et d'autres tâches du modérateur.", + 'eject' => "Éjecter les participants", + 'eject-text' => "Éjectez les participants de la session afin de les obliger à se reconnecter ou de remédier aux violations des règles." + . " Cliquez sur l'icône de l'utilisateur pour un renvoi effectif.", + 'silent' => "Membres du Public en Silence", + 'silent-text' => "Pour une séance de type webinaire, configurez la salle pour obliger tous les nouveaux participants à être des spectateurs silencieux.", + 'interpreters' => "Canaux d'Audio Spécifiques de Langues", + 'interpreters-text' => "Désignez un participant pour interpréter l'audio original dans une langue cible, pour les sessions avec des participants multilingues." + . " L'interprète doit être capable de relayer l'audio original et de le remplacer.", + 'beta-notice' => "Rappelez-vous qu'il s'agit d'une version bêta et pourrait entraîner des problèmes." + . " Au cas où vous rencontreriez des problèmes, n'hésitez pas à nous en faire part en contactant le support.", + + // Room options dialog + 'options' => "Options de salle", + 'password' => "Mot de passe", + 'password-none' => "aucun", + 'password-clear' => "Effacer mot de passe", + 'password-set' => "Définir le mot de passe", + 'password-text' => "Vous pouvez ajouter un mot de passe à votre session. Les participants devront fournir le mot de passe avant d'être autorisés à rejoindre la session.", + 'lock' => "Salle verrouillée", + 'lock-text' => "Lorsque la salle est verrouillée, les participants doivent être approuvés par un modérateur avant de pouvoir rejoindre la réunion.", + 'nomedia' => "Réservé aux abonnés", + 'nomedia-text' => "Force tous les participants à se joindre en tant qu'abonnés (avec caméra et microphone désactivés)" + . "Les modérateurs pourront les promouvoir en tant qu'éditeurs tout au long de la session.", + + // Room menu + 'partcnt' => "Nombres de participants", + 'menu-audio-mute' => "Désactiver le son", + 'menu-audio-unmute' => "Activer le son", + 'menu-video-mute' => "Désactiver la vidéo", + 'menu-video-unmute' => "Activer la vidéo", + 'menu-screen' => "Partager l'écran", + 'menu-hand-lower' => "Baisser la main", + 'menu-hand-raise' => "Lever la main", + 'menu-channel' => "Canal de langue interprétée", + 'menu-chat' => "Le Chat", + 'menu-fullscreen' => "Plein écran", + 'menu-fullscreen-exit' => "Sortir en plein écran", + 'menu-leave' => "Quitter la session", + + // Room setup screen + 'setup-title' => "Préparez votre session", + 'mic' => "Microphone", + 'cam' => "Caméra", + 'nick' => "Surnom", + 'nick-placeholder' => "Votre nom", + 'join' => "JOINDRE", + 'joinnow' => "JOINDRE MAINTENANT", + 'imaowner' => "Je suis le propriétaire", + + // Room + 'qa' => "Q & A", + 'leave-title' => "Salle fermée", + 'leave-body' => "La session a été fermée par le propriétaire de la salle.", + 'media-title' => "Configuration des médias", + 'join-request' => "Demande de rejoindre", + 'join-requested' => "{user} demandé à rejoindre.", + + // Status messages + 'status-init' => "Vérification de la salle...", + 'status-323' => "La salle est fermée. Veuillez attendre le démarrage de la session par le propriétaire.", + 'status-324' => "La salle est fermée. Elle sera ouverte aux autres participants après votre adhésion.", + 'status-325' => "La salle est prête. Veuillez entrer un mot de passe valide.", + 'status-326' => "La salle est fermée. Veuillez entrer votre nom et réessayer.", + 'status-327' => "En attendant la permission de joindre la salle.", + 'status-404' => "La salle n'existe pas.", + 'status-429' => "Trop de demande. Veuillez, patienter.", + 'status-500' => "La connexion à la salle a échoué. Erreur de serveur.", + + // Other menus + 'media-setup' => "configuration des médias", + 'perm' => "Permissions", + 'perm-av' => "Publication d'audio et vidéo", + 'perm-mod' => "Modération", + 'lang-int' => "Interprète de langue", + 'menu-options' => "Options", + ], + + 'menu' => [ + 'cockpit' => "Cockpit", + 'login' => "Connecter", + 'logout' => "Deconnecter", + 'signup' => "S'inscrire", + 'toggle' => "Basculer la navigation", + ], + + 'msg' => [ + 'initializing' => "Initialisation...", + 'loading' => "Chargement...", + 'loading-failed' => "Échec du chargement des données.", + 'notfound' => "Resource introuvable.", + 'info' => "Information", + 'error' => "Erreur", + 'warning' => "Avertissement", + 'success' => "Succès", + ], + + 'nav' => [ + 'more' => "Charger plus", + 'step' => "Étape {i}/{n}", + ], + + 'password' => [ + 'reset' => "Réinitialiser le mot de passe", + 'reset-step1' => "Entrez votre adresse e-mail pour réinitialiser votre mot de passe.", + 'reset-step1-hint' => "Veuillez vérifier votre dossier de spam ou débloquer {email}.", + 'reset-step2' => "Nous avons envoyé un code de confirmation à votre adresse e-mail externe." + . " Entrez le code que nous vous avons envoyé, ou cliquez sur le lien dans le message.", + ], + + 'signup' => [ + 'email' => "Adresse e-mail actuelle", + 'login' => "connecter", + 'title' => "S'inscrire", + 'step1' => "Inscrivez-vous pour commencer votre mois gratuit.", + 'step2' => "Nous avons envoyé un code de confirmation à votre adresse e-mail. Entrez le code que nous vous avons envoyé, ou cliquez sur le lien dans le message.", + 'step3' => "Créez votre identité Kolab (vous pourrez choisir des adresses supplémentaires plus tard).", + 'voucher' => "Coupon Code", + ], + + 'status' => [ + 'prepare-account' => "Votre compte est en cours de préparation.", + 'prepare-domain' => "Le domain est en cours de préparation.", + 'prepare-distlist' => "La liste de distribution est en cours de préparation.", + 'prepare-user' => "Le compte d'utilisateur est en cours de préparation.", + 'prepare-hint' => "Certaines fonctionnalités peuvent être manquantes ou en lecture seule pour le moment.", + 'prepare-refresh' => "Le processus ne se termine jamais? Appuyez sur le bouton \"Refresh\", s'il vous plaît.", + 'ready-account' => "Votre compte est presque prêt.", + 'ready-domain' => "Le domaine est presque prêt.", + 'ready-distlist' => "La liste de distribution est presque prête.", + 'ready-user' => "Le compte d'utilisateur est presque prêt.", + 'verify' => "Veuillez vérifier votre domaine pour terminer le processus de configuration.", + 'verify-domain' => "Vérifier domaine", + 'deleted' => "Supprimé", + 'suspended' => "Suspendu", + 'notready' => "Pas Prêt", + 'active' => "Actif", + ], + + 'support' => [ + 'title' => "Contacter Support", + 'id' => "Numéro de client ou adresse é-mail que vous avez chez nous.", + 'id-pl' => "e.g. 12345678 ou john@kolab.org", + 'id-hint' => "Laissez vide si vous n'êtes pas encore client", + 'name' => "Nom", + 'name-pl' => "comment nous devons vous adresser dans notre réponse", + 'email' => "adresse e-mail qui fonctionne", + 'email-pl' => "assurez-vous que nous pouvons vous atteindre à cette adresse", + 'summary' => "Résumé du problème", + 'summary-pl' => "une phrase qui résume votre situation", + 'expl' => "Analyse du problème", + ], + + 'user' => [ + '2fa-hint1' => "Cela éliminera le droit à l'authentification à 2-Facteurs ainsi que les éléments configurés par l'utilisateur.", + '2fa-hint2' => "Veuillez vous assurer que l'identité de l'utilisateur est correctement confirmée.", + 'address' => "Adresse", + 'aliases' => "Alias", + 'aliases-email' => "Alias E-mail", + 'aliases-none' => "Cet utilisateur n'aucune alias e-mail.", + 'add-bonus' => "Ajouter un bonus", + 'add-bonus-title' => "Ajouter un bonus au portefeuille", + 'add-penalty' => "Ajouter une pénalité", + 'add-penalty-title' => "Ajouter une pénalité au portefeuille", + 'auto-payment' => "Auto-paiement", + 'auto-payment-text' => "Recharger par {amount} quand le montant est inférieur à {balance} utilisant {method}", + 'country' => "Pays", + 'create' => "Créer un utilisateur", + 'custno' => "No. de Client.", + 'delete' => "Supprimer Utilisateur", + 'delete-email' => "Supprimer {email}", + 'delete-text' => "Voulez-vous vraiment supprimer cet utilisateur de façon permanente?" + . " Cela supprimera toutes les données du compte et retirera la permission d'accéder au compte d'e-email." + . " Veuillez noter que cette action ne peut pas être révoquée.", + 'discount' => "Rabais", + 'discount-hint' => "rabais appliqué", + 'discount-title' => "Rabais de compte", + 'distlists' => "Listes de Distribution", + 'distlists-none' => "Il y a aucune liste de distribution dans ce compte.", + 'domains' => "Domaines", + 'domains-none' => "Il y a pas de domaines dans ce compte.", + 'ext-email' => "E-mail externe", + 'finances' => "Finances", + 'greylisting' => "Greylisting", + 'greylisting-text' => "La greylisting est une méthode de défense des utilisateurs contre le spam." + . " Tout e-mail entrant provenant d'un expéditeur non reconnu est temporairement rejeté." + . " Le serveur d'origine doit réessayer après un délai cette fois-ci, le mail sera accepté." + . " Les spammeurs ne réessayent généralement pas de remettre le mail.", + 'list-title' => "Comptes d'utilisateur", + 'managed-by' => "Géré par", + 'new' => "Nouveau compte d'utilisateur", + 'org' => "Organisation", + 'package' => "Paquet", + 'price' => "Prix", + 'profile-title' => "Votre profile", + 'profile-delete' => "Supprimer compte", + 'profile-delete-title' => "Supprimer ce compte?", + 'profile-delete-text1' => "Cela supprimera le compte ainsi que tous les domaines, utilisateurs et alias associés à ce compte.", + 'profile-delete-warning' => "Cette opération est irrévocable", + 'profile-delete-text2' => "Comme vous ne pourrez plus rien récupérer après ce point, assurez-vous d'avoir migré toutes les données avant de poursuivre.", + 'profile-delete-support' => "Étant donné que nous nous attachons à toujours nous améliorer, nous aimerions vous demander 2 minutes de votre temps. " + . "Le meilleur moyen de nous améliorer est le feedback des utilisateurs, et nous voudrions vous demander" + . "quelques mots sur les raisons pour lesquelles vous avez quitté notre service. Veuillez envoyer vos commentaires au {email}.", + 'profile-delete-contact' => "Par ailleurs, n'hésitez pas à contacter le support de {app} pour toute question ou souci que vous pourriez avoir dans ce contexte.", + 'reset-2fa' => "Réinitialiser l'authentification à 2-Facteurs.", + 'reset-2fa-title' => "Réinitialisation de l'Authentification à 2-Facteurs", + 'title' => "Compte d'utilisateur", + 'search-pl' => "ID utilisateur,e-mail ou domamine", + 'skureq' => "{sku} demande {list}.", + 'subscription' => "Subscription", + 'subscriptions' => "Subscriptions", + 'subscriptions-none' => "Cet utilisateur n'a pas de subscriptions.", + 'users' => "Utilisateurs", + 'users-none' => "Il n'y a aucun utilisateur dans ce compte.", + ], + + 'wallet' => [ + 'add-credit' => "Ajouter un crédit", + 'auto-payment-cancel' => "Annuler l'auto-paiement", + 'auto-payment-change' => "Changer l'auto-paiement", + 'auto-payment-failed' => "La configuration des paiements automatiques a échoué. Redémarrer le processus pour activer les top-ups automatiques.", + 'auto-payment-hint' => "Cela fonctionne de la manière suivante: Chaque fois que votre compte est épuisé, nous débiterons votre méthode de paiement préférée d'un montant que vous aurez défini." + . " Vous pouvez annuler ou modifier l'option de paiement automatique à tout moment.", + 'auto-payment-setup' => "configurer l'auto-paiement", + 'auto-payment-disabled' => "L'auto-paiement configuré a été désactivé. Rechargez votre porte-monnaie ou augmentez le montant d'auto-paiement.", + 'auto-payment-info' => "L'auto-paiement est set pour recharger votre compte par {amount} lorsque le solde de votre compte devient inférieur à {balance}.", + 'auto-payment-inprogress' => "La configuration d'auto-paiement est toujours en cours.", + 'auto-payment-next' => "Ensuite, vous serez redirigé vers la page de paiement, où vous pourrez fournir les coordonnées de votre carte de crédit.", + 'auto-payment-disabled-next' => "L'auto-paiement est désactivé. Dès que vous aurez soumis de nouveaux paramètres, nous l'activerons et essaierons de recharger votre portefeuille.", + 'auto-payment-update' => "Mise à jour de l'auto-paiement.", + 'banktransfer-hint' => "Veuillez noter qu'un virement bancaire peut nécessiter plusieurs jours avant d'être effectué.", + 'currency-conv' => "Le principe est le suivant: Vous spécifiez le montant dont vous voulez recharger votre portefeuille en {wc}." + . " Nous convertirons ensuite ce montant en {pc}, et sur la page suivante, vous obtiendrez les coordonnées bancaires pour transférer le montant en {pc}.", + 'fill-up' => "Recharger par", + 'history' => "Histoire", + 'month' => "mois", + 'noperm' => "Seuls les propriétaires de compte peuvent accéder à un portefeuille.", + 'payment-amount-hint' => "Choisissez le montant dont vous voulez recharger votre portefeuille.", + 'payment-method' => "Mode de paiement: {method}", + 'payment-warning' => "Vous serez facturé pour {price}.", + 'pending-payments' => "Paiements en attente", + 'pending-payments-warning' => "Vous avez des paiements qui sont encore en cours. Voir l'onglet \"Paiements en attente\" ci-dessous.", + 'pending-payments-none' => "Il y a aucun paiement en attente pour ce compte.", + 'receipts' => "Reçus", + 'receipts-hint' => "Vous pouvez télécharger ici les reçus (au format PDF) pour les paiements de la période spécifiée. Sélectionnez la période et appuyez sur le bouton Télécharger.", + 'receipts-none' => "Il y a aucun reçu pour les paiements de ce compte. Veuillez noter que vous pouvez télécharger les reçus après la fin du mois.", + 'title' => "Solde du compte", + 'top-up' => "Rechargez votre portefeuille", + 'transactions' => "Transactions", + 'transactions-none' => "Il y a aucun transaction pour ce compte.", + 'when-below' => "lorsque le solde du compte est inférieur à", + ], +]; diff --git a/src/resources/lang/fr/validation.php b/src/resources/lang/fr/validation.php new file mode 100644 index 00000000..f1280fa0 --- /dev/null +++ b/src/resources/lang/fr/validation.php @@ -0,0 +1,187 @@ + 'Le champ :attribute doit être accepté.', + 'active_url' => 'Le champ :attribute n\'est pas une URL valide.', + 'after' => 'Le champ :attribute doit être une date postérieure au :date.', + 'after_or_equal' => 'Le champ :attribute doit être une date postérieure ou égale au :date.', + 'alpha' => 'Le champ :attribute doit contenir uniquement des lettres.', + 'alpha_dash' => 'Le champ :attribute doit contenir uniquement des lettres, des chiffres et des tirets.', + 'alpha_num' => 'Le champ :attribute doit contenir uniquement des chiffres et des lettres.', + 'array' => 'Le champ :attribute doit être un tableau.', + 'attached' => ':attribute est déjà attaché(e).', + 'before' => 'Le champ :attribute doit être une date antérieure au :date.', + 'before_or_equal' => 'Le champ :attribute doit être une date antérieure ou égale au :date.', + 'between' => [ + 'array' => 'Le tableau :attribute doit contenir entre :min et :max éléments.', + 'file' => 'La taille du fichier de :attribute doit être comprise entre :min et :max kilo-octets.', + 'numeric' => 'La valeur de :attribute doit être comprise entre :min et :max.', + 'string' => 'Le texte :attribute doit contenir entre :min et :max caractères.', + ], + 'boolean' => 'Le champ :attribute doit être vrai ou faux.', + 'confirmed' => 'Le champ de confirmation :attribute ne correspond pas.', + 'current_password' => 'Le mot de passe est incorrect.', + 'date' => 'Le champ :attribute n\'est pas une date valide.', + 'date_equals' => 'Le champ :attribute doit être une date égale à :date.', + 'date_format' => 'Le champ :attribute ne correspond pas au format :format.', + 'different' => 'Les champs :attribute et :other doivent être différents.', + 'digits' => 'Le champ :attribute doit contenir :digits chiffres.', + 'digits_between' => 'Le champ :attribute doit contenir entre :min et :max chiffres.', + 'dimensions' => 'La taille de l\'image :attribute n\'est pas conforme.', + 'distinct' => 'Le champ :attribute a une valeur en double.', + 'email' => 'Le champ :attribute doit être une adresse email valide.', + 'ends_with' => 'Le champ :attribute doit se terminer par une des valeurs suivantes : :values', + 'exists' => 'Le champ :attribute sélectionné est invalide.', + 'file' => 'Le champ :attribute doit être un fichier.', + 'filled' => 'Le champ :attribute doit avoir une valeur.', + 'gt' => [ + 'array' => 'Le tableau :attribute doit contenir plus de :value éléments.', + 'file' => 'La taille du fichier de :attribute doit être supérieure à :value kilo-octets.', + 'numeric' => 'La valeur de :attribute doit être supérieure à :value.', + 'string' => 'Le texte :attribute doit contenir plus de :value caractères.', + ], + 'gte' => [ + 'array' => 'Le tableau :attribute doit contenir au moins :value éléments.', + 'file' => 'La taille du fichier de :attribute doit être supérieure ou égale à :value kilo-octets.', + 'numeric' => 'La valeur de :attribute doit être supérieure ou égale à :value.', + 'string' => 'Le texte :attribute doit contenir au moins :value caractères.', + ], + 'image' => 'Le champ :attribute doit être une image.', + 'in' => 'Le champ :attribute est invalide.', + 'in_array' => 'Le champ :attribute n\'existe pas dans :other.', + 'integer' => 'Le champ :attribute doit être un entier.', + 'ip' => 'Le champ :attribute doit être une adresse IP valide.', + 'ipv4' => 'Le champ :attribute doit être une adresse IPv4 valide.', + 'ipv6' => 'Le champ :attribute doit être une adresse IPv6 valide.', + 'json' => 'Le champ :attribute doit être un document JSON valide.', + 'lt' => [ + 'array' => 'Le tableau :attribute doit contenir moins de :value éléments.', + 'file' => 'La taille du fichier de :attribute doit être inférieure à :value kilo-octets.', + 'numeric' => 'La valeur de :attribute doit être inférieure à :value.', + 'string' => 'Le texte :attribute doit contenir moins de :value caractères.', + ], + 'lte' => [ + 'array' => 'Le tableau :attribute doit contenir au plus :value éléments.', + 'file' => 'La taille du fichier de :attribute doit être inférieure ou égale à :value kilo-octets.', + 'numeric' => 'La valeur de :attribute doit être inférieure ou égale à :value.', + 'string' => 'Le texte :attribute doit contenir au plus :value caractères.', + ], + 'max' => [ + 'array' => 'Le tableau :attribute ne peut contenir plus de :max éléments.', + 'file' => 'La taille du fichier de :attribute ne peut pas dépasser :max kilo-octets.', + 'numeric' => 'La valeur de :attribute ne peut être supérieure à :max.', + 'string' => 'Le texte de :attribute ne peut contenir plus de :max caractères.', + ], + 'mimes' => 'Le champ :attribute doit être un fichier de type : :values.', + 'mimetypes' => 'Le champ :attribute doit être un fichier de type : :values.', + 'min' => [ + 'array' => 'Le tableau :attribute doit contenir au moins :min éléments.', + 'file' => 'La taille du fichier de :attribute doit être supérieure à :min kilo-octets.', + 'numeric' => 'La valeur de :attribute doit être supérieure ou égale à :min.', + 'string' => 'Le texte :attribute doit contenir au moins :min caractères.', + ], + 'multiple_of' => 'La valeur de :attribute doit être un multiple de :value', + 'not_in' => 'Le champ :attribute sélectionné n\'est pas valide.', + 'not_regex' => 'Le format du champ :attribute n\'est pas valide.', + 'numeric' => 'Le champ :attribute doit contenir un nombre.', + 'password' => 'Le mot de passe est incorrect', + 'present' => 'Le champ :attribute doit être présent.', + 'prohibited' => 'Le champ :attribute est interdit.', + 'prohibited_if' => 'Le champ :attribute est interdit quand :other a la valeur :value.', + 'prohibited_unless' => 'Le champ :attribute est interdit à moins que :other est l\'une des valeurs :values.', + 'regex' => 'Le format du champ :attribute est invalide.', + 'relatable' => ':attribute n\'est sans doute pas associé(e) avec cette donnée.', + 'required' => 'Le champ :attribute est obligatoire.', + 'required_if' => 'Le champ :attribute est obligatoire quand la valeur de :other est :value.', + 'required_unless' => 'Le champ :attribute est obligatoire sauf si :other est :values.', + 'required_with' => 'Le champ :attribute est obligatoire quand :values est présent.', + 'required_with_all' => 'Le champ :attribute est obligatoire quand :values sont présents.', + 'required_without' => 'Le champ :attribute est obligatoire quand :values n\'est pas présent.', + 'required_without_all' => 'Le champ :attribute est requis quand aucun de :values n\'est présent.', + 'same' => 'Les champs :attribute et :other doivent être identiques.', + 'size' => [ + 'array' => 'Le tableau :attribute doit contenir :size éléments.', + 'file' => 'La taille du fichier de :attribute doit être de :size kilo-octets.', + 'numeric' => 'La valeur de :attribute doit être :size.', + 'string' => 'Le texte de :attribute doit contenir :size caractères.', + ], + 'starts_with' => 'Le champ :attribute doit commencer avec une des valeurs suivantes : :values', + 'string' => 'Le champ :attribute doit être une chaîne de caractères.', + 'timezone' => 'Le champ :attribute doit être un fuseau horaire valide.', + 'unique' => 'La valeur du champ :attribute est déjà utilisée.', + 'uploaded' => 'Le fichier du champ :attribute n\'a pu être téléversé.', + 'url' => 'Le format de l\'URL de :attribute n\'est pas valide.', + 'uuid' => 'Le champ :attribute doit être un UUID valide', + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + 'attributes' => [ + 'address' => 'adresse', + 'age' => 'âge', + 'available' => 'disponible', + 'city' => 'ville', + 'content' => 'contenu', + 'country' => 'pays', + 'current_password' => 'mot de passe actuel', + 'date' => 'date', + 'day' => 'jour', + 'description' => 'description', + 'email' => 'adresse email', + 'excerpt' => 'extrait', + 'first_name' => 'prénom', + 'gender' => 'genre', + 'hour' => 'heure', + 'last_name' => 'nom', + 'minute' => 'minute', + 'mobile' => 'portable', + 'month' => 'mois', + 'name' => 'nom', + 'password' => 'mot de passe', + 'password_confirmation' => 'confirmation du mot de passe', + 'phone' => 'téléphone', + 'second' => 'seconde', + 'sex' => 'sexe', + 'size' => 'taille', + 'time' => 'heure', + 'title' => 'titre', + 'username' => 'nom d\'utilisateur', + 'year' => 'année', + ], + '2fareq' => "Le code du second facteur est requis.", + '2fainvalid' => "Le code du deuxième facteur n'est pas valideSecond factor code is invalid.", + 'emailinvalid' => "L'adresse e-mail spécifiée est invalide.", + 'domaininvalid' => "Le domaine spécifié n'est pas valide.", + 'domainnotavailable' => "Le domaine spécifié n'est pas disponible.", + 'logininvalid' => "Le login spécifié est invalide.", + 'loginexists' => "Le login spécifié n'est pas disponible.", + 'domainexists' => "Le domaine spécifié n'est pas disponible.", + 'noemailorphone' => "Le texte spécifié n'est pas un e-mail valide ni un numéro de téléphone.", + 'packageinvalid' => "Le paquet sélectionné est invalide.", + 'packagerequired' => "Le paquet est requis.", + 'usernotexists' => "Impossible de trouver l'utilisateur.", + 'voucherinvalid' => "Le code du coupon est invalide ou a expiré.", + 'noextemail' => "Cet utilisateur ne possède pas d'adresse e-mail externe.", + 'entryinvalid' => "L'attribut :attribute est invalide.", + 'entryexists' => "L'attribut :attribute n'est pas disponible.", + 'minamount' => "Le montant minimum pour un paiement unitaire est :amount.", + 'minamountdebt' => "Le montant indiqué ne couvre pas le solde du compte.", + 'notalocaluser' => "L'adresse e-mail indiquée n'existe pas.", + 'memberislist' => "Le destinataire ne peut pas être le même que l'adresse de la liste.", + 'listmembersrequired' => "Au moins un destinataire est requis.", + 'spf-entry-invalid' => "Le format de l'entrée est invalide. Un nom de domaine débutant par un point est attendu.", + 'invalid-config-parameter' => "Le paramètre de configuration demandé est inconnu.", + +]; diff --git a/src/resources/themes/default/lang/fr/faq.php b/src/resources/themes/default/lang/fr/faq.php new file mode 100644 index 00000000..875855ce --- /dev/null +++ b/src/resources/themes/default/lang/fr/faq.php @@ -0,0 +1,9 @@ + "Est-il possible de convertir un compte individuel en compte de groupe?", + 'storage' => "Combien d'espace de stockage est fourni avec mon compte?", + 'tos' => "quelles sont vos conditions de service?", + +]; diff --git a/src/resources/themes/default/lang/fr/menu.php b/src/resources/themes/default/lang/fr/menu.php new file mode 100644 index 00000000..7939b1c8 --- /dev/null +++ b/src/resources/themes/default/lang/fr/menu.php @@ -0,0 +1,10 @@ + "Blog", + 'explore' => "Explorer", + 'support' => "Support", + 'tos' => "Conditions de Service", + +]; diff --git a/src/resources/themes/default/lang/fr/support.php b/src/resources/themes/default/lang/fr/support.php new file mode 100644 index 00000000..dd9c9341 --- /dev/null +++ b/src/resources/themes/default/lang/fr/support.php @@ -0,0 +1,13 @@ + "Contacter Support", + 'text1' => "Notre équipe de support technique est là pour vous aider si vous rencontrez des difficultés." + . " Vous ne devriez pas avoir à parler à des machines ou à naviguer dans des menus vocaux," + . " mais plutôt à des êtres humains qui vous répondent personnellement.", + 'text2' => "Cette aide est déjà intégrée dans votre souscription, il n'y a donc aucun coût supplémentaire pour vous." + . " Si vous rencontrez des problèmes avec votre compte :site, ou si vous avez des questions" + . " sur notre produit avant de vous inscrire, veuillez nous contacter.", + +]; diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php index 6f7b6aef..472f8db7 100644 --- a/src/tests/Browser/LogonTest.php +++ b/src/tests/Browser/LogonTest.php @@ -1,289 +1,291 @@ browse(function (Browser $browser) { $browser->visit(new Home()) ->within(new Menu(), function ($browser) { $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login', 'lang']) ->assertSeeIn('#footer-copyright', '@ Apheleia IT AG, ' . date('Y')); }); if ($browser->isDesktop()) { $browser->within(new Menu('footer'), function ($browser) { $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'login']); }); } else { $browser->assertMissing('#footer-menu .navbar-nav'); } $browser->assertSeeLink('Forgot password?') ->assertSeeLink('Webmail'); }); } /** * Test language menu, and language change */ public function testLocales(): void { $this->browse(function (Browser $browser) { if (!$browser->isDesktop()) { $this->markTestIncomplete(); } $browser->visit(new Home()) // ->plainCookie('language', '') ->within(new Menu(), function ($browser) { $browser->assertSeeIn('@lang', 'EN') ->click('@lang'); }) // Switch English -> German ->whenAvailable('nav .dropdown-menu', function (Browser $browser) { - $browser->assertElementsCount('a', 2) + $browser->assertElementsCount('a', 3) ->assertSeeIn('a:nth-child(1)', 'EN - English') ->assertSeeIn('a:nth-child(2)', 'DE - German') + ->assertSeeIn('a:nth-child(3)', 'FR - French') ->click('a:nth-child(2)'); }) ->waitUntilMissing('nav .dropdown-menu') ->within(new Menu(), function ($browser) { $browser->assertSeeIn('@lang', 'DE'); }) ->waitForTextIn('#header-menu .link-login', 'EINLOGGEN') ->assertSeeIn('#footer-menu .link-login', 'Einloggen') ->assertSeeIn('@logon-button', 'Anmelden') // refresh the page to see if it uses the lang previously set ->refresh() ->waitForTextIn('#header-menu .link-login', 'EINLOGGEN') ->assertSeeIn('#footer-menu .link-login', 'Einloggen') ->assertSeeIn('@logon-button', 'Anmelden') ->within(new Menu(), function ($browser) { $browser->assertSeeIn('@lang', 'DE') ->click('@lang'); }) // Switch German -> English ->whenAvailable('nav .dropdown-menu', function (Browser $browser) { - $browser->click('a:nth-child(1)'); + $browser->assertSeeIn('a:nth-child(1)', 'Englisch') + ->click('a:nth-child(1)'); }) ->waitUntilMissing('nav .dropdown-menu') ->within(new Menu(), function ($browser) { $browser->assertSeeIn('@lang', 'EN'); }) ->waitForTextIn('#header-menu .link-login', 'LOGIN'); }); } /** * Test redirect to /login if user is unauthenticated */ public function testRequiredAuth(): void { $this->browse(function (Browser $browser) { $browser->visit('/dashboard'); // Checks if we're really on the login page $browser->waitForLocation('/login') ->on(new Home()); }); } /** * Logon with wrong password/user test */ public function testLogonWrongCredentials(): void { $this->browse(function (Browser $browser) { $browser->visit(new Home()) ->submitLogon('john@kolab.org', 'wrong'); // Error message $browser->assertToast(Toast::TYPE_ERROR, 'Invalid username or password.'); // Checks if we're still on the logon page $browser->on(new Home()); }); } /** * Successful logon test */ public function testLogonSuccessful(): void { $this->browse(function (Browser $browser) { $browser->visit(new Home()) ->submitLogon('john@kolab.org', 'simple123', true) // Checks if we're really on Dashboard page ->on(new Dashboard()) ->assertVisible('@links a.link-profile') ->assertVisible('@links a.link-domains') ->assertVisible('@links a.link-users') ->assertVisible('@links a.link-wallet') ->assertVisible('@links a.link-webmail') ->within(new Menu(), function ($browser) { $browser->assertMenuItems(['explore', 'blog', 'support', 'dashboard', 'logout', 'lang']); }); if ($browser->isDesktop()) { $browser->within(new Menu('footer'), function ($browser) { $browser->assertMenuItems(['explore', 'blog', 'support', 'tos', 'dashboard', 'logout']); }); } else { $browser->assertMissing('#footer-menu .navbar-nav'); } $browser->assertUser('john@kolab.org'); // Assert no "Account status" for this account $browser->assertMissing('@status'); // Goto /domains and assert that the link on logo element // leads to the dashboard $browser->visit('/domains') ->waitForText('Domains') ->click('a.navbar-brand') ->on(new Dashboard()); // Test that visiting '/' with logged in user does not open logon form // but "redirects" to the dashboard $browser->visit('/') ->waitForLocation('/dashboard') ->on(new Dashboard()); }); } /** * Logout test * * @depends testLogonSuccessful */ public function testLogout(): void { $this->browse(function (Browser $browser) { $browser->on(new Dashboard()); // Click the Logout button $browser->within(new Menu(), function ($browser) { $browser->clickMenuItem('logout'); }); // We expect the logon page $browser->waitForLocation('/login') ->on(new Home()); // with default menu $browser->within(new Menu(), function ($browser) { $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login', 'lang']); }); // Success toast message $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out'); }); } /** * Logout by URL test */ public function testLogoutByURL(): void { $this->browse(function (Browser $browser) { $browser->visit(new Home()) ->submitLogon('john@kolab.org', 'simple123', true); // Checks if we're really on Dashboard page $browser->on(new Dashboard()); // Use /logout url, and expect the logon page $browser->visit('/logout') ->waitForLocation('/login') ->on(new Home()); // with default menu $browser->within(new Menu(), function ($browser) { $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login', 'lang']); }); // Success toast message $browser->assertToast(Toast::TYPE_SUCCESS, 'Successfully logged out'); }); } /** * Test 2-Factor Authentication * * @depends testLogoutByURL */ public function test2FA(): void { $this->browse(function (Browser $browser) { // Test missing 2fa code $browser->on(new Home()) ->type('@email-input', 'ned@kolab.org') ->type('@password-input', 'simple123') ->press('form button') ->waitFor('@second-factor-input.is-invalid + .invalid-feedback') ->assertSeeIn( '@second-factor-input.is-invalid + .invalid-feedback', 'Second factor code is required.' ) ->assertFocused('@second-factor-input') ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); // Test invalid code $browser->type('@second-factor-input', '123456') ->press('form button') ->waitUntilMissing('@second-factor-input.is-invalid') ->waitFor('@second-factor-input.is-invalid + .invalid-feedback') ->assertSeeIn( '@second-factor-input.is-invalid + .invalid-feedback', 'Second factor code is invalid.' ) ->assertFocused('@second-factor-input') ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); $code = \App\Auth\SecondFactor::code('ned@kolab.org'); // Test valid (TOTP) code $browser->type('@second-factor-input', $code) ->press('form button') ->waitUntilMissing('@second-factor-input.is-invalid') ->waitForLocation('/dashboard') ->on(new Dashboard()); }); } /** * Test redirect to the requested page after logon * * @depends test2FA */ public function testAfterLogonRedirect(): void { $this->browse(function (Browser $browser) { // User is logged in $browser->visit(new UserProfile()); // Test redirect if the token is invalid $browser->script("localStorage.setItem('token', '123')"); $browser->refresh() ->on(new Home()) ->submitLogon('john@kolab.org', 'simple123', false) ->waitForLocation('/profile'); }); } }