Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F120766565
D2395.1776875906.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
31 KB
Referenced Files
None
Subscribers
None
D2395.1776875906.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());
}
@@ -57,6 +60,113 @@
// TODO: Support pages with variables, e.g. users/<user-id>
}
+ // 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'];
+ }
+
+ /**
+ * 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/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
@@ -387,20 +387,8 @@
$env['paymentProvider'] = \config('services.payment_provider');
$env['stripePK'] = \config('services.stripe.public_key');
- $theme_file = resource_path("themes/{$env['app.theme']}/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'];
- }
- }
-
- $env['menu'] = $menu;
+ $env['languages'] = \App\Http\Controllers\ContentController::locales();
+ $env['menu'] = \App\Http\Controllers\ContentController::menu();
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/faq.php b/src/resources/themes/default/lang/de/faq.php
new file mode 100644
--- /dev/null
+++ b/src/resources/themes/default/lang/de/faq.php
@@ -0,0 +1,9 @@
+<?php
+
+return [
+
+ 'account-upgrade' => "Kann ich ein einzelnes Konto auf ein Gruppenkonto aktualisieren?",
+ 'storage' => "Wie viel Speicherplatz ist in meinem Konto enthalten?",
+ 'tos' => "Was sind Ihre Nutzungsbedingungen?",
+
+];
diff --git a/src/resources/themes/default/lang/de/menu.php b/src/resources/themes/default/lang/de/menu.php
new file mode 100644
--- /dev/null
+++ b/src/resources/themes/default/lang/de/menu.php
@@ -0,0 +1,10 @@
+<?php
+
+return [
+
+ 'blog' => "Blog",
+ 'explore' => "Erkunden",
+ 'support' => "Unterstützung",
+ 'tos' => "LdD",
+
+];
\ No newline at end of file
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/faq.php b/src/resources/themes/default/lang/en/faq.php
new file mode 100644
--- /dev/null
+++ b/src/resources/themes/default/lang/en/faq.php
@@ -0,0 +1,9 @@
+<?php
+
+return [
+
+ 'account-upgrade' => "Can I upgrade an individual account to a group account?",
+ 'storage' => "How much storage comes with my account?",
+ 'tos' => "What are your terms of service?",
+
+];
diff --git a/src/resources/themes/default/lang/en/menu.php b/src/resources/themes/default/lang/en/menu.php
new file mode 100644
--- /dev/null
+++ b/src/resources/themes/default/lang/en/menu.php
@@ -0,0 +1,10 @@
+<?php
+
+return [
+
+ 'blog' => "Blog",
+ 'explore' => "Explore",
+ 'support' => "Support",
+ 'tos' => "ToS",
+
+];
\ 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/default/theme.json b/src/resources/themes/default/theme.json
--- a/src/resources/themes/default/theme.json
+++ b/src/resources/themes/default/theme.json
@@ -1,23 +1,23 @@
{
"menu": [
{
- "title": "Explore",
+ "label": "explore",
"location": "https://kolabnow.com/",
"admin": true
},
{
- "title": "Blog",
+ "label": "blog",
"location": "https://blogs.kolabnow.com/",
"admin": true
},
{
- "title": "Support",
+ "label": "support",
"location": "/support",
"page": "support",
"admin": true
},
{
- "title": "ToS",
+ "label": "tos",
"location": "https://kolabnow.com/tos",
"footer": true
}
@@ -26,15 +26,15 @@
"signup": [
{
"href": "https://kolabnow.com/tos",
- "title": "What are your terms of service?"
+ "label": "tos"
},
{
"href": "https://kb.kolabnow.com/faq/can-i-upgrade-an-individual-account-to-a-group-account",
- "title": "Can I upgrade an individual account to a group account?"
+ "label": "account-upgrade"
},
{
"href": "https://kb.kolabnow.com/faq/how-much-storage-comes-with-my-account",
- "title": "How much storage comes with my account?"
+ "label": "storage"
}
]
}
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,13 +4,19 @@
<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 class="nav-item" v-for="item in menu()" :key="item.index">
+ <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"
:class="'nav-link link-' + item.index"
@@ -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,27 +50,41 @@
</template>
<script>
+ import { setLang, getLang } from '../../js/locale'
+
export default {
+ data() {
+ return {
+ languages: window.config['languages'] || [],
+ menuList: []
+ }
+ },
props: {
mode: { type: String, default: 'header' },
footer: { type: String, default: '' }
},
computed: {
loggedIn() { return this.$store.state.isLoggedIn },
+ menu() { return this.menuList.filter(item => !item.footer || this.mode == 'footer') },
route() { return this.$route.name }
},
mounted() {
+ this.menuList = this.loadMenu()
+
// On mobile close the menu when the menu item is clicked
if (this.mode == 'header') {
$('#header-menu .navbar').on('click', function() { $(this).removeClass('show') })
}
},
methods: {
- menu() {
+ loadMenu() {
let menu = []
+ const lang = this.getLang()
const loggedIn = this.loggedIn
window.config.menu.forEach(item => {
+ item.title = item['title-' + lang] || item['title-en'] || item.title
+
if (!item.location || !item.title) {
console.error("Invalid menu entry", item)
return
@@ -72,27 +92,26 @@
// TODO: Different menu for different loggedIn state
- if (window.isAdmin && !item.admin) {
- return
- } else if (!window.isAdmin && item.admin === 'only') {
- return
+ if (item.location.match(/^https?:/)) {
+ item.href = item.location
+ } else {
+ item.to = { path: item.location }
}
- if (!item.footer || this.mode == 'footer') {
- if (item.location.match(/^https?:/)) {
- item.href = item.location
- } else {
- item.to = { path: item.location }
- }
+ item.exact = item.location == '/'
+ item.index = item.page || item.title.toLowerCase().replace(/\s+/g, '')
- item.exact = item.location == '/'
- item.index = item.page || item.title.toLowerCase().replace(/\s+/g, '')
-
- menu.push(item)
- }
+ menu.push(item)
})
return menu
+ },
+ getLang() {
+ return getLang()
+ },
+ setLang(language) {
+ setLang(language)
+ this.menuList = this.loadMenu()
}
}
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Apr 22, 4:38 PM (1 w, 5 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18856569
Default Alt Text
D2395.1776875906.diff (31 KB)
Attached To
Mode
D2395: Localization with vue-i18n
Attached
Detach File
Event Timeline