Dynamically load language files (#446)

This commit is contained in:
Darko Gjorgjijoski
2025-08-28 15:19:51 +02:00
committed by GitHub
parent 32f7bc053a
commit a40bf5840d
10 changed files with 154 additions and 59 deletions

View File

@@ -16,7 +16,7 @@ class GetCompanySettingsController extends Controller
*/ */
public function __invoke(GetSettingsRequest $request) public function __invoke(GetSettingsRequest $request)
{ {
$settings = CompanySetting::getSettings($request->settings, $request->header('company')); $settings = CompanySetting::getSettings((array) $request->settings, $request->header('company'));
return response()->json($settings); return response()->json($settings);
} }

View File

@@ -17,6 +17,6 @@ class GetUserSettingsController extends Controller
{ {
$user = $request->user(); $user = $request->user();
return response()->json($user->getSettings($request->settings)); return response()->json($user->getSettings((array) $request->settings));
} }
} }

42
lang/locales.js vendored
View File

@@ -1,43 +1,7 @@
import cs from './cs.json'
import en from './en.json' import en from './en.json'
import fr from './fr.json'
import es from './es.json'
import ar from './ar.json'
import de from './de.json'
import ja from './ja.json'
import pl from './pl.json'
import pt_BR from './pt-br.json'
import it from './it.json'
import sr from './sr.json'
import nl from './nl.json'
import ko from './ko.json'
import lv from './lv.json'
import sv from './sv.json'
import sk from './sk.json'
import vi from './vi.json'
import el from './el.json'
import hr from './hr.json'
import th from './th.json'
// Only load English by default to reduce initial bundle size
// Other languages will be loaded dynamically when needed
export default { export default {
cs, en
en,
fr,
es,
ar,
de,
ja,
pt_BR,
it,
sr,
nl,
ko,
lv,
sv,
sk,
vi,
pl,
el,
hr,
th
} }

View File

