-
-
Notifications
You must be signed in to change notification settings - Fork 448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
almost impossible to use i18n with dynamic namespaces solution with inertia + react + ssr. #2147
Comments
Hi @moha-fl-dev, I have encountered this problem before, and I solved it like this: Step 1: Share the User LocaleStart by sharing the user's locale in the // handleInertiaRequests.php
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'user' => fn() => $request->user(),
'locale' => fn() => app()->getLocale(),
]);
} Step 2: Register the
|
thanks for your help and reply. your solution seems interesting and aside from few differences it is the first solution I tried. Reason I haven't gone with this solution is the fact that it doesn't support dynamic namespaces and it loads all translations for a given language at once. although fine for a small to medium application, loading all translations at once becomes a problem for moderately larger applications. It is also anti pattern to load resources that aren't needed. I spent more time researching and trying out different approaches. I think i found a approach that loads only the necessary namespaces when needed and is also scalable. given I'm a big fan of people sharing solutions to their issues, here is mine
// i18n.config.ts
import i18next, {Resource} from 'i18next';
i18next.init({
debug: true,
supportedLngs: ['en', 'nl'],
fallbackLng: 'en',
partialBundledLanguages: true,
ns: [],
resources: {}
});
export default i18next
/**
* use addResourceBundle to add the translations to the store
* this allows us to add namespaces dynamically for every route.
* it can also set the language
*/
export function addTranslationResource(lng: string, translations: Resource) {
if (translations) {
Object.entries(translations).forEach(([namespace, resource]) => {
if (!i18next.hasResourceBundle(lng, namespace)) {
i18next.addResourceBundle(lng, namespace, resource, true, true);
}
});
}
if (i18next.language !== lng) {
i18next.changeLanguage(lng);
}
} NOTE: nothing needs to be added to
after creating the config for the i18n store, add the languages and namespaces in Example:
create a helper class that loads namespaces and language on demand. this behavior mimics // app/Helpers/TranslationHelper.php
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\File;
class TranslationHelper
{
public static function getTranslations(string $language, array $namespaces): array
{
$translations = [];
foreach ($namespaces as $namespace) {
$path = public_path("locales/$language/$namespace.json");
if (File::exists($path)) {
$translations[$namespace] = json_decode(File::get($path), true);
}
}
return $translations;
}
}
create a custom config file for your translations. in this file, a key value pair of route name and namespaces array is kept. benefit of this file is that it allows for managing namespaces in a central place instead of adding a namespace to every route inside a controller // config/translations.php
<?php
return [
'routes' => [
'profile.edit' => ['dashboard', 'profile'],
'dashboard' => ['dashboard'],
'home' => ['dashboard'],
'logout' => [], // example: routes for which no ui exists
'login' => ['auth'],
],
];
modify the // HandleInertiaRequests.php
<?php
namespace App\Http\Middleware;
use App\Helpers\TranslationHelper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Inertia\Middleware;
use Tighten\Ziggy\Ziggy;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that is loaded on the first page visit.
*
* @var string
*/
protected $rootView = 'app';
/**
* Determine the current asset version.
*/
public function version(Request $request): ?string
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
// get route name. naming each route is required
// example: dashboard.index or profile.edit
$routeName = $request->route()->getName();
// get the required namespace(s) from the config
$requiredNamespaces = config('translations.routes')[$routeName];
// load the resources. json files containing the translations
// note: is there value is caching this?
$translations = TranslationHelper::getTranslations(App::getLocale(), $requiredNamespaces);
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
],
'ziggy' => fn () => [
...(new Ziggy)->toArray(),
'location' => $request->url(),
],
'lng' => App::getLocale(),
'translations' => $translations,
];
}
}
modify // resources/js/types/index.d.ts
import {Config} from 'ziggy-js';
import {Resource} from "i18next";
export interface User {
id: number;
name: string;
email: string;
email_verified_at?: string;
}
export type PageProps<
T extends Record<string, unknown> = Record<string, unknown>,
> = T & {
auth: {
user: User;
};
ziggy: Config & { location: string };
translations: Resource // add the translation prop. will be available for every route
lng: string // add the language props. will be available for evey route
};
use the translation in routes and components // Pages/Auth/Login.tsx
import Checkbox from '@/Components/Checkbox';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import GuestLayout from '@/Layouts/GuestLayout';
import { Head, Link, useForm } from '@inertiajs/react';
import { FormEventHandler } from 'react';
import {PageProps} from "@/types";
import {addTranslationResource} from "@/utils/i18n.config";
import i18next from "@/utils/i18n.config";
export default function Login({
status,
canResetPassword,
lng,
translations
}:PageProps< {
status?: string;
canResetPassword: boolean;
}>) {
// add the translation for this route to the i18n config. this must be done for every route.
// NOTE: this function must be called in the parent component for the route
addTranslationResource(lng, translations)
const { t} = i18next // note: use the exported i18n instance from config file. import i18next from "@/utils/i18n.config";
const { data, setData, post, processing, errors, reset } = useForm({
email: '',
password: '',
remember: false,
});
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('login'), {
onFinish: () => reset('password'),
});
};
return (
<GuestLayout>
<Head title="Log in" />
{status && (
<div className="mb-4 text-sm font-medium text-green-600">
{status}
</div>
)}
<form onSubmit={submit}>
<div>
<InputLabel htmlFor="email" value={t('auth:email')} />
<TextInput
id="email"
type="email"
name="email"
value={data.email}
className="mt-1 block w-full"
autoComplete="username"
isFocused={true}
onChange={(e) => setData('email', e.target.value)}
/>
<InputError message={errors.email} className="mt-2" />
</div>
<div className="mt-4">
<InputLabel htmlFor="password" value={t('auth:password')} />
<TextInput
id="password"
type="password"
name="password"
value={data.password}
className="mt-1 block w-full"
autoComplete="current-password"
onChange={(e) => setData('password', e.target.value)}
/>
<InputError message={errors.password} className="mt-2" />
</div>
<div className="mt-4 block">
<label className="flex items-center">
<Checkbox
name="remember"
checked={data.remember}
onChange={(e) =>
setData('remember', e.target.checked)
}
/>
<span className="ms-2 text-sm text-gray-600">
{t('auth:remember_me')}
</span>
</label>
</div>
<div className="mt-4 flex items-center justify-end">
{canResetPassword && (
<Link
href={route('password.request')}
className="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
{t('auth:forgot_password')}
</Link>
)}
<PrimaryButton className="ms-4" disabled={processing}>
{t('auth:login')}
</PrimaryButton>
</div>
</form>
</GuestLayout>
);
}
using the translation in underlying/child components: steps:
that is it. Benefits to this approach are as follows:
Downsides to this approach:
Lastly: if anyone has a better approach, please share |
Discussed in #2138
Originally posted by moha-fl-dev December 16, 2024
i use react for the frontend and react-i18next as my translations and localization manager. after 3 days of trying to find a solution that works in ssr i have come up with the following:
as you can see, i get the translations on the initial render and pass them through to the client. this is to prevent mismatch between client and server because if i dont forward this to the client, react-i18next will fetch on the client after the translations have been fetched on the server.
even though this solution is far from perfect it is the closest i have been able to come to a working solution after 3 days of struggling to what would have been achievable in 20 mins with next-i18-next.
the downside to this solution is that you need to fetch all your translations on the initial render even though you might need only 1 for a given page.
how come app.tsx doesnt run when client side navigating but even more importantly what does run in client side navigation aside from the components re-render that would allow me to have translation and ssr?
The text was updated successfully, but these errors were encountered: