diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index de23551fc14..5cd502f1d3a 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -10,6 +10,7 @@ import Matomo from "@/components/Matomo" import { getLastDeployDate } from "@/lib/utils/getLastDeployDate" import { getLocaleTimestamp } from "@/lib/utils/time" +import { toLanguageTag } from "@/lib/utils/url" import Providers from "./providers" @@ -66,7 +67,7 @@ export default async function LocaleLayout(props: { return ( diff --git a/app/sitemap.ts b/app/sitemap.ts index 614a0b25cc8..94fcfcfe171 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,6 +1,6 @@ import type { MetadataRoute } from "next" -import { getFullUrl } from "@/lib/utils/url" +import { getFullUrl, toLanguageTag } from "@/lib/utils/url" import { DEFAULT_LOCALE } from "@/lib/constants" @@ -21,7 +21,7 @@ export default async function sitemap(): Promise { "x-default": getFullUrl(DEFAULT_LOCALE, normalizedSlug), ...Object.fromEntries( translatedLocales.map((locale) => [ - locale, + toLanguageTag(locale), getFullUrl(locale, normalizedSlug), ]) ), diff --git a/src/lib/utils/metadata.ts b/src/lib/utils/metadata.ts index cb73858b508..ab86c9b91cb 100644 --- a/src/lib/utils/metadata.ts +++ b/src/lib/utils/metadata.ts @@ -9,7 +9,7 @@ import { import { getTranslatedLocales } from "../i18n/translationRegistry" -import { getFullUrl } from "./url" +import { getFullUrl, toLanguageTag } from "./url" import { routing } from "@/i18n/routing" @@ -103,7 +103,10 @@ export const getMetadata = async ({ languages: { "x-default": xDefault, ...Object.fromEntries( - localesForHreflang.map((loc) => [loc, getFullUrl(loc, slugString)]) + localesForHreflang.map((loc) => [ + toLanguageTag(loc), + getFullUrl(loc, slugString), + ]) ), }, }), diff --git a/src/lib/utils/url.ts b/src/lib/utils/url.ts index 2b29271c188..165905ed69d 100644 --- a/src/lib/utils/url.ts +++ b/src/lib/utils/url.ts @@ -115,6 +115,18 @@ export const slugify = (text: string): string => { ) } +/** + * Converts an internal locale code to a valid BCP 47 language tag. + * Internal codes use lowercase region subtags (e.g. "pt-br", "zh-tw") + * for use as URL path segments, but HTML lang and hreflang attributes + * require uppercase region subtags per BCP 47 (e.g. "pt-BR", "zh-TW"). + */ +export const toLanguageTag = (locale: string): string => { + const parts = locale.split("-") + if (parts.length === 2) return `${parts[0]}-${parts[1].toUpperCase()}` + return locale +} + export const normalizeUrlForJsonLd = ( locale: string | Lang | undefined, pathWithoutLocale: string