Support to set <html lang /> from metadata? #49415
Replies: 24 comments 47 replies
-
I would also like to add my support for this feature, and I would also like to request other export async function generateMetadata() {
return {
htmlAttributes: {
lang: "ar-AE",
dir: "rtl",
},
title: "Ahoj",
}
} Here's a summary of the currently available options/workaround we have, to demonstrate why I think we need framework-level support for this. Pattern 1. -
|
Beta Was this translation helpful? Give feedback.
-
Pattern 2. -
|
Beta Was this translation helpful? Give feedback.
-
I think layouts do refresh (via RSC) when their parameters change. The difference is keeping internal state of the component by remounting the component with changing Problem is supporting default I like idea of the dedicated api |
Beta Was this translation helpful? Give feedback.
-
This is a demo what I think could be achieved without middleware. https://middleware-cache-three.vercel.app |
Beta Was this translation helpful? Give feedback.
-
Also looking for a solution for this. As far as I can see, there's now no way to set the lang attribute serverside AND keep a certain component mounted while navigating between locales (e.g. have the |
Beta Was this translation helpful? Give feedback.
-
I have this same issue and find it insane that nextjs doesn't have an answer for this 🤔 |
Beta Was this translation helpful? Give feedback.
-
Same issue here. I need change html attr in children layout component |
Beta Was this translation helpful? Give feedback.
-
It is crazy that there is still not a possible workaround for such an essential issue. |
Beta Was this translation helpful? Give feedback.
-
For the past couple of months the workaround i managed to come up with is to change the layout to this: export default function RootLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>
} and then on page.tsx: return (
<html lang={data.lang ?? "en"}>
<head>
<title>{data.seo.title}</title>
...
</head>
<body>
...
</body>
</html>
) I don't use |
Beta Was this translation helpful? Give feedback.
-
There's a dirty workaround using a global state variable. For example, in export const state = { locale: "en" }; …then you update the variable in import { state } from "@/config";
export default function Page({ params: { locale } }) {
state.locale = locale;
return (
<div>
...
</div>
);
} …and use it in the root layout import { state } from "@/config";
export default function RootLayout({ children }) {
return (
<html lang={state.locale}>
<body>{children}</body>
</html>
);
} This, of course, is a terrible pattern in general, but it works. At the end of the day, the staticly generated HTML has the correct You can find an explanation of this approach in this article and you can also play around with a working version in this repo. |
Beta Was this translation helpful? Give feedback.
-
We are facing the same issue: The content is loaded through a CMS, and the language of the content cannot be determined either by the domain or necessarily by the path. Of course, it’s clear that all content under /en/* is in English, but there are also short URLs where the language is unknown and can/must only be set after fetching the content. How can this issue be resolved? |
Beta Was this translation helpful? Give feedback.
-
You can try this way:
Use it in
|
Beta Was this translation helpful? Give feedback.
-
As an alternative, you can group your routes let me show you by app router hierarchy:
and it is working for me, of course I hope next.js support it by maybe metadata/generateMetadata or built-in components, but for now, It can work for us, I hope so :) |
Beta Was this translation helpful? Give feedback.
-
I've found a workaround for this by using async layout and page, promises, and server-only-context (which uses React
import serverContext from "server-only-context";
export const [getLangResolve, setLangResolve] = serverContext<
(lang: string) => void
>(() => {});
import { setLangResolve } from "./contexts";
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const lang = await new Promise<string>((resolve) => {
setLangResolve(resolve);
setTimeout(() => resolve("en"), 100); // fallback, just in case
});
return (
<html lang={lang}>
<body>{children}</body>
</html>
);
}
import { getLangResolve } from "../contexts";
export default async function Home(props: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await props.params;
getLangResolve()(lang);
return <h1 className="text-6xl">lang {lang}</h1>;
} This basically does the following:
Generally, you can use this approach to pass whatever you want from the page up to the layout. I tested it for race conditions and it's doing great. It has also been on production for a few days and I haven't seen any issues there as well. Check a demo of it here: https://github.com/hdodov/test-nextjs/tree/demo-dynamic-lang @leerob although this workaround seems to do the job, it'd be much better if Next offers a native solution that doesn't require such despicable promise gymnastics. |
Beta Was this translation helpful? Give feedback.
-
Hi, I solved it this way but I am not fully certain about the approach:
Do you see any bad drawback by removing the RootLayout? |
Beta Was this translation helpful? Give feedback.
-
Hi, I have solved this in quite an easy way. It feels a bit hacky accessing
|
Beta Was this translation helpful? Give feedback.
-
For those of you, like me, don't mind using middleware: it seems Next.js official documentation on internationalization suggests an automatic redirect by the middleware:
They also mention:
This is analogous to @leo-russi 's solution. Having said that, it feels quite brittle to remove the root layout. I'd definitely prefer a much easier way to update the |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Here is my solution: Store the
|
Beta Was this translation helpful? Give feedback.
-
Not sure if I missed something from the initial requirement, but I have been using following pattern for a while w/o any trouble...
Pass-through root layout in export default function RootLayout(props: { children: React.ReactNode }) {
return props.children;
} Having this kind of root layout makes the 404 page work. Hope this helps, and kindly let me know if I missed something. --edit : my |
Beta Was this translation helpful? Give feedback.
-
Followed this approach: https://www.meje.dev/blog/html-lang-attribute-in-nextjs Someone enlighten me if this is terrible.
|
Beta Was this translation helpful? Give feedback.
-
It's almost April 2025, and this question that should have been addressed earlier still doesn't have an answer :( |
Beta Was this translation helpful? Give feedback.
-
Not without compromise, no. The issue and subsequent clarifying comments here have been very deliberately worded to demonstrate that this issue must be fixed upstream, because otherwise you subvert the concept of persisted shared layouts entirely. This needs an API for the exact same reason that Metadata needed its own API (because metadata exists so high in the DOM hierarchy that to change it between individual routes would invalidate layouts). |
Beta Was this translation helpful? Give feedback.
-
Some other alternative, since we have yet to find the ideal one... This one needs to follow the export async function generateStaticParams() {
return [{ lang: "en-US" }, { lang: "pt-BR" }];
}
interface DashboardLayoutProps {
children: React.ReactNode;
params: Promise<{ lang: Locale }>;
}
export default async function DashboardLayout({
params,
children,
}: DashboardLayoutProps) {
const { lang } = await params;
... Result of running Route (app) Size First Load JS
┌ ○ /_not-found 979 B 106 kB
├ ● /[lang]/dashboard 3.77 kB 149 kB
├ ├ /en-US/dashboard
├ └ /pt-BR/dashboard
├ ● /[lang]/forgot-password 2.92 kB 187 kB
├ ├ /en-US/forgot-password
├ └ /pt-BR/forgot-password
├ ● /[lang]/onboarding 1.1 kB 319 kB
├ ├ /en-US/onboarding
├ └ /pt-BR/onboarding
├ ● /[lang]/pricing 2.28 kB 114 kB
├ ├ /en-US/pricing
├ └ /pt-BR/pricing
├ ● /[lang]/referrals 9.05 kB 221 kB
├ ├ /en-US/referrals
├ └ /pt-BR/referrals
├ ● /[lang]/reset-password 4 kB 188 kB
├ ├ /en-US/reset-password
├ └ /pt-BR/reset-password
├ ● /[lang]/services 7.86 kB 186 kB
├ ├ /en-US/services
├ └ /pt-BR/services
├ ● /[lang]/signin 4.92 kB 191 kB
├ ├ /en-US/signin
├ └ /pt-BR/signin
├ ● /[lang]/signup 3.23 kB 319 kB
├ ├ /en-US/signup
├ └ /pt-BR/signup
├ ● /[lang]/verify 6.89 kB 188 kB
├ ├ /en-US/verify
├ └ /pt-BR/verify
├ ƒ /api/[...nextauth] 144 B 105 kB
├ ƒ /api/stripe/checkout 144 B 105 kB
├ ƒ /api/stripe/webhook 144 B 105 kB
└ ƒ /api/uploadthing 144 B 105 kB
+ First Load JS shared by all 105 kB
├ chunks/4bd1b696-a9b262982930989a.js 52.9 kB
├ chunks/517-f39a7a794abbf6cb.js 50.5 kB
└ other shared chunks (total) 1.92 kB
ƒ Middleware 110 kB
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
ƒ (Dynamic) server-rendered on demand edit: grammar |
Beta Was this translation helpful? Give feedback.
-
As far as I know you can only set
<html lang={lang} />
in root layout. It can be dynamic from[lang]
route, which is problematic when you want to have a default locale.In my opinion
lang
should be configurable same as other metadata as layout cannot always know before route loads data.This should result in something like
Do you plan tu support this? Or maybe is there a workaround similar to
useServerInsertedHTML
.I would like to avoid unnecessary usage of
middleware.ts
.Beta Was this translation helpful? Give feedback.
All reactions