Skip to content
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

setStaticParamsLocale doesn't work properly on static route handlers (build time) #319

Open
cglacet opened this issue Dec 24, 2023 · 9 comments
Labels
bug Something isn't working

Comments

@cglacet
Copy link

cglacet commented Dec 24, 2023

Describe the bug
Locale doesn't seem to be properly set on route handlers (app directory)

To Reproduce
Create a route handler and try to retrieve the translation for it, for example:

// File app/user/[slug]/route.tsx
import { ImageResponse } from "next/og";
import { setStaticParamsLocale } from "next-international/server";
import { getScopedI18n } from "locales/server";

export async function GET(_: Request, { params }: { slug: string, locale: string }) {
    setStaticParamsLocale(params.locale);
    const t = await getScopedI18n("user-profile");
    return new ImageResponse(<p>{t("user-name", { slug })}</p>); 
}

If you build a project with this route, you will get the following error:

Error: Could not find locale while pre-rendering page, make sure you called `setStaticParamsLocale` at the top of your pages

Expected behavior
Translation data should be available. I have the requested locale in my hands so its a bit frustrating to not have access to a variant of getScopedI18n where I could simply give the locale as parameter.

About

Node v18.18.2, dependencies:

├─ next-international@1.1.4
└─ next@14.0.4
@cglacet cglacet added the bug Something isn't working label Dec 24, 2023
@cglacet
Copy link
Author

cglacet commented Dec 24, 2023

I've tested to pass the locale I have to the getScopedI18n and it seems to work, its a hack that is very ugly as a end user but I wanted to make sure that was the only issue here. So yes, it just seems like the locale isn't correctly set by setStaticParamsLocale in that particular case.

To test this on my side I did this patch directly in the dist folder your package exposes:

function createGetScopedI18n(locales, config) {
-  return function getScopedI18n(scope) {
+  return function getScopedI18n(scope, userLocale = null) {
     return __async(this, null, function* () {
-     const locale = getLocaleCache();
+     const locale = userLocale ? userLocale : getLocaleCache();
      return createT(
        {
          localeContent: flattenLocale((yield locales[locale]()).default),
          fallbackLocale: config.fallbackLocale ? flattenLocale(config.fallbackLocale) : void 0,
          locale
        },
        scope
      );
    });
  };
}

With this change I can now use the following:

// File app/user/[slug]/route.tsx
import { ImageResponse } from "next/og";
import { getScopedI18n } from "locales/server";

export async function GET(_: Request, { params }: { slug: string, locale: string }) {
    // @ts-ignore
    const t = await getScopedI18n("user-profile", params.locale);
    return new ImageResponse(<p>{t("user-name", { slug })}</p>); 
}

Ideally I guess it would be better to have setStaticParamsLocale to work, I tried reading the code but there is too much I don't understand to have any chance in finding the issue.

@cglacet
Copy link
Author

cglacet commented Dec 24, 2023

Another thing I tried that also work (no idea if it makes sense) but I can also set the cookie that is internally used by getLocaleCache:

cookies().set("Next-Locale", params.locale);

Which also solves the issue but its not ideal to have this strange line of code.

The complete code:

// File app/user/[slug]/route.tsx
import { ImageResponse } from "next/og";
import { getScopedI18n } from "locales/server";

export async function GET(_: Request, { params }: { slug: string, locale: string }) {
    cookies().set("Next-Locale", params.locale);
    const t = await getScopedI18n("user-profile");
    return new ImageResponse(<p>{t("user-name", { slug })}</p>); 
}

@cglacet
Copy link
Author

cglacet commented Dec 24, 2023

I've noticed next-intl did use the first strategy (adding an optional argument that can force the locale):

import {NextResponse} from 'next/server';
import {getTranslations} from 'next-intl/server';
 
export async function GET(request) {
  // Example: Receive the `locale` via a search param
  const {searchParams} = new URL(request.url);
  const locale = searchParams.get('locale');
 
  const t = await getTranslations({locale, namespace: 'Hello'});
  return NextResponse.json({title: t('title')});
}

Eventhough they use the same strategy as you for server components, so maybe they had a similar issue?

import {getTranslations} from 'next-intl/server';
 
export default async function ProfilePage() {
  const user = await fetchUser();
  const t = await getTranslations('ProfilePage');
 
  return (
    <PageLayout title={t('title', {username: user.name})}>
      <UserDetails user={user} />
    </PageLayout>
  );
}

@QuiiBz
Copy link
Owner

QuiiBz commented Dec 24, 2023

Very interesting use-case. There are two possibilities:
A) params.locale is undefined, so setStaticParamsLocale will set an undefined locale
B) setStaticParamsLocale doesn't work in this case