@@ -7,6 +7,7 @@ import { defineGlobalComponents } from './global-components'
import utils from '@/scripts/helpers/utilities.js' import utils from '@/scripts/helpers/utilities.js'
import _ from 'lodash' import _ from 'lodash'
import { VTooltip } from 'v-tooltip' import { VTooltip } from 'v-tooltip'
import { setI18nLanguage } from '@/scripts/helpers/language-loader.js'
const app = createApp(App) const app = createApp(App)
@@ -14,6 +15,7 @@ export default class InvoiceShelf {
constructor() { constructor() {
this.bootingCallbacks = [] this.bootingCallbacks = []
this.messages = messages this.messages = messages
this.i18n = null
} }
booting(callback) { booting(callback) {
@@ -30,6 +32,17 @@ export default class InvoiceShelf {
_.merge(this.messages, moduleMessages) _.merge(this.messages, moduleMessages)
} }
/**
* Dynamically load and set a language
* @param {string} locale - Language code to load
* @returns {Promise<void>}
*/
async loadLanguage(locale) {
if (this.i18n) {
await setI18nLanguage(this.i18n, locale)
}
}
start() { start() {
this.executeCallbacks() this.executeCallbacks()
@@ -37,7 +50,7 @@ export default class InvoiceShelf {
app.provide('$utils', utils) app.provide('$utils', utils)
const i18n = createI18n({ this.i18n = createI18n({
legacy: false, legacy: false,
locale: 'en', locale: 'en',
fallbackLocale: 'en', fallbackLocale: 'en',
@@ -45,12 +58,15 @@ export default class InvoiceShelf {
messages: this.messages, messages: this.messages,
}) })
window.i18n = i18n window.i18n = this.i18n
// Expose language loader globally
window.loadLanguage = this.loadLanguage.bind(this)
const { createPinia } = window.pinia const { createPinia } = window.pinia
app.use(router) app.use(router)
app.use(i18n) app.use(this.i18n)
app.use(createPinia()) app.use(createPinia())
app.provide('utils', utils) app.provide('utils', utils)
app.directive('tooltip', VTooltip) app.directive('tooltip', VTooltip)

View File

@@ -50,7 +50,7 @@ export const useGlobalStore = (useWindow = false) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios axios
.get('/api/v1/bootstrap') .get('/api/v1/bootstrap')
.then((response) => { .then(async (response) => {
const companyStore = useCompanyStore() const companyStore = useCompanyStore()
const userStore = useUserStore() const userStore = useUserStore()
const moduleStore = useModuleStore() const moduleStore = useModuleStore()
@@ -71,8 +71,8 @@ export const useGlobalStore = (useWindow = false) => {
moduleStore.apiToken = response.data.global_settings.api_token moduleStore.apiToken = response.data.global_settings.api_token
moduleStore.enableModules = response.data.modules moduleStore.enableModules = response.data.modules
// company store // company store
companyStore.companies = response.data.companies companyStore.companies = response.data.companies
companyStore.selectedCompany = response.data.current_company companyStore.selectedCompany = response.data.current_company
companyStore.setSelectedCompany(response.data.current_company) companyStore.setSelectedCompany(response.data.current_company)
companyStore.selectedCompanySettings = companyStore.selectedCompanySettings =
@@ -80,9 +80,31 @@ export const useGlobalStore = (useWindow = false) => {
companyStore.selectedCompanyCurrency = companyStore.selectedCompanyCurrency =
response.data.current_company_currency response.data.current_company_currency
if(typeof global.locale !== 'string') { // Determine and load the appropriate language
global.locale.value = const userLanguage = response.data.current_user_settings?.language
response.data.current_user_settings.language || 'en' const companyLanguage = response.data.current_company_settings?.language
const targetLanguage = userLanguage || companyLanguage || 'en'
// Load the language dynamically if it's not English
if (targetLanguage !== 'en' && window.loadLanguage) {
try {
await window.loadLanguage(targetLanguage)
} catch (error) {
console.warn('Failed to load language during bootstrap:', error)
// Fall back to English if loading fails
if (typeof global.locale !== 'string') {
global.locale.value = 'en'
} else {
global.locale = 'en'
}
}
} else {
// Set locale for English or when loadLanguage is not available
if (typeof global.locale !== 'string') {
global.locale.value = targetLanguage
} else {
global.locale = targetLanguage
}
} }
this.isAppLoaded = true this.isAppLoaded = true

View File

@@ -65,7 +65,8 @@ export default {
} }
if(typeof res.data.profile_language === 'string') { if(typeof res.data.profile_language === 'string') {
global.locale.value = res.data.profile_language // Use dynamic language loading instead of direct assignment
await window.loadLanguage(res.data.profile_language)
} }
let dbstep = parseInt(res.data.profile_complete) let dbstep = parseInt(res.data.profile_complete)

View File

@@ -27,6 +27,8 @@
<BaseButton <BaseButton
v-show="!isFetchingInitialData" v-show="!isFetchingInitialData"
:loading="isChangingLanguage"
:disabled="isChangingLanguage"
@click="next" @click="next"
> >
{{ $t('wizard.continue') }} {{ $t('wizard.continue') }}
@@ -43,12 +45,11 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useInstallationStore } from '@/scripts/admin/stores/installation.js' import { useInstallationStore } from '@/scripts/admin/stores/installation.js'
const { global } = window.i18n
const emit = defineEmits(['next']) const emit = defineEmits(['next'])
let isFetchingInitialData = ref(false) let isFetchingInitialData = ref(false)
let isSaving = ref(false) let isSaving = ref(false)
let isChangingLanguage = ref(false)
let languages = ref([]) let languages = ref([])
let currentLanguage = 'en' let currentLanguage = 'en'
@@ -75,11 +76,19 @@ function next() {
isSaving.value = false isSaving.value = false
} }
function changeLanguage(event){ async function changeLanguage(event) {
if(typeof global.locale !== 'string') { if (!event) return
global.locale.value = event
isChangingLanguage.value = true
try {
// Dynamically load the selected language
await window.loadLanguage(event)
currentLanguage.value = event
} catch (error) {
console.error('Failed to change language:', error)
} finally {
isChangingLanguage.value = false
} }
} }
</script> </script>

View File

@@ -200,6 +200,9 @@ async function updateUserData() {
// Update Language if changed // Update Language if changed
if (userStore.currentUserSettings.language !== userForm.language) { if (userStore.currentUserSettings.language !== userForm.language) {
// Load the new language dynamically before updating settings
await window.loadLanguage(userForm.language)
await userStore.updateUserSettings({ await userStore.updateUserSettings({
settings: { settings: {
language: userForm.language, language: userForm.language,

View File

@@ -105,7 +105,7 @@
class="w-full" class="w-full"
/> />
</BaseInputGroup> </BaseInputGroup>
<BaseInputGroup <BaseInputGroup
:label="$t('settings.preferences.time_format')" :label="$t('settings.preferences.time_format')"
:content-loading="isFetchingInitialData" :content-loading="isFetchingInitialData"
@@ -378,6 +378,12 @@ async function updatePreferencesData() {
isSaving.value = true isSaving.value = true
delete data.settings.link_expiry_days delete data.settings.link_expiry_days
// If language is being changed, load it dynamically first
if (companyStore.selectedCompanySettings.language !== settingsForm.language) {
await window.loadLanguage(settingsForm.language)
}
let res = await companyStore.updateCompanySettings({ let res = await companyStore.updateCompanySettings({
data: data, data: data,
message: 'settings.preferences.updated_message', message: 'settings.preferences.updated_message',

View File

@@ -0,0 +1,74 @@
/**
* Dynamic language loader utility
* Loads language files on demand to reduce bundle size
*/
const loadedLanguages = new Set()
const languageCache = new Map()
/**
* Dynamically import a language file
* @param {string} locale - Language code (e.g., 'en', 'fr', 'pt_BR')
* @returns {Promise<Object>} - Language messages object
*/
export async function loadLanguage(locale) {
// Return cached language if already loaded
if (languageCache.has(locale)) {
return languageCache.get(locale)
}
try {
// Dynamic import of language file
const languageModule = await import(`../../../lang/${locale === 'pt_BR' ? 'pt-br' : locale}.json`)
const messages = languageModule.default || languageModule
// Cache the loaded language
languageCache.set(locale, messages)
loadedLanguages.add(locale)
return messages
} catch (error) {
console.warn(`Failed to load language: ${locale}`, error)
// Fallback to English if available
if (locale !== 'en' && !languageCache.has('en')) {
try {
const fallbackModule = await import('../../../lang/en.json')
const fallbackMessages = fallbackModule.default || fallbackModule
languageCache.set('en', fallbackMessages)
return fallbackMessages
} catch (fallbackError) {
console.error('Failed to load fallback language (en)', fallbackError)
return {}
}
}
return languageCache.get('en') || {}
}
}
/**
* Load and set language in i18n instance
* @param {Object} i18n - Vue i18n instance
* @param {string} locale - Language code to load
* @returns {Promise<void>}
*/
export async function setI18nLanguage(i18n, locale) {
// Load the language if not already loaded
if (!loadedLanguages.has(locale)) {
const messages = await loadLanguage(locale)
i18n.global.setLocaleMessage(locale, messages)
}
// Set the locale
i18n.global.locale.value = locale
}
/**
* Check if a language is already loaded
* @param {string} locale - Language code
* @returns {boolean}
*/
export function isLanguageLoaded(locale) {
return loadedLanguages.has(locale)
}