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:
+22
-3
@@ -1,13 +1,26 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { headers } from "next/headers";
|
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 { PWAInstallPrompt } from "@/components/pwa-install-prompt";
|
||||||
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
|
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
|
||||||
import { configManager } from "@/lib/admin/config-manager";
|
import { configManager } from "@/lib/admin/config-manager";
|
||||||
import { withBasePath } from "@/lib/browser-navigation";
|
import { withBasePath } from "@/lib/browser-navigation";
|
||||||
|
import { locales } from "@/i18n/routing";
|
||||||
import "../globals.css";
|
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({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -27,10 +40,16 @@ export const viewport: Viewport = {
|
|||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
await configManager.ensureLoaded();
|
await configManager.ensureLoaded();
|
||||||
const faviconUrl = configManager.get<string>("faviconUrl", "/branding/Bulwark_Favicon.svg");
|
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 {
|
return {
|
||||||
title: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || "Webmail",
|
title: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || "Webmail",
|
||||||
description: "Minimalist webmail client using JMAP protocol",
|
description: t("meta_description"),
|
||||||
appleWebApp: {
|
appleWebApp: {
|
||||||
capable: true,
|
capable: true,
|
||||||
statusBarStyle: "black-translucent",
|
statusBarStyle: "black-translucent",
|
||||||
@@ -48,7 +67,7 @@ export default async function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const locale = await getLocale();
|
const locale = await resolveRequestLocale();
|
||||||
const nonce = (await headers()).get("x-nonce") ?? "";
|
const nonce = (await headers()).get("x-nonce") ?? "";
|
||||||
const parentOrigin = process.env.NEXT_PUBLIC_PARENT_ORIGIN || "";
|
const parentOrigin = process.env.NEXT_PUBLIC_PARENT_ORIGIN || "";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Minimalistický webmailový klient využívající protokol JMAP",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "E-mail",
|
"username_label": "E-mail",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Minimalistisk webmail-klient, der bruger JMAP-protokollen",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "Email",
|
"username_label": "Email",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Minimalistischer Webmail-Client mit JMAP-Protokoll",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "E-Mail",
|
"username_label": "E-Mail",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Minimalist webmail client using JMAP protocol",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "Email",
|
"username_label": "Email",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Cliente de correo web minimalista que usa el protocolo JMAP",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Correo Web",
|
"title": "Correo Web",
|
||||||
"username_label": "Correo electrónico",
|
"username_label": "Correo electrónico",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Client de messagerie web minimaliste utilisant le protocole JMAP",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "Email",
|
"username_label": "Email",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Client di posta web minimalista che utilizza il protocollo JMAP",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "Email",
|
"username_label": "Email",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "JMAPプロトコルを使用するミニマルなウェブメールクライアント",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "ウェブメール",
|
"title": "ウェブメール",
|
||||||
"username_label": "メールアドレス",
|
"username_label": "メールアドレス",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "JMAP 프로토콜을 사용하는 미니멀한 웹메일 클라이언트",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "웹메일",
|
"title": "웹메일",
|
||||||
"username_label": "이메일",
|
"username_label": "이메일",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Minimālistisks tīmekļa pasta klients, kas izmanto JMAP protokolu",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Tīmekļa pasts",
|
"title": "Tīmekļa pasts",
|
||||||
"username_label": "E-pasta adrese",
|
"username_label": "E-pasta adrese",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Minimalistische webmailclient die het JMAP-protocol gebruikt",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "E-mail",
|
"username_label": "E-mail",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Minimalistyczny klient poczty internetowej wykorzystujący protokół JMAP",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "E-mail",
|
"username_label": "E-mail",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Cliente de webmail minimalista que usa o protocolo JMAP",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "E-mail",
|
"username_label": "E-mail",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Минималистичный веб-клиент электронной почты, использующий протокол JMAP",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Веб-почта",
|
"title": "Веб-почта",
|
||||||
"username_label": "Электронная почта",
|
"username_label": "Электронная почта",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "JMAP protokolünü kullanan minimalist web posta istemcisi",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Webmail",
|
"title": "Webmail",
|
||||||
"username_label": "E-posta",
|
"username_label": "E-posta",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "Мінімалістичний вебклієнт електронної пошти, що використовує протокол JMAP",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Веб-пошта",
|
"title": "Веб-пошта",
|
||||||
"username_label": "Електронна пошта",
|
"username_label": "Електронна пошта",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"meta_description": "使用 JMAP 协议的极简网页邮件客户端",
|
||||||
"login": {
|
"login": {
|
||||||
"title": "网页邮箱",
|
"title": "网页邮箱",
|
||||||
"username_label": "邮箱地址",
|
"username_label": "邮箱地址",
|
||||||
|
|||||||
@@ -143,11 +143,15 @@ export async function proxy(request: NextRequest) {
|
|||||||
const response = intlResponse ?? NextResponse.next();
|
const response = intlResponse ?? NextResponse.next();
|
||||||
|
|
||||||
const existing = response.headers.get("x-middleware-override-headers");
|
const existing = response.headers.get("x-middleware-override-headers");
|
||||||
response.headers.set(
|
// Expose the nonce AND the request pathname to server components as request
|
||||||
"x-middleware-override-headers",
|
// headers. The root (main)/layout renders <html> ABOVE the [locale] segment,
|
||||||
existing ? `${existing},x-nonce` : "x-nonce"
|
// 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-nonce", nonce);
|
||||||
|
response.headers.set("x-middleware-request-x-pathname", pathname);
|
||||||
|
|
||||||
response.headers.set("X-Content-Type-Options", "nosniff");
|
response.headers.set("X-Content-Type-Options", "nosniff");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user