A is more likely, since your handler is located in app/user/[slug]/route.tsx, which only have a [slug] dynamic param but no [locale] dynamic param. You could do the same as what next-intl recommends, and I think we should also document it properly.

@QuiiBz
Copy link
Owner

QuiiBz commented Dec 24, 2023

The reason why it doesn't work when building the app without using cookies or headers is that Next.js will by default try to statically render the route handler, if it doesn't depends on any dynamic input (specifically cookies, headers, url params...).

For example, this works in dev but not in build:

export async function GET(request: NextRequest) {
  const t = await getI18n();
  return NextResponse.json({ hello: t('hello') });
}

But this works both in dev and in build:

export async function GET(request: NextRequest) {
  request.cookies;

  const t = await getI18n();
  return NextResponse.json({ hello: t('hello') });
}

@cglacet
Copy link
Author

cglacet commented Dec 24, 2023

Ah, sorry, I made a mistake while modifying the path to post this message, my actual file is in the [locale] subtree, so A) is not the issue.

I confirm that simply adding a reference to request.cookies does also solve the issue. That's a bit weird tho, because its still generated statically (at build time). Also the handler without the cookie is still dependent on the params, and both the slug and locale are properly set (I added a console log in the GET route and it prints the expected value pairs).

I tried calling setStaticParamsLocale with this modifications:

// node_modules/next-international/dist/app/server/index.js
var getStaticParamsLocale = () => {
  console.log("getStaticParamsLocale", getLocale().current);
  getLocale().current;
};
var setStaticParamsLocale = (value) => {
  console.log("setStaticParamsLocale", value);
  getLocale().current = value;
};

It prints the correct locale in both get and set with the request.cookies line, but get prints undefined if I remove the request.cookies.

So I guess you are completely right and all of this is just an issue with Next build process trying to be smart (but failing?).

After reading the doc a bit I think the most likely suggestion you can make in your own doc is to simply include the "force-dynamic" settings in route handlers that are using translations and generateStaticParams:

// File app/[locale]/user/[slug]/route.tsx
import { ImageResponse } from "next/og";
import { setStaticParamsLocale } from "next-international/server";
import { getScopedI18n } from "locales/server";

+export const dynamic = "force-dynamic";

export async function GET(_: Request, { params }: { slug: string, locale: string }) {
    setStaticParamsLocale(params.locale);
    const t = await getScopedI18n("user-profile");
    return new ImageResponse(<p>{t("user-name", { slug })}</p>); 
}

In case someone else is comming here, my real use case is to generate opengraph images, this use case might be something several other users of your package might need because there is currenly a bug in Next with opengraph image static generation: OpenGraph images are not statically generated for dynamic routes.

The current patch that seems to work fine is to create a route handler to generate the images and manually add a reference to these images in the metadata as suggested here.

I think it would be a good idea to add a documentation page about this matter (export const dynamic = "force-dynamic";).

EDIT I understand it now, the pages were rendered at build time because I had generateStaticParams declared, so it built dynamic pages at build time, these page were then all served from cache (which sounds a lot like a static page, but not quite in the sense of Nextjs).

@cglacet
Copy link
Author

cglacet commented Dec 24, 2023

I think I'm still not getting it right, using export const dynamic = "force-dynamic"; does indeed render the page at build time, but the result is not kept in cache. The route needs to be requested once before its added in the cache. I'm still investigating a better solution for this.

@cglacet
Copy link
Author

cglacet commented Dec 24, 2023

For now, the only solution I found that actually render the route at build time and serves a cached version for the first incomming request is to add an optional locale argument to getI18n and getScopedI18n.

Saddly, export const dynamic = "force-dynamic"; might not be the answer.

@gabooh
Copy link

gabooh commented Mar 29, 2024

Another use case here, also inside an app router route : rendering several emails (with react-email), which don't have the same language. What I do for now, which works, but does not seem a proper way to do this :

for (const entry of entries) {
    cookies().set('Next-Locale', entry.language)
    const t = await getScopedI18n('scope')
    
    const html = await renderAsync(await Email())

    const options = {
      to: 'sender@email.com',
      subject: t('emailSubject'),
      html,
    }

    await sendEmail(options)
  }

Using setStaticParamsLocale doesn't work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants