import { createApp } from 'vue' import type { App } from 'vue' import type { Router } from 'vue-router' import App_ from './App.vue' import router from './router' import { createAppI18n, setI18nLanguage } from './plugins/i18n' import type { AppI18n } from './plugins/i18n' import { createAppPinia } from './plugins/pinia' import { installTooltipDirective } from './plugins/tooltip' import { defineGlobalComponents } from './global-components' /** * Callback signature for the `booting` hook. * Receives the Vue app instance and the router so that modules / * plugins can register additional routes, components, or providers. */ type BootCallback = (app: App, router: Router) => void /** * Bootstrap class for InvoiceShelf. * * External code (e.g. dynamically loaded modules) can call * `window.InvoiceShelf.booting(callback)` to hook into the app * before it mounts. * * Call `start()` to install all plugins, execute boot callbacks, * and mount the application. */ export default class InvoiceShelf { private bootingCallbacks: BootCallback[] = [] private messages: Record> = {} private i18n: AppI18n | null = null private app: App constructor() { this.app = createApp(App_) } /** * Register a callback that will be invoked before the app mounts. */ booting(callback: BootCallback): void { this.bootingCallbacks.push(callback) } /** * Merge additional i18n message bundles (typically from modules). */ addMessages(moduleMessages: Record>): void { for (const [locale, msgs] of Object.entries(moduleMessages)) { this.messages[locale] = { ...this.messages[locale], ...msgs, } } } /** * Dynamically load and activate a language. */ async loadLanguage(locale: string): Promise { if (this.i18n) { await setI18nLanguage(this.i18n, locale) } } /** * Execute all registered boot callbacks, install plugins, * and mount the app to `document.body`. */ async start(): Promise { // Execute boot callbacks so modules can register routes / components this.executeCallbacks() // Register global components defineGlobalComponents(this.app) // i18n this.i18n = createAppI18n(this.messages) // If the install wizard's Language step set a locale before the DB // existed, honor it now so the rest of the wizard renders in the right // language. Falls through to 'en' silently on any failure. const installLanguage = this.readInstallLanguage() if (installLanguage && installLanguage !== 'en') { try { await setI18nLanguage(this.i18n, installLanguage) } catch { // Locale file missing or load failed — fall back to en, no-op. } } // Install plugins this.app.use(router) this.app.use(this.i18n) this.app.use(createAppPinia()) // Directives installTooltipDirective(this.app) // Mount this.app.mount('body') } // ---- private ---- private executeCallbacks(): void { for (const callback of this.bootingCallbacks) { callback(this.app, router) } } /** * Read the install-wizard language choice from localStorage. Wrapped in * try/catch because localStorage can throw in private-browsing edge cases. */ private readInstallLanguage(): string | null { try { return localStorage.getItem('install_language') } catch { return null } } }