Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/vinext/src/shims/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ function applyLocaleToHref(href: string, locale: string | false | undefined): st
return href;
}

// Absolute and protocol-relative URLs must not be prefixed — locale
// only applies to local paths.
if (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//")) {
return href;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix looks correct and is well-placed — right after the undefined/false early returns and before the locale-prefixing logic.

Note: the sibling function applyNavigationLocale() in router.ts:125-132 has the same bug. It's used by Router.push() and Router.replace() when a locale option is passed. Consider adding the same guard there (and a corresponding test) — either in this PR or as an immediate follow-up:

Suggested change
return href;
// Absolute and protocol-relative URLs must not be prefixed — locale
// only applies to local paths.
if (/^https?:\/\//.test(href) || href.startsWith("//")) {
return href;
}

(The startsWith version you have is perfectly fine too — just showing the regex alternative for brevity. No change needed here.)

}

// locale is a string: prepend the locale prefix if not already present
const defaultLocale = getDefaultLocale();
// For the default locale, Next.js doesn't add a prefix
Expand Down
5 changes: 5 additions & 0 deletions packages/vinext/src/shims/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ function resolveUrl(url: string | UrlObject): string {
*/
export function applyNavigationLocale(url: string, locale?: string): string {
if (!locale || typeof window === "undefined") return url;
// Absolute and protocol-relative URLs must not be prefixed — locale
// only applies to local paths.
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) {
return url;
}
const defaultLocale = window.__VINEXT_DEFAULT_LOCALE__;
// Default locale doesn't get a prefix
if (locale === defaultLocale) return url;
Expand Down
24 changes: 24 additions & 0 deletions tests/link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,30 @@ describe("Link locale handling", () => {
// Should not become /fr/fr/about
expect(html).toContain('href="/fr/about"');
});

it("locale does not mangle absolute same-origin URLs", () => {
// An absolute URL like https://example.com/about should not become
// /fr/https://example.com/about — locale prefix only applies to paths
const html = ReactDOMServer.renderToString(
React.createElement(Link, { href: "https://example.com/about", locale: "fr" } as any, "x"),
);
expect(html).toContain('href="https://example.com/about"');
});

it("locale does not mangle protocol-relative URLs", () => {
// //example.com/about should not become /fr///example.com/about
const html = ReactDOMServer.renderToString(
React.createElement(Link, { href: "//example.com/about", locale: "fr" } as any, "x"),
);
expect(html).toContain('href="//example.com/about"');
});

it("locale does not mangle http:// URLs", () => {
const html = ReactDOMServer.renderToString(
React.createElement(Link, { href: "http://example.com/path", locale: "de" } as any, "x"),
);
expect(html).toContain('href="http://example.com/path"');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good coverage — three distinct URL patterns (https, protocol-relative, http) with clear comments explaining the expected behavior.

Minor: might be worth adding a test for mailto: or other non-http schemes to document whether those are handled (they'd currently get locale-prefixed, but that's a separate issue).

});

// ─── toSameOriginPath ────────────────────────────────────────────────────
Expand Down
49 changes: 49 additions & 0 deletions tests/shims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6795,6 +6795,55 @@ describe("Pages Router router helpers", () => {
expect(isHashOnlyChange("https://example.com#foo")).toBe(false);
});
});

describe("applyNavigationLocale", () => {
it("does not prefix absolute https:// URLs", async () => {
const { applyNavigationLocale } = await import("../packages/vinext/src/shims/router.js");
// Simulate a browser-like window so the locale guard is reached
(globalThis as any).window = { __VINEXT_DEFAULT_LOCALE__: "en" };
try {
expect(applyNavigationLocale("https://example.com/about", "fr")).toBe(
"https://example.com/about",
);
} finally {
delete (globalThis as any).window;
}
});

it("does not prefix absolute http:// URLs", async () => {
const { applyNavigationLocale } = await import("../packages/vinext/src/shims/router.js");
(globalThis as any).window = { __VINEXT_DEFAULT_LOCALE__: "en" };
try {
expect(applyNavigationLocale("http://example.com/path", "de")).toBe(
"http://example.com/path",
);
} finally {
delete (globalThis as any).window;
}
});

it("does not prefix protocol-relative // URLs", async () => {
const { applyNavigationLocale } = await import("../packages/vinext/src/shims/router.js");
(globalThis as any).window = { __VINEXT_DEFAULT_LOCALE__: "en" };
try {
expect(applyNavigationLocale("//cdn.example.com/img.png", "fr")).toBe(
"//cdn.example.com/img.png",
);
} finally {
delete (globalThis as any).window;
}
});

it("prefixes local paths with locale", async () => {
const { applyNavigationLocale } = await import("../packages/vinext/src/shims/router.js");
(globalThis as any).window = { __VINEXT_DEFAULT_LOCALE__: "en" };
try {
expect(applyNavigationLocale("/about", "fr")).toBe("/fr/about");
} finally {
delete (globalThis as any).window;
}
});
});
});

describe("next/server enhancements", () => {
Expand Down
Loading