Page MenuHomePhorge

D2395.1775779324.diff
No OneTemporary

Authored By
Unknown
Size
22 KB
Referenced Files
None
Subscribers
None

D2395.1775779324.diff

diff --git a/src/.env.example b/src/.env.example
--- a/src/.env.example
+++ b/src/.env.example
@@ -6,6 +6,8 @@
APP_PUBLIC_URL=
APP_DOMAIN=kolabnow.com
APP_THEME=default
+APP_LOCALE=en
+APP_LOCALES=en,de
ASSET_URL=http://127.0.0.1:8000
diff --git a/src/app/Http/Controllers/ContentController.php b/src/app/Http/Controllers/ContentController.php
--- a/src/app/Http/Controllers/ContentController.php
+++ b/src/app/Http/Controllers/ContentController.php
@@ -17,14 +17,17 @@
abort(404);
}
+ $theme = \config('app.theme');
$page = str_replace('/', '.', $page);
- $file = sprintf('themes/%s/pages/%s.blade.php', \config('app.theme'), $page);
- $view = sprintf('%s.pages.%s', \config('app.theme'), $page);
+ $file = sprintf('themes/%s/pages/%s.blade.php', $theme, $page);
+ $view = sprintf('%s.pages.%s', $theme, $page);
if (!file_exists(resource_path($file))) {
abort(404);
}
+ self::loadLocale($theme);
+
return view($view)->with('env', \App\Utils::uiEnv());
}
@@ -59,4 +62,16 @@
return response()->json(['status' => 'success', 'faq' => $faq]);
}
+
+ /**
+ * 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/app/Http/Kernel.php b/src/app/Http/Kernel.php
--- a/src/app/Http/Kernel.php
+++ b/src/app/Http/Kernel.php
@@ -21,6 +21,7 @@
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\DevelConfig::class,
+ \App\Http\Middleware\Locale::class,
// FIXME: CORS handling added here, I didn't find a nice way
// to add this only to the API routes
// \App\Http\Middleware\Cors::class,
diff --git a/src/app/Http/Middleware/Locale.php b/src/app/Http/Middleware/Locale.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Middleware/Locale.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+
+class Locale
+{
+ /**
+ * Handle an incoming request.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ *
+ * @return mixed
+ */
+ public function handle($request, Closure $next)
+ {
+ $langDir = resource_path('lang');
+ $preferences = array_map(
+ function ($lang) {
+ return preg_replace('/[^a-z].*$/', '', strtolower($lang));
+ },
+ $request->getLanguages()
+ );
+
+ $default = config('app.locale');
+ $lang = null;
+
+ foreach ($preferences as $pref) {
+ if (!empty($pref) && ($pref == $default || file_exists("$langDir/$pref"))) {
+ $lang = $pref;
+ break;
+ }
+ }
+
+ if ($lang != $default) {
+ app()->setLocale($lang);
+ }
+
+ return $next($request);
+ }
+}
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -402,6 +402,12 @@
$env['menu'] = $menu;
+ if (!empty(\env('APP_LOCALES'))) {
+ $env['languages'] = preg_split('/\s*,\s*/', strtolower(trim(\env('APP_LOCALES'))));
+ } else {
+ $env['languages'] = ['en', 'de'];
+ }
+
return $env;
}
}
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -98,7 +98,7 @@
|
*/
- 'locale' => 'en',
+ 'locale' => env('APP_LOCALE', 'en'),
/*
|--------------------------------------------------------------------------
diff --git a/src/package-lock.json b/src/package-lock.json
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -1519,7 +1519,11 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
- "optional": true
+ "optional": true,
+ "requires": {
+ "bindings": "^1.5.0",
+ "nan": "^2.12.1"
+ }
},
"glob-parent": {
"version": "3.1.0",
@@ -2746,6 +2750,16 @@
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
+ "bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -5512,6 +5526,13 @@
"integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==",
"dev": true
},
+ "file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "dev": true,
+ "optional": true
+ },
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -7864,6 +7885,13 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
+ "nan": {
+ "version": "2.14.2",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
+ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
+ "dev": true,
+ "optional": true
+ },
"nanoid": {
"version": "3.1.22",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz",
@@ -13758,6 +13786,12 @@
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
"dev": true
},
+ "vue-i18n": {
+ "version": "8.24.2",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.24.2.tgz",
+ "integrity": "sha512-+TkAPBQw4Cp2bQrSPtPNkhET7XcWYjjDt1UjWYQs+xbT41q5OAl1I3IZyhg0drjn1nlC1K0f8sLVB/nshUcF1Q==",
+ "dev": true
+ },
"vue-loader": {
"version": "15.9.6",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.6.tgz",
diff --git a/src/package.json b/src/package.json
--- a/src/package.json
+++ b/src/package.json
@@ -34,6 +34,7 @@
"stylelint": "^13.12.0",
"stylelint-config-standard": "^20.0.0",
"vue": "^2.6.12",
+ "vue-i18n": "^8.24.1",
"vue-loader": "^15.9.6",
"vue-router": "^3.5.1",
"vue-template-compiler": "^2.6.12",
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -10,6 +10,7 @@
import MenuComponent from '../vue/Widgets/Menu'
import SupportForm from '../vue/Widgets/SupportForm'
import store from './store'
+import { loadLangAsync, i18n } from './locale'
const loader = '<div class="app-loader"><div class="spinner-border" role="status"><span class="sr-only">Loading</span></div></div>'
@@ -69,11 +70,11 @@
})
const app = new Vue({
- el: '#app',
components: {
AppComponent,
MenuComponent,
},
+ i18n,
store,
router: window.router,
data() {
@@ -371,6 +372,9 @@
}
})
+// 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 => {
diff --git a/src/resources/js/locale.js b/src/resources/js/locale.js
new file mode 100644
--- /dev/null
+++ b/src/resources/js/locale.js
@@ -0,0 +1,60 @@
+import Vue from 'vue'
+import VueI18n from 'vue-i18n'
+
+// We do not pre-load English language, but it will be loaded automatically
+// on page load. This means that localization files must be complete,
+// because there will be no fallback localization loaded.
+// import messages from '../lang-js/en.json'
+
+Vue.use(VueI18n)
+
+export const i18n = new VueI18n({
+ locale: 'en',
+ fallbackLocale: 'en',
+ // messages: { en: messages },
+ silentFallbackWarn: true
+})
+
+let currentLanguage
+
+const loadedLanguages = [ /* 'en' */ ] // our default language that is preloaded
+
+const setI18nLanguage = (lang) => {
+ i18n.locale = lang
+ window.axios.defaults.headers.common['Accept-Language'] = lang
+ document.querySelector('html').setAttribute('lang', lang)
+
+ return lang
+}
+
+export const getLang = () => {
+ // TODO: On init get the current user language from the browser?
+ if (!currentLanguage) {
+ currentLanguage = document.querySelector('html').getAttribute('lang') || 'en'
+ }
+
+ return currentLanguage
+}
+
+export const setLang = lang => {
+ // TODO: Save the selected language in the browser?
+ currentLanguage = lang
+ loadLangAsync()
+}
+
+export function loadLangAsync() {
+ const lang = getLang()
+
+ // If the language was already loaded
+ if (loadedLanguages.includes(lang)) {
+ return Promise.resolve(setI18nLanguage(lang))
+ }
+
+ // If the language hasn't been loaded yet
+ return import(/* webpackChunkName: "locale/[request]" */ `../lang/${lang}/ui.json`)
+ .then(messages => {
+ i18n.setLocaleMessage(lang, messages.default)
+ loadedLanguages.push(lang)
+ return setI18nLanguage(lang)
+ })
+}
diff --git a/src/resources/lang/de/ui.json b/src/resources/lang/de/ui.json
new file mode 100644
--- /dev/null
+++ b/src/resources/lang/de/ui.json
@@ -0,0 +1,22 @@
+{
+ "buttons": {
+ "cancel": "Stornieren",
+ "save": "Speichern"
+ },
+ "menu": {
+ "cockpit": "Cockpit",
+ "login": "Einloggen",
+ "logout": "Ausloggen",
+ "signup": "Signup",
+ "toggle": "Navigation umschalten"
+ },
+ "lang": {
+ "en": "Englisch",
+ "de": "Deutsch"
+ },
+ "login": {
+ "forgot_password": "Passwort vergessen?",
+ "sign_in": "Anmelden",
+ "webmail": "Webmail"
+ }
+}
diff --git a/src/resources/lang/en/ui.json b/src/resources/lang/en/ui.json
new file mode 100644
--- /dev/null
+++ b/src/resources/lang/en/ui.json
@@ -0,0 +1,27 @@
+{
+ "buttons": {
+ "cancel": "Cancel",
+ "save": "Save"
+ },
+ "menu": {
+ "cockpit": "Cockpit",
+ "login": "Login",
+ "logout": "Logout",
+ "signup": "Signup",
+ "toggle": "Toggle navigation"
+ },
+ "lang": {
+ "en": "English",
+ "de": "German"
+ },
+ "login": {
+ "2fa": "Second factor code",
+ "2fa_desc": "Second factor code is optional for users with no 2-Factor Authentication setup.",
+ "email": "Email address",
+ "forgot_password": "Forgot password?",
+ "header": "Please sign in",
+ "password": "Password",
+ "sign_in": "Sign in",
+ "webmail": "Webmail"
+ }
+}
diff --git a/src/resources/themes/default/lang/de/support.php b/src/resources/themes/default/lang/de/support.php
new file mode 100644
--- /dev/null
+++ b/src/resources/themes/default/lang/de/support.php
@@ -0,0 +1,7 @@
+<?php
+
+return [
+
+ 'btn' => "Kontaktieren Sie Support",
+
+];
\ No newline at end of file
diff --git a/src/resources/themes/default/lang/en/support.php b/src/resources/themes/default/lang/en/support.php
new file mode 100644
--- /dev/null
+++ b/src/resources/themes/default/lang/en/support.php
@@ -0,0 +1,7 @@
+<?php
+
+return [
+
+ 'btn' => "Contact Support",
+
+];
\ No newline at end of file
diff --git a/src/resources/themes/default/pages/support.blade.php b/src/resources/themes/default/pages/support.blade.php
--- a/src/resources/themes/default/pages/support.blade.php
+++ b/src/resources/themes/default/pages/support.blade.php
@@ -14,7 +14,7 @@
</p>
</div>
<div class="card-footer text-center">
- <a href="/support/contact" class="btn btn-info">Contact Support</a>
+ <a href="/support/contact" class="btn btn-info">@lang('theme::support.btn')</a>
</div>
</div>
</div>
diff --git a/src/resources/themes/menu.scss b/src/resources/themes/menu.scss
--- a/src/resources/themes/menu.scss
+++ b/src/resources/themes/menu.scss
@@ -56,6 +56,28 @@
}
}
+#language-selector {
+ margin: 2em 0;
+ height: 2em;
+ border-right: 1px solid #bbb;
+
+ .nav-link {
+ padding-left: 0;
+ padding-right: 25px;
+ line-height: 30px;
+ font-weight: lighter;
+
+ &:hover {
+ color: unset;
+ text-decoration: none;
+ }
+ }
+
+ .dropdown-item {
+ line-height: initial;
+ }
+}
+
@include media-breakpoint-up(lg) {
#header-menu {
a.menulogin {
@@ -114,6 +136,12 @@
flex-wrap: nowrap;
}
}
+
+ #language-selector {
+ border-right: 0;
+ margin: 0;
+ height: unset;
+ }
}
@include media-breakpoint-down(sm) {
diff --git a/src/resources/vue/Login.vue b/src/resources/vue/Login.vue
--- a/src/resources/vue/Login.vue
+++ b/src/resources/vue/Login.vue
@@ -2,40 +2,40 @@
<div class="container d-flex flex-column align-items-center justify-content-center">
<div id="logon-form" class="card col-sm-8 col-lg-6">
<div class="card-body">
- <h1 class="card-title text-center mb-3">Please sign in</h1>
+ <h1 class="card-title text-center mb-3">{{ $t('login.header') }}</h1>
<div class="card-text">
<form class="form-signin" @submit.prevent="submitLogin">
<div class="form-group">
- <label for="inputEmail" class="sr-only">Email address</label>
+ <label for="inputEmail" class="sr-only">{{ $t('login.email') }}</label>
<div class="input-group">
<span class="input-group-prepend">
<span class="input-group-text"><svg-icon icon="user"></svg-icon></span>
</span>
- <input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus v-model="email">
+ <input type="email" id="inputEmail" class="form-control" :placeholder="$t('login.email')" required autofocus v-model="email">
</div>
</div>
<div class="form-group">
- <label for="inputPassword" class="sr-only">Password</label>
+ <label for="inputPassword" class="sr-only">{{ $t('login.password') }}</label>
<div class="input-group">
<span class="input-group-prepend">
<span class="input-group-text"><svg-icon icon="lock"></svg-icon></span>
</span>
- <input type="password" id="inputPassword" class="form-control" placeholder="Password" required v-model="password">
+ <input type="password" id="inputPassword" class="form-control" :placeholder="$t('login.password')" required v-model="password">
</div>
</div>
<div class="form-group pt-3" v-if="!$root.isAdmin">
- <label for="secondfactor" class="sr-only">2FA</label>
+ <label for="secondfactor" class="sr-only">{{ $t('login.2fa') }}</label>
<div class="input-group">
<span class="input-group-prepend">
<span class="input-group-text"><svg-icon icon="key"></svg-icon></span>
</span>
- <input type="text" id="secondfactor" class="form-control rounded-right" placeholder="Second factor code" v-model="secondFactor">
+ <input type="text" id="secondfactor" class="form-control rounded-right" :placeholder="$t('login.2fa')" v-model="secondFactor">
</div>
- <small class="form-text text-muted">Second factor code is optional for users with no 2-Factor Authentication setup.</small>
+ <small class="form-text text-muted">{{ $t('login.2fa_desc') }}</small>
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">
- <svg-icon icon="sign-in-alt"></svg-icon> Sign in
+ <svg-icon icon="sign-in-alt"></svg-icon> {{ $t('login.sign_in') }}
</button>
</div>
</form>
@@ -43,8 +43,8 @@
</div>
</div>
<div id="logon-form-footer" class="mt-1">
- <router-link v-if="!$root.isAdmin && $root.hasRoute('password-reset')" :to="{ name: 'password-reset' }" id="forgot-password">Forgot password?</router-link>
- <a v-if="webmailURL && !$root.isAdmin" :href="webmailURL" id="webmail">Webmail</a>
+ <router-link v-if="!$root.isAdmin && $root.hasRoute('password-reset')" :to="{ name: 'password-reset' }" id="forgot-password">{{ $t('login.forgot_password') }}</router-link>
+ <a v-if="webmailURL && !$root.isAdmin" :href="webmailURL" id="webmail">{{ $t('login.webmail') }}</a>
</div>
</div>
</template>
diff --git a/src/resources/vue/Widgets/Menu.vue b/src/resources/vue/Widgets/Menu.vue
--- a/src/resources/vue/Widgets/Menu.vue
+++ b/src/resources/vue/Widgets/Menu.vue
@@ -4,12 +4,18 @@
<router-link class="navbar-brand" to="/" v-html="$root.logo(mode)"></router-link>
<button v-if="mode == 'header'" class="navbar-toggler" type="button"
data-toggle="collapse" :data-target="'#' + mode + '-menu-navbar'"
- aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"
+ aria-controls="navbar" aria-expanded="false" :aria-label="$t('menu.toggle')"
>
<span class="navbar-toggler-icon"></span>
</button>
<div :id="mode + '-menu-navbar'" :class="'navbar' + (mode == 'header' ? ' collapse navbar-collapse' : '')">
<ul class="navbar-nav">
+ <li v-if="languages.length > 1 && mode == 'header'" id="language-selector" class="nav-item dropdown">
+ <a href="#" class="nav-link" role="button" data-toggle="dropdown">{{ $t('lang.' + getLang()) }}</a>
+ <div class="dropdown-menu">
+ <a class="dropdown-item" href="#" v-for="lang in languages" @click="setLang(lang)">{{ $t('lang.' + lang) }}</a>
+ </div>
+ </li>
<li class="nav-item" v-for="item in menu()" :key="item.index">
<a v-if="item.href" :class="'nav-link link-' + item.index" :href="item.href">{{ item.title }}</a>
<router-link v-if="item.to"
@@ -22,16 +28,16 @@
</router-link>
</li>
<li class="nav-item" v-if="!loggedIn && !$root.isAdmin">
- <router-link class="nav-link link-signup" active-class="active" :to="{name: 'signup'}">Signup</router-link>
+ <router-link class="nav-link link-signup" active-class="active" :to="{name: 'signup'}">{{ $t('menu.signup') }}</router-link>
</li>
<li class="nav-item" v-if="loggedIn">
- <router-link class="nav-link link-dashboard" active-class="active" :to="{name: 'dashboard'}">Cockpit</router-link>
+ <router-link class="nav-link link-dashboard" active-class="active" :to="{name: 'dashboard'}">{{ $t('menu.cockpit') }}</router-link>
</li>
<li class="nav-item" v-if="loggedIn">
- <router-link class="nav-link menulogin link-logout" active-class="active" :to="{name: 'logout'}">Logout</router-link>
+ <router-link class="nav-link menulogin link-logout" active-class="active" :to="{name: 'logout'}">{{ $t('menu.logout') }}</router-link>
</li>
<li class="nav-item" v-if="!loggedIn">
- <router-link class="nav-link menulogin link-login" :to="{name: 'login'}">Login</router-link>
+ <router-link class="nav-link menulogin link-login" :to="{name: 'login'}">{{ $t('menu.login') }}</router-link>
</li>
</ul>
<div v-if="mode == 'footer'" class="footer">
@@ -44,7 +50,14 @@
</template>
<script>
+ import { setLang, getLang } from '../../js/locale'
+
export default {
+ data() {
+ return {
+ languages: window.config['languages'] || []
+ }
+ },
props: {
mode: { type: String, default: 'header' },
footer: { type: String, default: '' }
@@ -93,6 +106,12 @@
})
return menu
+ },
+ getLang() {
+ return getLang()
+ },
+ setLang(language) {
+ setLang(language)
}
}
}

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 10, 12:02 AM (15 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18853645
Default Alt Text
D2395.1775779324.diff (22 KB)

Event Timeline