i18next-fs-backend icon indicating copy to clipboard operation
i18next-fs-backend copied to clipboard

remix-i18next server translations fail with Vercel

Open JulienHe opened this issue 3 years ago • 10 comments
trafficstars

🐛 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

JulienHe avatar Oct 08 '22 02:10 JulienHe

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?

adrai avatar Oct 08 '22 06:10 adrai

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

JulienHe avatar Oct 08 '22 06:10 JulienHe

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 🤔

CleanShot 2022-10-08 at 03 05 17@2x

JulienHe avatar Oct 08 '22 07:10 JulienHe

What happens if you directly requires the json file?

adrai avatar Oct 08 '22 07:10 adrai

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 ?

JulienHe avatar Oct 08 '22 15:10 JulienHe

When you require the json files, are they also found by fs.exists (i18next-fs-backend)?

adrai avatar Oct 08 '22 15:10 adrai

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

adrai avatar Oct 08 '22 15:10 adrai

I'll create a ticket on Vercel and see what's going on :)

JulienHe avatar Oct 08 '22 15:10 JulienHe

Thanks for your help 🙏, will update you if I get some feedbacks

JulienHe avatar Oct 08 '22 15:10 JulienHe

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.

stale[bot] avatar Oct 15 '22 18:10 stale[bot]

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);

danieljb avatar Nov 10 '22 09:11 danieljb

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;

jpresaor avatar Aug 29 '23 08:08 jpresaor

@JulienHe have you been able to fix the problem?

juniorforlife avatar Sep 01 '23 10:09 juniorforlife

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.

Loschcode avatar Feb 24 '24 21:02 Loschcode

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.

Loschcode avatar Feb 29 '24 17:02 Loschcode

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.

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

AnthonyLzq avatar Jun 20 '24 09:06 AnthonyLzq