Inertia

Page is not being translated when the language is changed

I have a App.vue file that is a layout included in a page

script setup is the following:

const locale = usePage().props.locale

    const locales = [
        {
            id: 'it',
            name: 'Italian',
        },
        {
            id: 'en',
            name: 'English',
        }
    ]
    
    const form = useForm({ locale: locale })

    const submit = () => {
        form.post(route('change-language'))
    }

and what is important in the template is just a select field:

<Select 
    v-model="form.locale" 
    :options="locales"
    @change="submit"
/>

I have this as shared data in HandleInertiaRequest.php:

'locale' => function () {
    return app()->getLocale();
}

My LanguageSwitcher middleware is like this:

public function handle($request, Closure $next)
{
    $locale = null;

    if (session()->has('locale')) {
        $newLocale = session()->get('locale');

        if (in_array($newLocale, config()->get('app.locales'))) {
            $locale = $newLocale;
        }
    }

    if (!$locale) {
        $locale = config()->get('app.locale');
    }

    app()->setLocale($locale);

    return $next($request);
}

And finally my LanguageController:

public function change(Request $request)
    {
        $locale = $request->locale;

        if (in_array($locale, config()->get('app.locales'))) {
            session()->put('locale', $locale);
        }

        return back();
    }

This code exactly the same worked on my previous app that wasn't built with Inertia, just Laravel/Vue, but now I've been rewriting the code and this seems that works only when i reload the page.

So... After i change language, i see that language actually changes, but on the page nothing is translated. Then as soon as i reload the page, i see translated version. Also, if i switch page after changing the language, pages are not translated, only on page reload.

Thank you in advance!

shogun
shogun
0
13
540
alex
alex
Moderator

Looking forward to solving this one! I can't see anything immediately obvious, so I'm going to re-create this locally and will post the fix/updated solution here when I'm done.

shogun
shogun

Thank you a lot Alex :)

alex
alex
Moderator

Can you just let me know (and even better give code examples) of how you're displaying translated text in your Vue components?

shogun
shogun

const __ = (key, replacements = {}) => {
        let translation = window._translations[key] || key;
      
        Object.keys(replacements).forEach(r => {
            translation = translation.replace(`:${r}`, replacements[r]);
        });
      
        return translation;
    }

I'm using this as a function for translations (similar like laravel, but i made my own system doing the same thing).

then in templates i do something like this:

{{ __('string_to_be_translated')  }}

This is y Translations.php component

<?php

namespace App\View\Components;

use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\View\Component;

class Translations extends Component
{
    /**
     * Get the view / contents that represent the component.
     */
    public function render(): View|Closure|string
    {
        $locale = app()->getLocale();

        $translations = Cache::rememberForever("translations_$locale", function () use ($locale) {
            $phpTranslations = [];
            $jsonTranslations = [];
    
            if (File::exists(base_path("lang/$locale"))) {
                $phpTranslations = collect(File::allFiles(base_path("lang/$locale")))
                    ->filter(function ($file) {
                        return $file->getExtension() == 'php';
                    })->flatMap(function ($file) {
                        return Arr::dot(File::getRequire($file->getRealPath()));
                    })->toArray();
            }
    
            if (File::exists(base_path("lang/$locale.json"))) {
                $jsonTranslations = json_decode(File::get(base_path("lang/$locale.json")), true);
            }
    
            return array_merge($phpTranslations, $jsonTranslations);
        });

        return view('components.translations', compact('translations'));
    }
}

translations.blade.php is something like this

<script>
    window._translations = @json($translations)
</script>

Then i include the component in app.blade.php

<body class="font-sans antialiased">
        @inertia

        <x-translations></x-translations>
    </body>

Hope this helps!

shogun
shogun

I thought caching was the problem, but not... I was wrong. Just thought i could try.

shogun
shogun

Maybe because translations are not reactive. When I check the page source, they stay the same. Could this be the problem?

shogun
shogun

Probably the best idea would be to put the translations not inside the laravel component, but inside the inertia shared data middleware...

alex
alex
Moderator

I'm just working through a solution for you. I have the reactivity working, and will create a simplified way to share/access the translations too.

shogun
shogun

I fixed it. It works... Thank you and sorry for troubling you.

alex
alex
Moderator

