i18next-fs-backend
i18next-fs-backend copied to clipboard
remix-i18next server translations fail with Vercel
🐛 Bug Report
Remix-i18next server translations fail with Vercel but run perfectly locally. Locally: https://share.cleanshot.com/3PMN2F On Vercel: https://share.cleanshot.com/lc18k4
Even tho on the client side it's working like expected when I check what the server is rendered, I have my i18n key and not the text like Bonjour/Hello everyone/Allo.
Locally I do have the correct content, on Vercel I do not have the right one.
I'm not the only one that encountered that issue. https://github.com/sergiodxa/remix-i18next/discussions/95
To Reproduce
I followed the content from the remix-i18n Github project.
// entry.client.tsx
import { ApolloProvider } from "@apollo/client";
import { hydrate } from "react-dom";
import { RemixBrowser } from "@remix-run/react";
import { initApollo } from "./context/apollo";
// i18n
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { I18nextProvider, initReactI18next } from 'react-i18next';
import { getInitialNamespaces } from 'remix-i18next';
import i18n from './i18n';
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({
...i18n, // spread the configuration
// This function detects the namespaces your routes rendered while SSR use
ns: getInitialNamespaces(),
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
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: [],
},
})
.then(() => {
const client = initApollo(false);
return hydrate(
<ApolloProvider client={client}>
<I18nextProvider i18n={i18next}>
<RemixBrowser />
</I18nextProvider>
</ApolloProvider>,
document
);
});
// entry.server.tsx
import type { EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
import ApolloContext, { initApollo } from "./context/apollo";
import { ApolloProvider } from "@apollo/client";
import { getDataFromTree } from "@apollo/client/react/ssr";
import { createInstance } from "i18next";
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { I18nextProvider, initReactI18next } from "react-i18next";
import i18next from "./i18next.server";
import i18n from "./i18n";
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const client = initApollo();
// First, we create a new instance of i18next so every request will have a
// completely unique instance and not share any state
let instance = createInstance();
// Then we could detect locale from the request
let lng = await i18next.getLocale(request);
// And here we detect what namespaces the routes about to render want to use
let ns = i18next.getRouteNamespaces(remixContext);
await instance
.use(initReactI18next) // Tell our instance to use react-i18next
.use(Backend) // Setup our backend
.init({
...i18n, // spread the configuration
lng, // The locale we detected above
ns, // The namespaces the routes about to render wants to use
backend: {
loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json"),
},
});
const App = (
<ApolloProvider client={client}>
<I18nextProvider i18n={instance}>
<RemixServer context={remixContext} url={request.url} />
</I18nextProvider>
</ApolloProvider>
);
return getDataFromTree(App).then(() => {
const initialState = client.extract();
const markup = renderToString(
<ApolloContext.Provider value={initialState}>
{App}
</ApolloContext.Provider>
);
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders,
});
});
}
// i18n.ts
export default {
// This is the list of languages your application supports
supportedLngs: ["fr", "en", "nl"],
// This is the language you want to use in case
// if the user language is not in the supportedLngs
fallbackLng: "fr",
// The default namespace of i18next is "translation", but you can customize it here
defaultNS: "common",
// Disabling suspense is recommended
react: { useSuspense: false },
};
Expected behavior
The behaviour should be that the content is rendered correctly on Vercel. Looks like the locales files are not there in the build.
Your Environment
- runtime version: i.e. node v16, browser: chrome
- i18next version: i.e. ^21.9.2
- os: Mac
- any other relevant information
I suspect this is because of the way how Vercel packages the files...
Can you try to call: fs.existsSync(path) for each translation file before initializing i18next?
const french = fs.existsSync("./public/locales/fr/common.json");
console.log("french", french);
const english = fs.existsSync("./public/locales/en/common.json");
console.log("english", english);
const dutch = fs.existsSync("./public/locales/nl/common.json");
console.log("dutch", dutch);
Locally I'm getting
french true
english true
dutch true
Production
[GET] /
02:53:50:55
french false
english false
dutch false
I guess I'll have to run more test to see if it's somewhere :D
I tried all of these:
let french = fs.existsSync("./locales/fr/common.json");
console.log("french", french);
french = fs.existsSync("/locales/fr/common.json");
console.log("french", french);
french = fs.existsSync("locales/fr/common.json");
console.log("french", french);
french = fs.existsSync("../locales/fr/common.json");
console.log("french", french);
french = fs.existsSync("../../locales/fr/common.json");
console.log("french", french);
french = fs.existsSync("/public/fr/common.json");
console.log("french", french);
french = fs.existsSync("public/fr/common.json");
console.log("french", french);
french = fs.existsSync("/fr/common.json");
console.log("french", french);
french = fs.existsSync("./public/fr/common.json");
console.log("french", french);
french = fs.existsSync("../public/fr/common.json");
console.log("french", french);
All returning false :/.
I really don't get it 🤔

