瀏覽代碼

Add support of the Accept_language header for UI localization

Bubka 3 年之前
父節點
當前提交
9f574feada

+ 3 - 1
app/Http/Controllers/SinglePageController.php

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

+ 20 - 1
app/Http/Middleware/SetLanguage.php

@@ -3,6 +3,7 @@
 namespace App\Http\Middleware;
 
 use Closure;
+use Illuminate\Support\Facades\App;
 use Facades\App\Services\SettingService;
 
 class SetLanguage
@@ -16,7 +17,25 @@ class SetLanguage
      */
     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);
     }

+ 23 - 2
app/Services/SettingService.php

@@ -5,14 +5,29 @@ namespace App\Services;
 use Throwable;
 use Exception;
 use App\Models\Option;
+use Illuminate\Support\Arr;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Crypt;
+use Illuminate\Support\Facades\App;
 use App\Exceptions\DbEncryptionException;
 
 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
      *
@@ -40,9 +55,15 @@ class SettingService
         $userOptions->transform(function ($item, $key) {
             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;
     }
 
 

+ 13 - 0
config/2fauth.php

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

+ 28 - 7
resources/js/views/settings/Options.vue

@@ -8,6 +8,7 @@
                     <h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
                     <!-- 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')" />
+                    <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 -->
                     <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 -->
@@ -74,7 +75,7 @@
         data(){
             return {
                 form: new Form({
-                    lang: '',
+                    lang: 'browser',
                     showOtpAsDot: null,
                     closeOtpOnCopy: null,
                     useBasicQrcodeReader: null,
@@ -87,11 +88,6 @@
                     defaultCaptureMode: '',
                     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: [
                     { text: this.$t('settings.forms.grid'), value: 'grid', icon: 'th' },
                     { 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() {
             const { data } = await this.form.get('/api/v1/settings')
 
             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.fetchGroups()
         },

+ 1 - 0
resources/lang/en/languages.php

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

+ 3 - 1
resources/lang/en/settings.php

@@ -39,9 +39,11 @@ return [
         'edit_settings' => 'Edit settings',
         'setting_saved' => 'Settings saved',
         'new_token' => 'New token',
+        'some_translation_are_missing' => 'Some translations are missing using the browser preferred language?',
+        'help_translate_2fauth' => 'Help translate 2FAuth',
         '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' => [
             'label' => 'Show generated one-time passwords as dot',

+ 1 - 0
resources/views/landing.blade.php

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