Certainly! The problem you're seeing—namely, the original label flash before translation is shown—is a classic Flash of Untranslated Content (FOUC) or "flicker" due to your i18n setup being asynchronous during component mount.
This means Vue renders your template before the i18n resources are loaded and registered, so the first render shows the raw key, and then the translations appear once they’re loaded.
Why does this happen?
- The i18n plugin is initialized asynchronously (
resolve: async (lang: string) => ...), so translations aren't immediately available at first render. - Until the async loading is done and i18n is ready,
$tjust returns the key ('hello world').
Two Main Solutions
1. Delay Application Mount Until i18n is Ready
Best Practice: Only mount your Vue app after the i18n resources are loaded.
How-To:
Refactor your setup so that loading and mounting are done after i18n files are loaded, e.g.:
import { createApp, h } from 'vue';
import { i18nVue } from 'laravel-vue-i18n';
import { createInertiaApp } from '@inertiajs/vue3';
createInertiaApp({
// ...other settings...
setup({ el, App, props, plugin }) {
// Language code from props, fallback 'en'
const lang = (props.initialPage.props.locale as string) ?? 'en';
// Dynamically import translation JSON
const langs = import.meta.glob('../../lang/*.json');
// Start loading translation before mounting app
langs[`../../lang/${lang}.json`]()
.then(messages => {
const app = createApp({ render: () => h(App, props) });
app.use(plugin);
app.use(i18nVue, {
lang: lang,
fallbackLang: 'en',
messages: messages.default ?? messages,
});
if (typeof window !== 'undefined' && el) {
app.mount(el);
}
});
},
});
- Now your app will only be mounted after the translations are loaded, eliminating the flicker.
2. Hide the App with CSS Until Translations Are Ready
Another simple, though less semantically clean, solution is to hide your app until i18n is (re)hydrated.
- Add a style in your
app.blade.php(orindex.html) to initially hide the app:<style> [data-app-loading] { display: none; } </style> - Add a
data-app-loadingattribute to your app container:<div id="app" data-app-loading></div> - In your Vue mount logic, remove the attribute after mounting/app is ready:
// After app.mount(el) el.removeAttribute('data-app-loading');
3. For a More Seamless DX, Use Synchronous Locale Loading (Optional)
If your app only supports a handful of locales and bundle size is not an issue, you might import all of them at build time:
import en from '../../lang/en.json';
import es from '../../lang/es.json';
// etc.
const messages = { en, es /* ... */ };
// Then setup synchronously
app.use(i18nVue, {
lang,
fallbackLang: 'en',
messages: messages[lang] ?? messages['en'],
});
Summary
The issue is caused by async loading of translation files. The safest fix:
Defer .mount() until your translations are loaded, so the app only renders after i18n is ready.
Key Refactored Setup Example
import { createApp, h } from 'vue';
import { i18nVue } from 'laravel-vue-i18n';
import { createInertiaApp } from '@inertiajs/vue3';
createInertiaApp({
// ...other settings...
setup({ el, App, props, plugin }) {
const lang = props.initialPage.props.locale ?? 'en';
const langs = import.meta.glob('../../lang/*.json');
langs[`../../lang/${lang}.json`]()
.then(messages => {
const app = createApp({ render: () => h(App, props) });
app.use(plugin);
app.use(i18nVue, {
lang,
fallbackLang: 'en',
messages: messages.default ?? messages,
});
app.mount(el);
});
},
});
This ensures the Vue app is not shown until translations are in place, preventing the FOUC.
Let me know if you need more help!