-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathproxy.ts
More file actions
149 lines (128 loc) · 4.4 KB
/
proxy.ts
File metadata and controls
149 lines (128 loc) · 4.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { trackUnsupportedLanguage } from "./lib/actions/analytics";
import { defaultLocale, locales } from "./lib/locales";
// Helper function to validate if a string is a valid BCP 47 locale
function isValidLocale(locale: string): boolean {
try {
// Skip quality values, wildcards, and other non-locale strings
if (locale === "*" || locale.startsWith("q=") || locale.includes(";")) {
return false;
}
// Basic BCP 47 format validation: language[-script][-region]
// Language: 2-3 letters (ISO 639-1 or ISO 639-2)
// Script: 4 letters (ISO 15924)
// Region: 2-3 letters (ISO 3166-1 or UN M.49)
const localeRegex = /^[a-z]{2,3}(-[A-Z]{4})?(-[A-Z]{2,3})?$/;
return localeRegex.test(locale);
} catch {
return false;
}
}
function getLocale(request: NextRequest): string {
// Check for stored preference in cookie
const preferredLanguage = request.cookies.get("preferredLanguage")?.value;
if (
preferredLanguage &&
(locales as readonly string[]).includes(preferredLanguage)
) {
return preferredLanguage;
}
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
try {
const languages = new Negotiator({
headers: negotiatorHeaders,
}).languages();
// Log all languages for debugging (only in development)
if (process.env.NODE_ENV === "development") {
console.log("All languages from negotiator:", languages);
}
// Filter out invalid locales and ensure we have valid ones
const validLanguages = languages.filter((lang) => {
try {
// Use our robust locale validation
const isValid =
lang &&
typeof lang === "string" &&
lang.length > 0 &&
isValidLocale(lang);
return isValid;
} catch {
return false;
}
});
// If no valid languages, return default
if (!validLanguages.length) {
return defaultLocale;
}
const matchedLocale = match(
validLanguages,
locales as readonly string[],
defaultLocale,
);
// Track unsupported language requests
if (matchedLocale === defaultLocale && validLanguages.length > 0) {
// Get the most preferred unsupported language
const mostPreferredUnsupported = validLanguages.find(
(lang) => !(locales as readonly string[]).includes(lang),
);
if (mostPreferredUnsupported) {
// Normalize to primary language code (first two characters)
const primaryLanguage = mostPreferredUnsupported
.substring(0, 2)
.toLowerCase();
// Only track if the primary language isn't supported
if (!(locales as readonly string[]).includes(primaryLanguage)) {
// Track the request asynchronously (don't block the redirect)
trackUnsupportedLanguage(primaryLanguage);
}
}
}
return matchedLocale;
} catch (error) {
console.error("Error in locale matching:", error);
// Track that an error occurred during locale matching
trackUnsupportedLanguage("error");
return defaultLocale;
}
}
export function proxy(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Skip if the request is for an asset or API route
if (
pathname.startsWith("/_next") ||
pathname.startsWith("/api/") ||
pathname.includes(".")
) {
return;
}
// Check if the pathname is missing a locale
const pathnameIsMissingLocale = (locales as readonly string[]).every(
(locale) =>
!pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
);
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
// Redirect root path directly to root [lang] page
const targetPath =
pathname === "/" ? `/${locale}/` : `/${locale}${pathname}`;
const newUrl = new URL(targetPath, request.url);
// Preserve all query parameters
request.nextUrl.searchParams.forEach((value, key) => {
newUrl.searchParams.set(key, value);
});
return NextResponse.redirect(newUrl);
}
}
export const config = {
matcher: [
// Skip all internal paths (_next)
"/((?!_next|api|.*\\.).*)",
// Optional: Match all root level pages
"/",
],
};