Fix: correct <html lang> and localize the <head> description per locale

The root (main)/layout renders <html> ABOVE the [locale] segment, so next-intl's
getLocale() returns the default locale there - emitting <html lang="en"> on every
page regardless of UI language (e.g. /de) and a hardcoded English <head>
description. Both are strong "translate this page" triggers in Chrome.

proxy.ts already exposes the nonce to server components via the
x-middleware-request-* mechanism; expose the request pathname the same way as
x-pathname, and have the root layout derive the locale from it (falling back to
getLocale() when the path has no locale segment) for both <html lang> and the
localized meta_description (new key in all 17 locales; the English value is
unchanged).
This commit is contained in:
dealerweb
2026-05-30 13:51:21 +02:00
parent 7341e47a1b
commit 241544cd08
19 changed files with 47 additions and 7 deletions
+22 -3
View File
@@ -1,13 +1,26 @@
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { headers } from "next/headers";
import { getLocale } from "next-intl/server";
import { getLocale, getTranslations } from "next-intl/server";
import { PWAInstallPrompt } from "@/components/pwa-install-prompt";
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
import { configManager } from "@/lib/admin/config-manager";
import { withBasePath } from "@/lib/browser-navigation";
import { locales } from "@/i18n/routing";
import "../globals.css";
// This layout renders <html> and sits ABOVE the [locale] segment, so
// next-intl's getLocale() returns the default locale here - emitting
// <html lang="en"> on e.g. /de pages, which makes browsers offer to
// "translate this page". Recover the active locale from the request pathname
// (exposed by proxy.ts as x-pathname), falling back to getLocale() (cookie /
// Accept-Language) when the path carries no locale segment.
async function resolveRequestLocale(): Promise<string> {
const pathname = (await headers()).get("x-pathname") || "";
const seg = pathname.split("/").find((s) => (locales as readonly string[]).includes(s));
return seg ?? (await getLocale());
}
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
@@ -27,10 +40,16 @@ export const viewport: Viewport = {
export async function generateMetadata(): Promise<Metadata> {
await configManager.ensureLoaded();
const faviconUrl = configManager.get<string>("faviconUrl", "/branding/Bulwark_Favicon.svg");
// Localize the <head> description to match the UI language; a hardcoded
// English description is another signal that makes Chrome offer to
// "translate this page". Resolve the locale from the request path, since this
// layout is above the [locale] segment (see resolveRequestLocale).
const locale = await resolveRequestLocale();
const t = await getTranslations({ locale });
return {
title: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || "Webmail",
description: "Minimalist webmail client using JMAP protocol",
description: t("meta_description"),
appleWebApp: {
capable: true,
statusBarStyle: "black-translucent",
@@ -48,7 +67,7 @@ export default async function RootLayout({
}: {
children: React.ReactNode;
}) {
const locale = await getLocale();
const locale = await resolveRequestLocale();
const nonce = (await headers()).get("x-nonce") ?? "";
const parentOrigin = process.env.NEXT_PUBLIC_PARENT_ORIGIN || "";
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Minimalistický webmailový klient využívající protokol JMAP",
"login": {
"title": "Webmail",
"username_label": "E-mail",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Minimalistisk webmail-klient, der bruger JMAP-protokollen",
"login": {
"title": "Webmail",
"username_label": "Email",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Minimalistischer Webmail-Client mit JMAP-Protokoll",
"login": {
"title": "Webmail",
"username_label": "E-Mail",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Minimalist webmail client using JMAP protocol",
"login": {
"title": "Webmail",
"username_label": "Email",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Cliente de correo web minimalista que usa el protocolo JMAP",
"login": {
"title": "Correo Web",
"username_label": "Correo electrónico",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Client de messagerie web minimaliste utilisant le protocole JMAP",
"login": {
"title": "Webmail",
"username_label": "Email",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Client di posta web minimalista che utilizza il protocollo JMAP",
"login": {
"title": "Webmail",
"username_label": "Email",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "JMAPプロトコルを使用するミニマルなウェブメールクライアント",
"login": {
"title": "ウェブメール",
"username_label": "メールアドレス",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "JMAP 프로토콜을 사용하는 미니멀한 웹메일 클라이언트",
"login": {
"title": "웹메일",
"username_label": "이메일",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Minimālistisks tīmekļa pasta klients, kas izmanto JMAP protokolu",
"login": {
"title": "Tīmekļa pasts",
"username_label": "E-pasta adrese",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Minimalistische webmailclient die het JMAP-protocol gebruikt",
"login": {
"title": "Webmail",
"username_label": "E-mail",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Minimalistyczny klient poczty internetowej wykorzystujący protokół JMAP",
"login": {
"title": "Webmail",
"username_label": "E-mail",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Cliente de webmail minimalista que usa o protocolo JMAP",
"login": {
"title": "Webmail",
"username_label": "E-mail",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Минималистичный веб-клиент электронной почты, использующий протокол JMAP",
"login": {
"title": "Веб-почта",
"username_label": "Электронная почта",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "JMAP protokolünü kullanan minimalist web posta istemcisi",
"login": {
"title": "Webmail",
"username_label": "E-posta",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "Мінімалістичний вебклієнт електронної пошти, що використовує протокол JMAP",
"login": {
"title": "Веб-пошта",
"username_label": "Електронна пошта",
+1
View File
@@ -1,4 +1,5 @@
{
"meta_description": "使用 JMAP 协议的极简网页邮件客户端",
"login": {
"title": "网页邮箱",
"username_label": "邮箱地址",
+8 -4
View File
@@ -143,11 +143,15 @@ export async function proxy(request: NextRequest) {
const response = intlResponse ?? NextResponse.next();
const existing = response.headers.get("x-middleware-override-headers");
response.headers.set(
"x-middleware-override-headers",
existing ? `${existing},x-nonce` : "x-nonce"
);
// Expose the nonce AND the request pathname to server components as request
// headers. The root (main)/layout renders <html> ABOVE the [locale] segment,
// so getLocale() can't resolve the active locale there and falls back to the
// default - emitting <html lang="en"> on e.g. /de pages, which makes browsers
// offer to "translate this page". The layout reads x-pathname to recover it.
const overrides = [existing, "x-nonce", "x-pathname"].filter(Boolean).join(",");
response.headers.set("x-middleware-override-headers", overrides);
response.headers.set("x-middleware-request-x-nonce", nonce);
response.headers.set("x-middleware-request-x-pathname", pathname);
response.headers.set("X-Content-Type-Options", "nosniff");