I keep getting a 404 on a locales/dev/common.json file #112
-
I followed the directions to setup remix-18next. But I ran into an issue that whenever I load a page I see a call to GET http://localhost:3000/locales/dev/common.json 404 (Not Found). I tried looking into what could be the cause of the issue and found that the entry.client.tsx within the I'm using remix with MUI and remix-i18next:
entry.client.tsx import { CacheProvider, ThemeProvider } from '@emotion/react';
import { CssBaseline } from '@mui/material';
import { RemixBrowser } from '@remix-run/react';
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { startTransition } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { I18nextProvider, initReactI18next } from 'react-i18next';
import { getInitialNamespaces } from 'remix-i18next';
import { i18nConfig } from './services/i18n.server';
import createEmotionCache from './styles/createEmotionCache';
import theme from './styles/theme';
const emotionCache = createEmotionCache();
async function hydrate() {
if (!i18next.isInitialized) {
await i18next
.use(initReactI18next) // Tell i18next to use the react-i18next plugin
.use(LanguageDetector) // Setup a client-side language detector
.use(Backend) // Setup your backend
.init({
...i18nConfig, // spread the configuration
// This function detects the namespaces your routes rendered while SSR use
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
ns: getInitialNamespaces(),
detection: {
// Here only enable htmlTag detection, we'll detect the language only
// server-side with remix-i18next, by using the `<html lang>` attribute
// we can communicate to the client the language detected server-side
order: ['htmlTag'],
// Because we only use htmlTag, there's no reason to cache the language
// on the browser, so we disable it
caches: [],
},
});
}
startTransition(() => {
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<RemixBrowser />
</ThemeProvider>
</CacheProvider>
</I18nextProvider>,
);
});
}
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
setTimeout(hydrate, 1);
} entry.server.tsx import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider } from '@mui/material/styles';
import type { EntryContext } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import { createInstance } from 'i18next';
import Backend from 'i18next-fs-backend';
import { resolve as resolvePath } from 'node:path';
import { renderToString } from 'react-dom/server';
import { I18nextProvider, initReactI18next } from 'react-i18next';
import i18next, { i18nConfig } from './services/i18n.server';
import ClientStylesContext from './styles/ClientStylesContext';
import createEmotionCache from './styles/createEmotionCache';
import theme from './styles/theme';
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
// First, we create a new instance of i18next so every request will have a
// completely unique instance and not share any state
const instance = createInstance();
// Then we could detect locale from the request
const lng = await i18next.getLocale(request);
// And here we detect what namespaces the routes about to render want to use
const ns = i18next.getRouteNamespaces(remixContext);
// First, we create a new instance of i18next so every request will have a
// completely unique instance and not share any state.
await instance
.use(initReactI18next) // Tell our instance to use react-i18next
.use(Backend) // Setup our backend.init({
.init({
...i18nConfig, // use the same configuration as in your client side.
lng, // The locale we detected above
ns, // The namespaces the routes about to render want to use
backend: {
loadPath: resolvePath('./public/locales/{{lng}}/{{ns}}.json'),
},
});
const MuiRemixServer = () => (
<I18nextProvider i18n={instance}>
<CacheProvider value={cache}>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<RemixServer context={remixContext} url={request.url} />
</ThemeProvider>
</CacheProvider>
</I18nextProvider>
);
// Render the component to a string.
const html = renderToString(
<ClientStylesContext.Provider value={null}>
<MuiRemixServer />
</ClientStylesContext.Provider>,
);
// Grab the CSS from emotion
const emotionChunks = extractCriticalToChunks(html);
// Re-render including the extracted css.
const markup = renderToString(
<ClientStylesContext.Provider value={emotionChunks.styles}>
<MuiRemixServer />
</ClientStylesContext.Provider>,
);
responseHeaders.set('Content-Type', 'text/html');
return new Response(`<!DOCTYPE html>${markup}`, {
status: responseStatusCode,
headers: responseHeaders,
});
} root.tsx import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useCatch,
useLoaderData,
useMatches,
} from '@remix-run/react';
import * as React from 'react';
import { useContext } from 'react';
import ClientStylesContext from './styles/ClientStylesContext';
import theme from './styles/theme';
import { json, LoaderArgs } from '@remix-run/node';
import { useTranslation } from 'react-i18next';
import Layout from './components/Layout';
import i18next from './services/i18n.server';
const ROUTES_WITHOUT_LAYOUT = ['routes/signin'];
export function useChangeLanguage(locale = 'en') {
const { i18n } = useTranslation();
React.useEffect(() => {
i18n.changeLanguage(locale);
}, [locale, i18n]);
}
export async function loader({ request }: LoaderArgs) {
const locale = await i18next.getLocale(request);
return json({ locale });
}
export const handle = {
i18n: ['common'],
};
function Document({ children, title }: { children: React.ReactNode; title?: string }) {
const styleData = useContext(ClientStylesContext);
const { i18n } = useTranslation();
// Get the locale from the loader
const loaderData = useLoaderData<typeof loader>();
useChangeLanguage(loaderData?.locale);
return (
<html lang={i18n.language}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="theme-color" content={theme.palette.primary.main} />
{title ? <title>{title}</title> : null}
<Meta />
<Links />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
{styleData?.map(({ key, ids, css }) => (
<style
key={key}
data-emotion={`${key} ${ids.join(' ')}`}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: css }}
/>
))}
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
{process.env.NODE_ENV === 'development' && <LiveReload />}
</body>
</html>
);
}
export default function App() {
const matches = useMatches();
const showLayout = matches.every((el) => !ROUTES_WITHOUT_LAYOUT.includes(el.id));
if (showLayout) {
return (
<Document>
<Layout>
<Outlet />
</Layout>
</Document>
);
}
return (
<Document>
<Outlet />
</Document>
);
}
// https://remix.run/docs/en/v1/api/conventions#errorboundary
export function ErrorBoundary({ error }: { error: Error }) {
console.error(error);
return (
<Document title="Error!">
<Layout>
<div>
<h1>There was an error</h1>
<p>{error.message}</p>
<hr />
<p>Hey, developer, you should replace this with what you want your users to see.</p>
</div>
</Layout>
</Document>
);
}
// https://remix.run/docs/en/v1/api/conventions#catchboundary
export function CatchBoundary() {
const caught = useCatch();
let message;
switch (caught.status) {
case 401:
message = <p>Oops! Looks like you tried to visit a page that you do not have access to.</p>;
break;
case 404:
message = <p>Oops! Looks like you tried to visit a page that does not exist.</p>;
break;
default:
throw new Error(caught.data || caught.statusText);
}
return (
<Document title={`${caught.status} ${caught.statusText}`}>
<Layout>
<h1>
{caught.status}: {caught.statusText}
</h1>
{message}
</Layout>
</Document>
);
} i18n.server.ts import Backend from 'i18next-fs-backend';
import { resolve } from 'node:path';
import { RemixI18Next } from 'remix-i18next';
export const i18nConfig = {
debug: process.env.NODE_ENV !== 'production',
fallbackLng: 'en',
supportedLngs: ['en', 'es'],
defaultNS: 'common',
react: { useSuspense: false },
};
const i18next = new RemixI18Next({
detection: {
supportedLanguages: i18nConfig.supportedLngs,
fallbackLanguage: i18nConfig.fallbackLng,
},
// This is the configuration for i18next used
// when translating messages server-side only
i18next: {
backend: {
loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'),
},
},
// The backend you want to use to load the translations
// Tip: You could pass `resources` to the `i18next` configuration and avoid
// a backend here
backend: Backend,
});
export default i18next; Any help appreciated! Also, thanks in advance! |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
NVM I found the issue. I had to separate the config from the |
Beta Was this translation helpful? Give feedback.
NVM I found the issue. I had to separate the config from the
i18n.server.ts