Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F118363230
D2395.1775779324.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
22 KB
Referenced Files
None
Subscribers
None
D2395.1775779324.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D2395: Localization with vue-i18n
Attached
Detach File
Event Timeline