Add support of the Accept_language header for UI localization

This commit is contained in:
Bubka 2021-12-03 22:50:28 +01:00
parent fbe91cc90e
commit 9f574feada
8 changed files with 92 additions and 12 deletions

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Services\SettingService; use App\Services\SettingService;
use Illuminate\Support\Facades\App;
class SinglePageController extends Controller class SinglePageController extends Controller
{ {
@ -31,7 +32,8 @@ public function index()
{ {
return view('landing')->with([ return view('landing')->with([
'appSettings' => $this->settingService->all()->toJson(), 'appSettings' => $this->settingService->all()->toJson(),
'lang' => $this->settingService->get('lang') 'lang' => App::currentLocale(),
'locales' => collect(config("2fauth.locales"))->toJson(),
]); ]);
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Closure; use Closure;
use Illuminate\Support\Facades\App;
use Facades\App\Services\SettingService; use Facades\App\Services\SettingService;
class SetLanguage class SetLanguage
@ -16,7 +17,25 @@ class SetLanguage
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
\App::setLocale(SettingService::get('lang', 'en')); // 3 possible cases here:
// - The user has choosen a specific language among those available in the Setting view of 2FAuth
// - The client send an accept-language header
// - No language is passed from the client
//
// We prioritize the user defined one, then the request header one, and finally the fallback one.
// FI: SettingService::get() always returns a fallback value
$lang = SettingService::get('lang');
if($lang === 'browser') {
if ($request->hasHeader("Accept-Language")) {
// We only keep the primary language passed via the header.
$lang = head(explode(',', $request->header("Accept-Language")));
}
else $lang = config('app.fallback_locale');
}
// If the language is not available (or partial), strings will be translated using the fallback language.
App::setLocale($lang);
return $next($request); return $next($request);
} }

View File

@ -5,14 +5,29 @@
use Throwable; use Throwable;
use Exception; use Exception;
use App\Models\Option; use App\Models\Option;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\App;
use App\Exceptions\DbEncryptionException; use App\Exceptions\DbEncryptionException;
class SettingService class SettingService
{ {
/**
* Determine if the given setting has been customized by the user
*
* @param string $key
* @return bool
*/
public function isUserDefined($key) : bool
{
return DB::table('options')->where('key', $key)->exists();
}
/** /**
* Get a setting * Get a setting
* *
@ -40,9 +55,15 @@ public function all() : Collection
$userOptions->transform(function ($item, $key) { $userOptions->transform(function ($item, $key) {
return $this->restoreType($item); return $this->restoreType($item);
}); });
$userOptions = collect(config('2fauth.options'))->merge($userOptions);
return $userOptions; // Merge 2fauth/app config values as fallback values
$settings = collect(config('2fauth.options'))->merge($userOptions);
if(!Arr::has($settings, 'lang')) {
$settings['lang'] = 'browser';
}
return $settings;
} }

View File

@ -13,6 +13,19 @@
'isDemoApp' => env('IS_DEMO_APP', false), 'isDemoApp' => env('IS_DEMO_APP', false),
], ],
/*
|--------------------------------------------------------------------------
| 2FAuth available translations
|--------------------------------------------------------------------------
|
*/
'locales' => [
'en',
'fr',
'de'
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Application fallback for user options | Application fallback for user options

View File

@ -8,6 +8,7 @@
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4> <h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
<!-- Language --> <!-- Language -->
<form-select v-on:lang="saveSetting('lang', $event)" :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" /> <form-select v-on:lang="saveSetting('lang', $event)" :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
<div class="field help">{{ $t('settings.forms.some_translation_are_missing') }}<a class="ml-2" href="https://crowdin.com/project/2fauth">{{ $t('settings.forms.help_translate_2fauth') }}</a></div>
<!-- display mode --> <!-- display mode -->
<form-toggle v-on:displayMode="saveSetting('displayMode', $event)" :choices="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" /> <form-toggle v-on:displayMode="saveSetting('displayMode', $event)" :choices="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
<!-- show icon --> <!-- show icon -->
@ -74,7 +75,7 @@
data(){ data(){
return { return {
form: new Form({ form: new Form({
lang: '', lang: 'browser',
showOtpAsDot: null, showOtpAsDot: null,
closeOtpOnCopy: null, closeOtpOnCopy: null,
useBasicQrcodeReader: null, useBasicQrcodeReader: null,
@ -87,11 +88,6 @@
defaultCaptureMode: '', defaultCaptureMode: '',
rememberActiveGroup: true, rememberActiveGroup: true,
}), }),
langs: [
{ text: this.$t('languages.en'), value: 'en' },
{ text: this.$t('languages.fr'), value: 'fr' },
{ text: this.$t('languages.de'), value: 'de' },
],
layouts: [ layouts: [
{ text: this.$t('settings.forms.grid'), value: 'grid', icon: 'th' }, { text: this.$t('settings.forms.grid'), value: 'grid', icon: 'th' },
{ text: this.$t('settings.forms.list'), value: 'list', icon: 'list' }, { text: this.$t('settings.forms.list'), value: 'list', icon: 'list' },
@ -119,11 +115,36 @@
} }
}, },
computed : {
langs: function() {
let locales = [{
text: this.$t('languages.browser_preference') + ' (' + this.$root.$i18n.locale + ')',
value: 'browser'
}];
for (const locale of window.appLocales) {
locales.push({
text: this.$t('languages.' + locale),
value: locale
})
}
return locales
}
},
async mounted() { async mounted() {
const { data } = await this.form.get('/api/v1/settings') const { data } = await this.form.get('/api/v1/settings')
this.form.fillWithKeyValueObject(data) this.form.fillWithKeyValueObject(data)
this.form.lang = this.$root.$i18n.locale let lang = data.filter(x => x.key === 'lang')
if (lang.value == 'browser') {
if(window.appLocales.includes(lang.value)) {
this.form.lang = lang
}
}
// this.$root.$i18n.locale
this.form.setOriginal() this.form.setOriginal()
this.fetchGroups() this.fetchGroups()
}, },

View File

@ -10,6 +10,7 @@
| |
*/ */
'browser_preference' => 'Browser preference',
'en' => 'English', 'en' => 'English',
'fr' => 'French', 'fr' => 'French',
'de' => 'German', 'de' => 'German',

View File

@ -39,9 +39,11 @@
'edit_settings' => 'Edit settings', 'edit_settings' => 'Edit settings',
'setting_saved' => 'Settings saved', 'setting_saved' => 'Settings saved',
'new_token' => 'New token', 'new_token' => 'New token',
'some_translation_are_missing' => 'Some translations are missing using the browser preferred language?',
'help_translate_2fauth' => 'Help translate 2FAuth',
'language' => [ 'language' => [
'label' => 'Language', 'label' => 'Language',
'help' => 'Change the language used to translate the app interface.' 'help' => 'Language used to translate the 2FAuth user interface. Named languages are complete, set the one of your choice to override your browser preference.'
], ],
'show_otp_as_dot' => [ 'show_otp_as_dot' => [
'label' => 'Show generated one-time passwords as dot', 'label' => 'Show generated one-time passwords as dot',

View File

@ -25,6 +25,7 @@
<script type="text/javascript"> <script type="text/javascript">
var appSettings = {!! $appSettings !!}; var appSettings = {!! $appSettings !!};
var appVersion = '{{ config("app.version") }}'; var appVersion = '{{ config("app.version") }}';
var appLocales = {!! $locales !!};
</script> </script>
<script src="{{ mix('js/manifest.js') }}"></script> <script src="{{ mix('js/manifest.js') }}"></script>
<script src="{{ mix('js/vendor.js') }}"></script> <script src="{{ mix('js/vendor.js') }}"></script>