sergiodxa / remix-i18next

The easiest way to translate your Remix apps
https://sergiodxa.github.io/remix-i18next/
MIT License
602 stars 44 forks source link

Warning: Text content did not match. (Server/Client) #177

Closed Loschcode closed 8 months ago

Loschcode commented 8 months ago

Describe the bug

After copying almost exactly the remix-18next instructions in my project, the client side doesn't seem to update the translations when I update the JSON file, so it throws the following error

react-dom.development.js:86 Warning: Text content did not match. Server: "Aquiestoy EN" Client: "Aquiestoy"
    at div
    at div
    at a
    at LinkWithRef (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:5723:9)
    at http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:6618:3
    at Logo (http://localhost:3000/build/_shared/chunk-JDPKH4MH.js:122:7)
    at div
    at div
    at div
    at div
    at Header (http://localhost:3000/build/_shared/chunk-JDPKH4MH.js:174:16)
    at Index (http://localhost:3000/build/_shared/chunk-JDPKH4MH.js:1299:62)
    at RenderedRoute (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:3893:5)
    at Outlet (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:4311:26)
    at body
    at html
    at App (http://localhost:3000/build/root-ESODRE53.js:91:7)
    at RenderedRoute (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:3893:5)
    at RenderErrorBoundary (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:4540:9)
    at DataRoutes2 (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:5186:5)
    at Router (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:4318:15)
    at RouterProvider2 (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:5003:5)
    at RemixErrorBoundary (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:7216:5)
    at RemixBrowser (http://localhost:3000/build/_shared/chunk-6JHZZSOU.js:8074:46)
    at I18nextProvider (http://localhost:3000/build/_shared/chunk-ESP2UOJP.js:122:5)

The content of my configuration is as follow

// i18n.tsx
export default {
  supportedLngs: ["en", "es"],
  fallbackLng: "en",
  defaultNS: "misc",
  // important for dev reload
  reloadOnPrerender: process.env.NODE_ENV == "development",
};

// i18next.server.tsx
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next/server";
import i18n from "~/i18n"; // your i18n configuration file

let i18next = new RemixI18Next({
  detection: {
    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;

// entry.server.tsx
import {
  createReadableStreamFromReadable,
  type EntryContext,
} from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { createInstance } from "i18next";
import Backend from "i18next-fs-backend";
import { isbot } from "isbot";
import { resolve } from "node:path";
import { renderToPipeableStream } from "react-dom/server";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { PassThrough } from "stream";
import i18n from "./i18n"; // your i18n configuration file
import i18next from "./i18next.server";

const ABORT_DELAY = 5000;

export default async function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  let callbackName = isbot(request.headers.get("user-agent"))
    ? "onAllReady"
    : "onShellReady";

  let instance = createInstance();
  let lng = await i18next.getLocale(request);
  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") },
    });

  return new Promise((resolve, reject) => {
    let didError = false;

    let { pipe, abort } = renderToPipeableStream(
      <I18nextProvider i18n={instance}>
        <RemixServer context={remixContext} url={request.url} />
      </I18nextProvider>,
      {
        [callbackName]: () => {
          let body = new PassThrough();
          const stream = createReadableStreamFromReadable(body);
          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: didError ? 500 : responseStatusCode,
            })
          );

          pipe(body);
        },
        onShellError(error: unknown) {
          reject(error);
        },
        onError(error: unknown) {
          didError = true;

          console.error(error);
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}

// entry.client.tsx
import { RemixBrowser } from "@remix-run/react";
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { getInitialNamespaces } from "remix-i18next/client";
import i18n from "./i18n";

async function hydrate() {
  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({
      ...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: [],
      },
    });

  startTransition(() => {
    hydrateRoot(
      document,
      <I18nextProvider i18n={i18next}>
        <StrictMode>
          <RemixBrowser />
        </StrictMode>
      </I18nextProvider>
    );
  });
}

if (window.requestIdleCallback) {
  window.requestIdleCallback(hydrate);
} else {
  // Safari doesn't support requestIdleCallback
  // https://caniuse.com/requestidlecallback
  window.setTimeout(hydrate, 1);
}

As I said, I basically just copied everything and replaced the common namesapce by misc, that's all. The only place I use it, I do it like so

import { useTranslation } from "react-i18next";

export function Logo() {
  const { t } = useTranslation("misc");

  return (
    <Link onClick={scrollToTop}>
      <div className="flex justify-start">
        <div>
          <img alt="logo-aquiestoy" style={{ width: "40px" }} src={logo} />
        </div>
        <div
          className="pl-3 mt-auto mb-auto text-2xl font-semibold tracking-normal"
          style={{ color: "rgb(17, 24, 39)" }}
        >
          {t("logoname")}
        </div>
      </div>
    </Link>
  );
}

Is there something wrong with my code? One disturbing detail is also that yesterday it just didn't show any translation, it returned the key, but now it shows an old translation, like it finally took a change into consideration but is never quite updated. What am I missing here?

Your Example Website or App

https://github.com/aquiestoy-io/app

Steps to Reproduce the Bug or Issue

  1. Copy the example given on the site of Remix
  2. Use it

Expected behavior

  1. It renders the right translation without yielding any error

Screenshots or Videos

Screenshot 2024-02-24 at 10 27 26

Platform

"@remix-run/css-bundle": "^2.6.0",
"@remix-run/node": "^2.6.0",
"@remix-run/react": "^2.6.0",
"@remix-run/serve": "^2.6.0",
"i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-fs-backend": "^2.3.1",
"i18next-http-backend": "^2.5.0",

Additional context

No response

Loschcode commented 8 months ago

After following my intuition, I commented this part

if (window.requestIdleCallback) {
  window.requestIdleCallback(hydrate);
} else {
  // Safari doesn't support requestIdleCallback
  // https://caniuse.com/requestidlecallback
  window.setTimeout(hydrate, 1);
}

And it fixed the problem. I'm not quite sure why but I hope it helps other people in the same situation.

sergiodxa commented 8 months ago

Are you adding export const handle = { i18n: ["misc"] } in the route where you call useTranslation("misc")?

Also change the supported languages to set your default/fallback one last, otherwise it may prioritize it over the other options.