RogerManich's avatar

Inertia + vue3 starter kit Localization support.

Hi,

I am in learning process and I started a new project to practice a bit. My first goal is having language support to my App using vue starter kit.

The backend part is acomplished and the frontend almost acomplished there is a refresh issue. When I refresh the page I see the original labels and then the translated ones. This is weird for me. But first I want to review what I did in case I did somenthing wrong. Once I create standard project with Larave 13 + inertia + vue3 + authorization enabled:

  1. populate language files
php artisan lang:publish
  1. create en.json in lang folder with my labels.
  2. translate al language files to my supported languages (somthing I'll do at the end. For the moment. Just create a few keys tot test it.)
  3. Add new field in users to store desired languge (in fact it is a json field named preferences with multiple options.)
  4. Create a new middleware SetLocal to get current language in Laravel
  1. Register middleware in bootstrap app.php
<?php

use App\Http\Middleware\HandleAppearance;
use App\Http\Middleware\HandleInertiaRequests;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
use App\Http\Middleware\SetLocale;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->encryptCookies(except: ['appearance', 'sidebar_state']);

        $middleware->web(append: [
            HandleAppearance::class,
            SetLocale::class, /* THIS */
            HandleInertiaRequests::class,
            AddLinkHeadersForPreloadedAssets::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();
  1. add in shere method in HandleInertiaRequests two data to be shered with frontend. current language and available language (for future language selector not covered here.).
public function share(Request $request): array
{
    return [
        ...parent::share($request),
        'auth' => [
            'user' => $request->user(),
        ],
        'locale' => fn () => app()->getLocale(),
        'available_locales' => fn () => config('app.available_locales'),
    ];
}
  1. Install laravel-vue-i18n
  2. Add in vite.config.js
//at the beginning of the file.
import i18n from 'laravel-vue-i18n/vite';

//in plugins section
i18n(),

  1. In app ts
//at the beginning of file
import { createApp, h } from 'vue';
import { i18nVue } from 'laravel-vue-i18n';

//Inside the createInertiaApp

setup({ el, App, props, plugin }) {
        const app = createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(i18nVue, {
                lang: (props.initialPage.props.locale as string) ?? 'en',
                fallbackLang: 'en',
                resolve: async (lang: string) => {
                    const langs = import.meta.glob('../../lang/*.json');
                    return await langs[`../../lang/${lang}.json`]();
                },
            });
             // Only mount in the browser. On SSR `el` is undefined and mounting
        // would try to access `window`.
        if (typeof window !== 'undefined' && el) {
            app.mount(el);
        }

        return app;
    },
  1. Now we can use this to translate

in php

__('hello world!');

In vue3

{{ $t('hello world') }}

And now my question:

It works fine. But when I refresh the page I see the original label "hello world" and then the translated text "Hola món". Is there something wrong? Maybe my vue part is not quite fine and I forget something or I doing something too late?

thank you

0 likes
2 replies
LaryAI's avatar
Level 58

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, $t just 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.:

  • 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.

  1. Add a style in your app.blade.php (or index.html) to initially hide the app:
    <style>
        [data-app-loading] { display: none; }
    </style>
    
  2. Add a data-app-loading attribute to your app container:
    <div id="app" data-app-loading></div>
    
  3. 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!

RogerManich's avatar

Finally I found a possible solution that avoid flickr and no warnings errors on debug console.

Please or to participate in this conversation.