No worries! I've created a response anyway, and have included a GitHub repository. A lot of what you've done can be simplified to avoid any future headaches :)

alex
alex
Moderator

I've created a repository with a working example of this, which you can find here. However, I'll write the solution and steps out below with some context.

1. Use an enum to store the available languages.

This makes it easier to have all available languages in one place for display/validation.

enum Lang: string
{
    case EN = 'en';
    case DE = 'de';

    public function label(): string
    {
        return match($this) {
            self::EN => 'English',
            self::DE => 'Deutsch',
        };
    }
}

2. Also create a LangResource API resource to make this easy to display.

class LangResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'value' => $this->value,
            'label' => $this->label(),
        ];
    }
}

3. This makes it a bit easier to switch and validate the language through the controller.

Route::post('/language', LanguageStoreController::class)->name('language.store');
class LanguageStoreController extends Controller
{
    public function __invoke(Request $request)
    {
        session()->put('language', Lang::tryFrom($request->language)->value);

        return back();
    }
}

4. Now you can simplify the dropdown for selecting the language

This no longer relies on a form – everything is done within this select element.

<select name="language" id="language" v-on:change="router.post(route('language.store'), { language: $event.target.value })">
    <option v-for="language in $page.props.languages" :key="language.value" :value="language.value" :selected="language.value === $page.props.language">
        {{ language.label }}
    </option>
</select>

5. The middleware can now be simplified too.

class SetLanguage
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        app()->setLocale(Lang::tryFrom(session()->get('language', config('app.locale')))->value);

        return $next($request);
    }
}

6. Read and share translations from HandleInertiaRequests

This may be too simplified for your use, and doesn't include caching. Feel free to modify this. The output from this is something like this:

['auth.failed' => 'These credentials do not match our records.', 'auth.password' => 'The provided password is incorrect.', // etc]
'translations' => function () {
    return collect(File::allFiles(base_path('lang/' . app()->getLocale())))
        ->flatMap(function ($file) {
            return Arr::dot(
                File::getRequire($file->getRealPath()),
                $file->getBasename('.' . $file->getExtension()) . '.'
            );
        });
}

7. Create a translation helper

This is pretty much the same as yours, but reads from shared data from the Inertia middleware instead

import { usePage } from "@inertiajs/vue3"

export default function __ (key, replacements = {}) {
    let translation = usePage().props.translations[key]

    Object.keys(replacements).forEach(r => {
        translation = translation.replace(`:${r}`, replacements[r])
    })

    return translation
}

8. Import and register the __ function as a global in Vue

Do this within setup() in app.js

import __ from '@/lang'

//...

setup({ el, App, props, plugin }) {
    const app = createApp({ render: () => h(App, props) })
        .use(plugin)
        .use(ZiggyVue, Ziggy)

    app.config.globalProperties.__ = __

    app.mount(el)
},

9. Use within templates

{{ __('dashboard.greeting', { name: $page.props.auth.user.name }) }}

Hope this helps!

shogun
shogun

Thank you very much. Looks nice.

Btw, middleware was created like 7 years ago, so i just copy pasted from the old app since I'm rebuilding it from scratch. Had in plan to refactor it for sure. Looks terrible :D

This is how i solved it:

I just moved the code from translation component to HandleInertiaRequests middleware

'translations' => function () {
                $locale = app()->getLocale();

                return Cache::rememberForever("translations_$locale", function () use ($locale) {
                    $phpTranslations = [];
                    $jsonTranslations = [];
            
                    if (File::exists(base_path("lang/$locale"))) {
                        $phpTranslations = collect(File::allFiles(base_path("lang/$locale")))
                            ->filter(function ($file) {
                                return $file->getExtension() == 'php';
                            })->flatMap(function ($file) {
                                return Arr::dot(File::getRequire($file->getRealPath()));
                            })->toArray();
                    }
            
                    if (File::exists(base_path("lang/$locale.json"))) {
                        $jsonTranslations = json_decode(File::get(base_path("lang/$locale.json")), true);
                    }
            
                    return array_merge($phpTranslations, $jsonTranslations);
                });
            },

and then added this for the __ function, since props are not reactive

const translations = computed(() => usePage().props.translations)

Of course removed all the blade stuff.

I will also check you code and see if i can use something to make it better, seems more concise and clearer.

Haz
Haz
Moderator

Stealing this. Thanks.