What happens if you directly requires the json file?
const fr = resolve("../public/locales/fr/common.json");
const frrequire = require("../public/locales/fr/common.json");
Log this on Vercel.
/var/public/locales/fr/common.json
{ greeting: 'Bonjour' }
We have the content and the place of the file. If I use loadPath: fr, it's not working.
Is there a way to have the content from frrequire to be loaded in the backend??
I'm wondering now how can we have something like ../public/locales/{{lng}}/{{ns}}.json with require.
Is this something doable ? Any idea ?
When you require the json files, are they also found by fs.exists (i18next-fs-backend)?
this might be an alternative: https://www.i18next.com/how-to/add-or-load-translations#lazy-load-in-memory-translations
... but honestly this sounds like to be a Vercel issue
I'll create a ticket on Vercel and see what's going on :)
Thanks for your help 🙏, will update you if I get some feedbacks
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Had the same issue. Using the paths as described in @JulienHe’s comment and the lazy memory loader as suggested it works on Vercel. Here’s the corresponding code in entry.server.ts:
import FileSystemBackend from "i18next-fs-backend";
import resourcesToBackend from "i18next-resources-to-backend";
// …
const ResourceBackend = resourcesToBackend(
(language, namespace, callback) => {
const path = `../public/locales/${language}/${namespace}.json`;
try {
const resource = require(path);
callback(null, resource);
} catch (error) {
console.error("Loading server locale failed", error);
callback(new Error(`Could not locale at ${path}`), null);
}
}
);
await instance
.use(initReactI18next)
.use<FileSystemBackend | BackendModule<object>>(
process.env.NODE_ENV === "development"
? FileSystemBackend
: ResourceBackend
)
.init(config);
Hey @danieljb @JulienHe, other than the code in the entry.server.ts file, what else did you have to change to make it work? I can't get it to work in Vercel
Something wrong here?
entry.server.ts
// …
let instance = createInstance();
let lng = await i18next.getLocale(request);
let ns = i18next.getRouteNamespaces(remixContext);
const ResourceBackend = resourcesToBackend(
(
language,
namespace,
callback,
) => {
const path = `../public/locales/${language}/${namespace}.json`;
try {
const resource = require(path);
callback(null, resource);
} catch (error) {
console.error('Loading server locale failed', error);
callback(new Error(`Could not locale at ${path}`), null);
}
},
);
await instance
.use(initReactI18next) // Tell our instance to use react-i18next
.use<FileSystemBackend | BackendModule<object>>(
process.env.NODE_ENV === 'development'
? FileSystemBackend
: ResourceBackend,
) // Setup our backend
.init({
...i18n, // spread the configuration
lng, // The locale we detected above
ns, // The namespaces the routes about to render wants to use
backend: {
loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'),
},
});
// …
i18next.server.ts
import Backend from 'i18next-fs-backend';
import { resolve } from 'node:path';
import { RemixI18Next } from 'remix-i18next';
import i18n from '~/i18n'; // your i18n configuration file
import { i18nCookie } from '~/utils/cookie';
let i18next = new RemixI18Next({
detection: {
cookie: i18nCookie,
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
},
// This is the configuration for i18next used
// when translating messages server-side only
i18next: {
...i18n,
backend: {
loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'),
},
},
// The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions.
// E.g. The Backend plugin for loading translations from the file system
// Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here
plugins: [Backend],
});
export default i18next;
@JulienHe have you been able to fix the problem?
I've tried @danieljb solution too but sadly it did not work. On the logs of Vercel I can see
Loading server locale failed Error: Dynamic require of "../locales/en/misc.json" is not supported
at file:///var/task/build/index.js:7:9
at file:///var/task/build/index.js:145:24
at Object.read (file:///var/task/node_modules/.pnpm/[email protected]/node_modules/i18next-resources-to-backend/dist/esm/index.js:22:9)
So far I can't figure it out.
I'm not quite sure how I made it work, but this is my fix
import { resolve } from "node:path";
import path from "path";
[...]
await instance
.use(initReactI18next)
.use(Backend)
.init({
...i18n,
lng,
ns, // The namespaces the routes about to render wants to use
backend: {
localePath: path.resolve("./public/locales"),
loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json"),
},
});
Note that we need both the localePath and loadPath to make it work.
I'm not quite sure how I made it work, but this is my fix
import { resolve } from "node:path"; import path from "path"; [...] await instance .use(initReactI18next) .use(Backend) .init({ ...i18n, lng, ns, // The namespaces the routes about to render wants to use backend: { localePath: path.resolve("./public/locales"), loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json"), }, });Note that we need both the
localePathandloadPathto make it work.
Hey @Loschcode, could you please share your i18next.server.ts and entry.client.tsx files to? Or if it's not too much to ask, the link to the repo where you managed to solve the problem