forked from next-multilingual/next-multilingual
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
373 lines (343 loc) · 13 KB
/
Copy pathindex.ts
File metadata and controls
373 lines (343 loc) · 13 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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
import { cyanBright } from 'colorette';
import * as nextLog from 'next/dist/build/output/log';
import Cookies from 'nookies';
import { sep as pathSeparator } from 'path';
import { ParsedUrlQuery } from 'querystring';
import resolveAcceptLanguage from 'resolve-accept-language';
import type { ParsedUrlQueryInput } from 'node:querystring';
import type { GetServerSidePropsContext, PreviewData } from 'next';
/**
* Wrapper in front of Next.js' log to only show messages in non-production environments.
*
* To avoid exposing sensitive data (e.g., server paths) to the clients, we only display logs in non-production environments.
*/
export class log {
/**
* Log a warning message in the console(s) to non-production environments.
*
* @param message - The warning message to log.
*/
static warn(message: string): void {
if (process.env.NEXT_PUBLIC_nextMultilingualWarnings && process.env.NODE_ENV !== 'production') {
nextLog.warn(message);
}
}
}
/**
* Highlight a segment of a log message.
*
* @param segment - A segment of a log message.
*
* @returns The highlighted segment of a log message.
*/
export function highlight(segment: string): string {
return cyanBright(segment);
}
/**
* Highlight a file path segment of a log message, normalized with the current file system path separator
*
* @param filePath - A file path segment of a log message.
*
* @returns The highlighted file path segment of a log message.
*/
export function highlightFilePath(filePath: string): string {
return highlight(pathSeparator !== '/' ? filePath.replace(/\//g, pathSeparator) : filePath);
}
/**
* Get the actual locale based on the current locale from Next.js.
*
* To get a dynamic locale resolution on `/` without redirection, we need to add a "multilingual" locale as the
* default locale so that we can identify when the homepage is requested without a locale. With this setup it
* also means that we can no longer easily know what is the current locale. This function is meant to return the
* actual current of locale by replacing the "multilingual" default locale by the actual default locale.
*
* @param locale - The current locale from Next.js.
* @param defaultLocale - The configured i18n default locale from Next.js.
* @param locales - The configured i18n locales from Next.js.
*
* @returns The list of actual locales.
*/
export function getActualLocale(
locale?: string,
defaultLocale?: string,
locales?: string[]
): string {
if (locale === undefined || defaultLocale === undefined || locales === undefined) {
throw Error('locales must be configured in Next.js');
}
const actualDefaultLocale = getActualDefaultLocale(locales, defaultLocale);
return locale === defaultLocale ? actualDefaultLocale : locale;
}
/**
* Get the actual locales based on the Next.js i18n locale configuration.
*
* To get a dynamic locale resolution on `/` without redirection, we need to add a "multilingual" locale as the
* default locale so that we can identify when the homepage is requested without a locale. With this setup it
* also means that we can no longer use `locales`. This function is meant to return the actual list of locale
* by removing the "multilingual" default locale.
*
* @param locales - The configured i18n locales from Next.js.
* @param defaultLocale - The configured i18n default locale from Next.js.
*
* @returns The list of actual locales.
*/
export function getActualLocales(locales?: string[], defaultLocale?: string): string[] {
if (locales === undefined || defaultLocale === undefined) {
throw Error('locales must be configured in Next.js');
}
return locales.filter((locale) => locale !== defaultLocale);
}
/**
* Get the actual default locale based on the Next.js i18n locale configuration.
*
* To get a dynamic locale resolution on `/` without redirection, we need to add a "multilingual" locale as the
* default locale so that we can identify when the homepage is requested without a locale. With this setup it
* also means that we can no longer use `defaultLocale`. This function is meant to return the actual default
* locale (excluding the "multilingual" default locale). By convention (and for simplicity), the first
* `actualLocales` will be used as the actual default locale.
*
* @param locales - The configured i18n locales from Next.js.
* @param defaultLocale - The configured i18n default locale from Next.js.
*
* @returns The actual default locale.
*/
export function getActualDefaultLocale(locales?: string[], defaultLocale?: string): string {
if (locales === undefined || defaultLocale === undefined) {
throw Error('locales must be configured in Next.js');
}
return getActualLocales(locales, defaultLocale)?.shift() as string;
}
/**
* Is a given string a locale identifier following the `language`-`country` format?
*
* @param locale - A locale identifier.
* @param checkNormalizedCase - Test is the provided locale follows the ISO 3166 case convention (language code lowercase, country code uppercase).
*
* @returns `true` if the string is a locale identifier following the `language`-`country`, otherwise `false`.
*/
export function isLocale(locale: string, checkNormalizedCase = false): boolean {
const regexp = new RegExp(/^[a-z]{2}-[A-Z]{2}$/, !checkNormalizedCase ? 'i' : '');
return regexp.test(locale);
}
/**
* Get a normalized locale identifier.
*
* `next-multilingual-alternate` only uses locale identifiers following the `language`-`country` format. Locale identifiers
* are case insensitive and can be lowercase, however it is recommended by ISO 3166 convention that language codes
* are lowercase and country codes are uppercase.
*
* @param locale - A locale identifier.
*
* @returns The normalized locale identifier following the ISO 3166 convention.
*/
export function normalizeLocale(locale: string): string {
if (!isLocale(locale)) {
return locale;
}
const [languageCode, countryCode] = locale.split('-');
return `${languageCode.toLowerCase()}-${countryCode.toUpperCase()}`;
}
/**
* Generic type when using `getServerSideProps` on `/` to do dynamic locale detection.
*/
export type ResolvedLocaleServerSideProps = {
/** The locale resolved by the server side detection. */
readonly resolvedLocale: string;
};
/**
* Resolve the preferred locale from an HTTP `Accept-Language` header.
*
* @param acceptLanguageHeader - The value of an HTTP request `Accept-Language` header.
* @param actualLocales - The list of actual locales used by `next-multilingual-alternate`.
* @param actualDefaultLocale - The actual default locale used by `next-multilingual-alternate`.
*
* @returns The preferred locale identifier.
*/
export function getPreferredLocale(
acceptLanguageHeader: string | undefined,
actualLocales: string[],
actualDefaultLocale: string
): string {
if (acceptLanguageHeader === undefined) {
return actualDefaultLocale;
}
return resolveAcceptLanguage(acceptLanguageHeader, actualLocales, actualDefaultLocale);
}
// The name of the cookie used to store the user locale, can be overwritten in an `.env` file.
const LOCALE_COOKIE_NAME = process.env.NEXT_PUBLIC_LOCALE_COOKIE_NAME
? process.env.NEXT_PUBLIC_LOCALE_COOKIE_NAME
: 'L';
// The lifetime of the cookie used to store the user locale, can be overwritten in an `.env` file.
const LOCALE_COOKIE_LIFETIME: number =
process.env.NEXT_PUBLIC_LOCALE_COOKIE_LIFETIME !== undefined
? +process.env.NEXT_PUBLIC_LOCALE_COOKIE_LIFETIME
: 60 * 60 * 24 * 365 * 10;
/**
* Save the current user's locale to the locale cookie.
*
* @param locale - A locale identifier.
*/
export function setCookieLocale(locale?: string): void {
if (locale === undefined) {
throw Error('locales must be configured in Next.js');
}
Cookies.set(null, LOCALE_COOKIE_NAME, locale, {
...((LOCALE_COOKIE_LIFETIME !== -1) ? { maxAge: LOCALE_COOKIE_LIFETIME } : null),
path: '/',
sameSite: 'lax',
});
}
/**
* Get the locale that was saved to the locale cookie.
*
* @param serverSidePropsContext - The Next.js server side properties context.
* @param actualLocales - The list of actual locales used by `next-multilingual-alternate`.
*
* @returns The locale that was saved to the locale cookie.
*/
export function getCookieLocale(
serverSidePropsContext: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>,
actualLocales: string[]
): string | undefined {
const cookies = Cookies.get(serverSidePropsContext);
if (!Object.keys(cookies).includes(LOCALE_COOKIE_NAME)) {
return undefined;
}
const cookieLocale = cookies[LOCALE_COOKIE_NAME];
if (!actualLocales.includes(cookieLocale)) {
// Delete the cookie if the value is invalid (e.g., been tampered with).
Cookies.destroy(serverSidePropsContext, LOCALE_COOKIE_NAME);
return undefined;
}
return cookieLocale;
}
/**
* Hydrate a path back with its query values.
*
* Missing query parameters will show warning messages and will be kept in their original format.
*
* @see https://nextjs.org/docs/routing/dynamic-routes
*
* @param path - A path containing "query parameters".
* @param parsedUrlQueryInput - A `ParsedUrlQueryInput` object containing router queries.
* @param suppressWarning - If set to true, will not display a warning message if the key is missing.
*
* @returns The hydrated path containing `query` values instead of placeholders.
*/
export function hydrateQueryParameters(
path: string,
parsedUrlQueryInput: ParsedUrlQueryInput,
suppressWarning = false
): string {
const pathSegments = path.split('/');
const missingParameters: string[] = [];
const hydratedPath = pathSegments
.map((pathSegment) => {
if (/^\[.+\]$/.test(pathSegment)) {
const parameterName = pathSegment.slice(1, -1);
if (parsedUrlQueryInput[parameterName] !== undefined) {
return parsedUrlQueryInput[parameterName];
} else {
missingParameters.push(parameterName);
}
}
return pathSegment;
})
.join('/');
if (missingParameters.length && !suppressWarning) {
log.warn(
`unable to hydrate the path ${highlight(path)} because the following query parameter${
missingParameters.length > 1 ? 's are' : ' is'
} missing: ${highlight(missingParameters.join(','))}.`
);
}
return hydratedPath;
}
/**
* Convert a path using "query parameters" to "rewrite parameters".
*
* Next.js' router uses the bracket format (e.g., `/[example]`) to identify dynamic routes, called "query parameters". The
* rewrite statements use the colon format (e.g., `/:example`), called "rewrite parameters".
*
* @see https://nextjs.org/docs/routing/dynamic-routes
* @see https://nextjs.org/docs/api-reference/next.config.js/rewrites
*
* @param path - A path containing "query parameters".
*
* @returns The path converted to the "rewrite parameters" format.
*/
export function queryToRewriteParameters(path: string): string {
return path
.split('/')
.map((pathSegment) => {
if (/^\[.+\]$/.test(pathSegment)) {
return `:${pathSegment.slice(1, -1)}`;
}
return pathSegment;
})
.join('/');
}
/**
* Convert a path using "rewrite parameters" to "query parameters".
*
* Next.js' router uses the bracket format (e.g., `/[example]`) to identify dynamic routes, called "query parameters". The
* rewrite statements use the colon format (e.g., `/:example`), called "rewrite parameters".
*
* @see https://nextjs.org/docs/routing/dynamic-routes
* @see https://nextjs.org/docs/api-reference/next.config.js/rewrites
*
* @param path - A path containing "rewrite parameters".
*
* @returns The path converted to the "router queries" format.
*/
export function rewriteToQueryParameters(path: string): string {
return path
.split('/')
.map((pathSegment) => {
if (pathSegment.startsWith(':')) {
return `[${pathSegment.slice(1)}]`;
}
return pathSegment;
})
.join('/');
}
/**
* Does a given path contain "query parameters" (using the bracket syntax)?
*
* @param path - A path containing "query parameters".
*
* @returns True if the path contains "query parameters", otherwise false.
*/
export function containsQueryParameters(path: string): boolean {
return path.split('/').find((pathSegment) => /^\[.+\]$/.test(pathSegment)) === undefined
? false
: true;
}
/**
* Get "query parameters" (using the bracket syntax) from a path.
*
* @param path - A path containing "query parameters".
*
* @returns An array of "query parameters" or an empty array when not found.
*/
export function getQueryParameters(path: string): string[] {
const parameters = path.split('/').filter((pathSegment) => /^\[.+\]$/.test(pathSegment));
if (parameters === undefined) {
return [];
}
return parameters.map((parameter) => parameter.slice(1, -1));
}
/**
* Strips the base path from a URL if present.
*
* @param url - The URL from which to strip the base path.
* @param basePath - The base path to strip.
*
* @returns The URL without the base path if present.
*/
export function stripBasePath(url: string, basePath: string): string {
if (url.startsWith(basePath)) {
return url.replace(basePath, '');
}
return url;
}