From 78ed332d0661f0704bca3eca6897b2cafa344e17 Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski Date: Tue, 7 Apr 2026 04:41:00 +0200 Subject: [PATCH] Add per-user language preference with company default fallback Existing accounts inherited the company language at creation time and there was no way to change UI language per user. Add a 'Default (Company Language)' entry to the language selector in UserGeneralView, persist the choice through userStore.updateUserSettings and reload the i18n bundle via window.loadLanguage. The 'default' sentinel keeps the user opted in to the company-wide setting. Bootstrap (global.store) now syncs userForm from current_user data and resolves the active UI language as user > company > 'en'. RegisterController, InvitationRegistrationController and MemberService seed new users with language=default instead of copying the current company setting, so promoting/inviting members no longer leaks the inviter's frozen language. --- .../Auth/InvitationRegistrationController.php | 2 ++ .../Company/Auth/RegisterController.php | 6 +++- app/Services/MemberService.php | 3 +- lang/en.json | 3 +- .../settings/views/UserGeneralView.vue | 34 ++++++++++++++++--- resources/scripts-v2/stores/global.store.ts | 21 ++++++++++++ resources/scripts-v2/stores/user.store.ts | 4 +-- 7 files changed, 63 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/Company/Auth/InvitationRegistrationController.php b/app/Http/Controllers/Company/Auth/InvitationRegistrationController.php index 1142243c..f8e9ddc7 100644 --- a/app/Http/Controllers/Company/Auth/InvitationRegistrationController.php +++ b/app/Http/Controllers/Company/Auth/InvitationRegistrationController.php @@ -73,6 +73,8 @@ class InvitationRegistrationController extends Controller 'password' => $request->password, ]); + $user->setSettings(['language' => 'default']); + $this->invitationService->accept($invitation, $user); return response()->json([ diff --git a/app/Http/Controllers/Company/Auth/RegisterController.php b/app/Http/Controllers/Company/Auth/RegisterController.php index b5969155..5a222b63 100644 --- a/app/Http/Controllers/Company/Auth/RegisterController.php +++ b/app/Http/Controllers/Company/Auth/RegisterController.php @@ -61,10 +61,14 @@ class RegisterController extends Controller */ protected function create(array $data) { - return User::create([ + $user = User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => $data['password'], ]); + + $user->setSettings(['language' => 'default']); + + return $user; } } diff --git a/app/Services/MemberService.php b/app/Services/MemberService.php index d4e33a97..22d76af7 100644 --- a/app/Services/MemberService.php +++ b/app/Services/MemberService.php @@ -3,7 +3,6 @@ namespace App\Services; use App\Http\Requests\MemberRequest; -use App\Models\CompanySetting; use App\Models\User; use Silber\Bouncer\BouncerFacade; @@ -14,7 +13,7 @@ class MemberService $user = User::create($request->getUserPayload()); $user->setSettings([ - 'language' => CompanySetting::getSetting('language', $request->header('company')), + 'language' => 'default', ]); $companies = collect($request->companies); diff --git a/lang/en.json b/lang/en.json index 0efb22a9..d57b8754 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1178,7 +1178,8 @@ "profile_picture_description": "Upload or remove your profile photo.", "save": "Save", "section_description": "You can update your name, email & password using the form below.", - "updated_message": "Account Settings updated successfully" + "updated_message": "Account Settings updated successfully", + "default_language": "Default (Company Language)" }, "user_profile": { "name": "Name", diff --git a/resources/scripts-v2/features/company/settings/views/UserGeneralView.vue b/resources/scripts-v2/features/company/settings/views/UserGeneralView.vue index c75dc6d1..8e3f2f29 100644 --- a/resources/scripts-v2/features/company/settings/views/UserGeneralView.vue +++ b/resources/scripts-v2/features/company/settings/views/UserGeneralView.vue @@ -5,14 +5,33 @@ import { required, minLength, email, helpers } from '@vuelidate/validators' import { useVuelidate } from '@vuelidate/core' import { useUserStore } from '../../../../stores/user.store' import { useGlobalStore } from '../../../../stores/global.store' +import { useCompanyStore } from '../../../../stores/company.store' + +const LANGUAGE_DEFAULT = 'default' const userStore = useUserStore() const globalStore = useGlobalStore() +const companyStore = useCompanyStore() const { t } = useI18n() const isSaving = ref(false) const userForm = computed(() => userStore.userForm) +const selectedLanguage = computed({ + get: () => userForm.value.language || LANGUAGE_DEFAULT, + set: (v: string) => { + userForm.value.language = v === LANGUAGE_DEFAULT ? '' : v + }, +}) + +const languageOptions = computed(() => { + const languages = (globalStore.config as Record)?.languages as Array<{ name: string; code: string }> ?? [] + return [ + { name: t('settings.account_settings.default_language'), code: LANGUAGE_DEFAULT }, + ...languages, + ] +}) + const rules = computed(() => ({ name: { required: helpers.withMessage(t('validation.required'), required), @@ -32,11 +51,17 @@ async function updateGeneral(): Promise { isSaving.value = true try { + const language = userForm.value.language || 'default' + + await userStore.updateUserSettings({ settings: { language } }) + await userStore.updateCurrentUser({ name: userForm.value.name, email: userForm.value.email, - language: userForm.value.language || undefined, }) + + const effectiveLanguage = (language === 'default' ? '' : language) || companyStore.selectedCompanySettings?.language || 'en' + await (window as Record).loadLanguage?.(effectiveLanguage) } finally { isSaving.value = false } @@ -77,12 +102,13 @@ async function updateGeneral(): Promise { diff --git a/resources/scripts-v2/stores/global.store.ts b/resources/scripts-v2/stores/global.store.ts index 098ce287..7c1fe0f5 100644 --- a/resources/scripts-v2/stores/global.store.ts +++ b/resources/scripts-v2/stores/global.store.ts @@ -68,6 +68,17 @@ export const useGlobalStore = defineStore('global', () => { userStore.currentUserSettings = response.current_user_settings userStore.currentAbilities = response.current_user_abilities + // Sync user form with bootstrap data + if (response.current_user) { + userStore.userForm = { + name: response.current_user.name ?? '', + email: response.current_user.email ?? '', + password: '', + confirm_password: '', + language: response.current_user_settings?.language ?? '', + } + } + // company store companyStore.companies = response.companies @@ -89,6 +100,16 @@ export const useGlobalStore = defineStore('global', () => { } isAppLoaded.value = true + + // Load UI language: user preference > company setting > English + // 'default' means "use company language" + const userLang = userStore.currentUserSettings.language + const uiLanguage = + (userLang && userLang !== 'default' ? userLang : '') || + (response.current_company_settings as Record)?.language || + 'en' + await (window as Record).loadLanguage?.(uiLanguage) + return response } catch (err: unknown) { handleApiError(err) diff --git a/resources/scripts-v2/stores/user.store.ts b/resources/scripts-v2/stores/user.store.ts index 423612c0..418ce7f9 100644 --- a/resources/scripts-v2/stores/user.store.ts +++ b/resources/scripts-v2/stores/user.store.ts @@ -45,7 +45,7 @@ export const useUserStore = defineStore('user', () => { email: response.data.email, password: '', confirm_password: '', - language: '', + language: currentUserSettings.value.language || '', } return response } catch (err: unknown) { @@ -63,7 +63,7 @@ export const useUserStore = defineStore('user', () => { email: response.data.email, password: '', confirm_password: '', - language: '', + language: currentUserSettings.value.language || '', } const notificationStore = useNotificationStore()