From 3f340d57fc8e3b9b45fdb6a1ccb634162963b905 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Mon, 28 Nov 2022 12:32:48 -0700 Subject: [PATCH] adding a bunch of wip: i18n stuff --- .../Commands/TranslationCheckerCommand.php | 83 ++++++++++++++++++ .../Http/Middleware/HandleInertiaRequests.php | 44 +++++++++- src/app/Http/Middleware/SetLocale.php | 29 +++++++ src/app/Models/User.php | 14 +++ src/database/factories/UserFactory.php | 2 + src/database/seeders/LanguageSeeder.php | 45 ++-------- src/lang/en.json | 87 ++++++++++++++++++- src/package.json | 18 ++-- src/resources/js/app.js | 2 + src/resources/js/base.js | 30 +++++++ 10 files changed, 301 insertions(+), 53 deletions(-) create mode 100644 src/app/Console/Commands/TranslationCheckerCommand.php create mode 100644 src/app/Http/Middleware/SetLocale.php create mode 100644 src/resources/js/base.js diff --git a/src/app/Console/Commands/TranslationCheckerCommand.php b/src/app/Console/Commands/TranslationCheckerCommand.php new file mode 100644 index 0000000..8ff04a2 --- /dev/null +++ b/src/app/Console/Commands/TranslationCheckerCommand.php @@ -0,0 +1,83 @@ +option('locale'))) { + $masterLocale = trim($this->option('locale')); + } + + $langDir = base_path('lang'); + $langFiles = scandir($langDir); + $bigCount = count($langFiles); + for ($i = 0; $i < $bigCount; $i++) { + if (! preg_match('/.*\.json$/', $langFiles[$i]) || "{$masterLocale}.json" === $langFiles[$i]) { + unset($langFiles[$i]); + } + } + + $langFilesCount = count($langFiles); + $this->info("Checking {$langFilesCount} file(s) against {$masterLocale}..."); + + $masterLocaleTranslations = translations(base_path("lang/{$masterLocale}.json")); + + foreach ($langFiles as $localeFile) { + $otherLocaleTranslations = translations(base_path("lang/{$localeFile}")); + $counts = 0; + $mergedTranslations = $this->recursiveMergeArray($masterLocaleTranslations, $otherLocaleTranslations, $counts); + file_put_contents(base_path("lang/{$localeFile}"), json_encode($mergedTranslations, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); + $this->info("{$localeFile} had {$counts} missing translations."); + } + + return Command::SUCCESS; + } + + public function recursiveMergeArray(array $firstArray, array $secondArray, int &$counts) + { + $mergedArray = $secondArray; + foreach ($firstArray as $key => $value) { + if (is_array($value)) { + if (!array_key_exists($key, $secondArray)) { + $mergedArray[$key] = $value; + } else { + $subDiff = $this->recursiveMergeArray($value, $secondArray[$key], $counts); + if (!empty($subDiff)) { + $mergedArray[$key] = $subDiff; + } + } + } else { + if (!array_key_exists($key, $secondArray)) { + $counts++; + $mergedArray[$key] = $value; + } + } + } + + return $mergedArray; + } +} diff --git a/src/app/Http/Middleware/HandleInertiaRequests.php b/src/app/Http/Middleware/HandleInertiaRequests.php index 8f12170..3f3d740 100644 --- a/src/app/Http/Middleware/HandleInertiaRequests.php +++ b/src/app/Http/Middleware/HandleInertiaRequests.php @@ -2,6 +2,7 @@ namespace App\Http\Middleware; +use App\Models\Language; use Illuminate\Http\Request; use Inertia\Middleware; @@ -41,19 +42,56 @@ class HandleInertiaRequests extends Middleware */ public function share(Request $request): array { + $localeFields = ['locale', 'iso_code', 'name', 'localized_name']; + $currentLocale = $request->session()->get('locale', null); + if (is_null($currentLocale)) { + $currentLocale = Language::where(['locale' => 'en', 'iso_code' => 'en_US'])->get($localeFields)[0]->toArray(); + $request->session()->put('locale', [ + 'locale' => $currentLocale['locale'], + 'iso_code' => $currentLocale['iso_code'], + 'name' => $currentLocale['name'], + 'localized_name' => $currentLocale['localized_name'], + ]); + } + $localeFilePath = base_path("lang/{$currentLocale['locale']}.json"); + $notifications = []; + $notificationsCount = count($notifications); $unreadNotifications = false; if (! is_null($request->user())) { $notifications = $request->user()->notifications; + $notificationsCount = count($notifications); + for ($i = 0; $i < $notificationsCount; $i++) { + $newData = $notifications[$i]->data; + $createdAt = carbon($notifications[$i]->created_at); + $dateFormat = 'F j'; + if (!$createdAt->is(gmdate('Y'))) { + $dateFormat = 'F j, Y'; + } + $newData['created_at_date'] = $createdAt->copy()->format($dateFormat); + $newData['created_at_time'] = $createdAt->copy()->format('H:i'); + $notifications[$i]->data = $newData; + } if (count($request->user()->unreadNotifications) > 0) { $unreadNotifications = true; } } - return array_merge(parent::share($request), [ - 'appName' => config('app.name'), + $additionalData = [ + 'appName' => config('app.name'), + + 'availableLocales' => Language::get($localeFields), + 'currentLocale' => $currentLocale, + 'language' => translations($localeFilePath), + 'notifications' => $notifications, 'unreadNotifications' => $unreadNotifications, - ]); + ]; + + if ($request->user()->email === env('ADMIN_EMAIL')) { + $additionalData['is_admin_user'] = true; + } + + return array_merge(parent::share($request), $additionalData); } } diff --git a/src/app/Http/Middleware/SetLocale.php b/src/app/Http/Middleware/SetLocale.php new file mode 100644 index 0000000..33e8e61 --- /dev/null +++ b/src/app/Http/Middleware/SetLocale.php @@ -0,0 +1,29 @@ +has('locale')) { + app()->setLocale(session('locale')['locale']); + } + + return $next($request); + } +} diff --git a/src/app/Models/User.php b/src/app/Models/User.php index 93f9908..718d9b8 100644 --- a/src/app/Models/User.php +++ b/src/app/Models/User.php @@ -37,6 +37,7 @@ class User extends Authenticatable 'name', 'surname', 'timezone_name', + 'language_id', 'current_team_id', 'profile_photo_path', 'email', @@ -183,4 +184,17 @@ class User extends Authenticatable { return $this->morphOne(Address::class, 'addressable'); } + + /** + * Language relationship. + * + * @package App\Models\User + * @since 1.0.0 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function language(): BelongsTo + { + return $this->belongsTo(Language::class); + } } diff --git a/src/database/factories/UserFactory.php b/src/database/factories/UserFactory.php index e0a9d34..1bad4ca 100644 --- a/src/database/factories/UserFactory.php +++ b/src/database/factories/UserFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\Models\Language; use App\Models\Team; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; @@ -31,6 +32,7 @@ class UserFactory extends Factory 'email' => $this->faker->unique()->safeEmail(), 'email_verified_at' => now(), 'timezone_name' => $this->faker->timezone(), + 'timezone_name' => Language::all()->random()->id, 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), ]; diff --git a/src/database/seeders/LanguageSeeder.php b/src/database/seeders/LanguageSeeder.php index af8063e..8c1151d 100644 --- a/src/database/seeders/LanguageSeeder.php +++ b/src/database/seeders/LanguageSeeder.php @@ -16,49 +16,18 @@ class LanguageSeeder extends Seeder */ public function run() { - $languages = [ - [ - 'iso_code' => 'en_US', - 'locale' => 'en', - 'title' => 'English', - 'title_localized' => 'English', - ], - [ - 'iso_code' => 'de_DE', - 'locale' => 'de', - 'title' => 'German', - 'title_localized' => 'Deutsch', - ], - [ - 'iso_code' => 'fr_FR', - 'locale' => 'fr', - 'title' => 'French', - 'title_localized' => 'Français', - ], - [ - 'iso_code' => 'es_SP', - 'locale' => 'es', - 'title' => 'Spanish', - 'title_localized' => 'Español', - ], - [ - 'iso_code' => 'jp_JP', - 'locale' => 'jp', - 'title' => 'Japanese', - 'title_localized' => '日本', - ], - [ - 'iso_code' => 'zh_TW', - 'locale' => 'zh', - 'title' => 'Taiwanese', - 'title_localized' => '台湾', - ], + $languagesData = [ + ['locale' => 'en', 'iso_code' => 'en_US', 'name' => 'English', 'localized_name' => 'English (US)'], + ['locale' => 'de', 'iso_code' => 'de_DE', 'name' => 'German', 'localized_name' => 'Deutsch'], + ['locale' => 'fr', 'iso_code' => 'fr_FR', 'name' => 'French', 'localized_name' => 'Français'], + ['locale' => 'jp', 'iso_code' => 'jp_JP', 'name' => 'Japanese', 'localized_name' => '日本'], + ['locale' => 'mx', 'iso_code' => 'es_MX', 'name' => 'Spanish', 'localized_name' => 'Español'], ]; $startTime = Carbon::now(); $offset = 0; - foreach ($languages as $language) { + foreach ($languagesData as $language) { $datetime = $startTime->copy()->addMinute($offset)->toDateTimeString(); $offset++; $language['created_at'] = $datetime; diff --git a/src/lang/en.json b/src/lang/en.json index 176b58b..a86dbd6 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1,3 +1,88 @@ { - "key": "translation that has :attribute in the string." + "titles": { + "welcome": "Welcome", + "dashboard": "Dashboard", + "secure_area": "Secure Area", + "forgot_password": "Forgot Password", + "my_profile": "My Profile", + "edit_profile": "Edit Profile", + "log_in": "Log In To Your Account", + "register": "Register A New Account", + "reset_password": "Reset Password", + "two_factor_confirmation": "Two-factor Confirmation", + "verify_email": "Email Verification" + }, + "labels": { + "type": "Type", + "none": "None", + "title": "Title", + "name": "Name", + "created_at": "Created At", + "updated_at": "Updated At", + "actions": "Actions", + "email": "Email", + "timezone_name": "Timezone", + "language": "Language", + "password": "Password", + "confirm_password": "Confirm Password", + "current_password": "Current Password", + "remember_me": "Remember me", + "first_name": "First Name", + "last_name": "Surname", + "forgot_password": "Forgot your password?", + "already_registered": "Already registered?", + "code": "Code", + "recovery_code": "Recovery Code", + "use_recovery_code": "Use a recovery code", + "use_authentication_code": "Use an authentication code", + "email_verification": "Email Verification", + "user": "User", + "optional": "optional" + }, + "actions": { + "save": "Save", + "update": "Update", + "edit": "Edit", + "delete": "Delete", + "confirm": "Confirm", + "log_in": "Log in", + "log_out": "Log Out", + "register": "Register", + "edit_profile": "Edit Profile", + "confirm_password": "Confirm Password", + "reset_password": "Reset Password", + "email_password_reset_link": "Email Password Reset Link", + "resend_verification_email": "Resend Verification Email" + }, + "nav": { + "manage_team": "Manage Team", + "team_settings": "Team Settings", + "create_new_team": "Create New Team", + "switch_teams": "Switch Teams", + "manage_account": "Manage Account", + "profile": "My Profile", + "admin_settings": "Admin Settings", + "api_tokens": "API Tokens" + }, + "pagination": { + "show_from_to_count": "Showing :from to :to of :count results" + }, + "notifications": { + "saved": "Saved.", + "verified": "Verified!", + "unverified": "Unverified.", + "new_email_verification_sent": "A new verification link has been sent to the email address you provided in your profile settings." + }, + "pages": { + "profile": { + "titles": { + "update_password": "Update Your Password" + }, + "text_blocks": { + "unverified_message": "Please verify your account.", + "verified_message": "Thanks for being amazing.", + "secure_password": "Ensure your account is using a long, random password to stay secure." + } + } + } } diff --git a/src/package.json b/src/package.json index 8789879..479162a 100644 --- a/src/package.json +++ b/src/package.json @@ -1,28 +1,24 @@ { "private": true, "scripts": { - "dev": "npm run development", - "development": "mix", - "watch": "mix watch", - "watch-poll": "mix watch -- --watch-options-poll=1000", - "hot": "mix watch --hot", - "prod": "npm run production", - "production": "mix --production" + "dev": "vite", + "build": "vite build" }, "devDependencies": { - "laravel-mix": "^6.0.49", + "@vitejs/plugin-vue": "^3.0.0", + "laravel-vite-plugin": "^0.6.0", "postcss": "^8.4.14", - "postcss-import": "^14.1.0", - "vue-loader": "^17.0.0" + "vite": "^3.0.0" }, "dependencies": { "@inertiajs/inertia": "^0.11.0", "@inertiajs/inertia-vue3": "^0.6.0", "@inertiajs/progress": "^0.2.7", "@tailwindcss/aspect-ratio": "^0.4.0", - "@tailwindcss/forms": "^0.5.2", + "@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/typography": "^0.5.2", "@vueuse/core": "^8.7.5", + "apexcharts": "^3.35.5", "axios": "^0.27.2", "daisyui": "^2.29.0", "dayjs": "^1.11.3", diff --git a/src/resources/js/app.js b/src/resources/js/app.js index fe8103b..b9becca 100644 --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -5,6 +5,7 @@ import { createInertiaApp } from '@inertiajs/inertia-vue3'; import { InertiaProgress } from '@inertiajs/progress'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m'; +import translationHelper from './base.js'; import Notifications from 'notiwind'; import AppLayout from './Layouts/AppLayout.vue'; import AuthLayout from './Layouts/AuthLayout.vue'; @@ -43,6 +44,7 @@ createInertiaApp({ .use(plugin) .use(Notifications) .use(ZiggyVue, Ziggy) + .mixin(translationHelper) .mixin({ methods: { route } }) .mount(el); }, diff --git a/src/resources/js/base.js b/src/resources/js/base.js new file mode 100644 index 0000000..993d26b --- /dev/null +++ b/src/resources/js/base.js @@ -0,0 +1,30 @@ +export default { + methods: { + __(key, replace = {}) { + let translation = '' + + // check for dot notation + if (key.includes('.')) { + translation = this.$page.props.language + key.split('.').forEach(subKey => { + translation = translation[subKey] + }, key) + } else { + // check if it exists on the translated strings we got + if (this.$page.props.language.hasOwnProperty(key)) { + translation = this.$page.props.language[key] + } else { + // otherwise just take it raw + translation = key + } + } + + // used to fill in variables for translation string + Object.keys(replace).forEach(key => { + translation = translation.replace(':' + key, replace[key]) + }); + + return translation; + }, + }, +};