@intlayer/docs 6.1.4 → 6.1.6-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blog/ar/next-i18next_vs_next-intl_vs_intlayer.md +1366 -75
- package/blog/ar/nextjs-multilingual-seo-comparison.md +364 -0
- package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +1288 -72
- package/blog/de/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/en/intlayer_with_next-i18next.mdx +431 -0
- package/blog/en/intlayer_with_next-intl.mdx +335 -0
- package/blog/en/next-i18next_vs_next-intl_vs_intlayer.md +583 -336
- package/blog/en/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +1144 -37
- package/blog/en-GB/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +1236 -64
- package/blog/es/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +1142 -75
- package/blog/fr/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/hi/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +1130 -55
- package/blog/it/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +1150 -76
- package/blog/ja/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +1139 -73
- package/blog/ko/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +1143 -76
- package/blog/pt/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +1150 -74
- package/blog/ru/nextjs-multilingual-seo-comparison.md +370 -0
- package/blog/tr/next-i18next_vs_next-intl_vs_intlayer.md +2 -0
- package/blog/tr/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +1152 -75
- package/blog/zh/nextjs-multilingual-seo-comparison.md +394 -0
- package/dist/cjs/generated/blog.entry.cjs +16 -0
- package/dist/cjs/generated/blog.entry.cjs.map +1 -1
- package/dist/cjs/generated/docs.entry.cjs +16 -0
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/esm/generated/blog.entry.mjs +16 -0
- package/dist/esm/generated/blog.entry.mjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs +16 -0
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/types/generated/blog.entry.d.ts +1 -0
- package/dist/types/generated/blog.entry.d.ts.map +1 -1
- package/dist/types/generated/docs.entry.d.ts +1 -0
- package/dist/types/generated/docs.entry.d.ts.map +1 -1
- package/docs/ar/component_i18n.md +186 -0
- package/docs/ar/vs_code_extension.md +48 -109
- package/docs/de/component_i18n.md +186 -0
- package/docs/de/vs_code_extension.md +46 -107
- package/docs/en/component_i18n.md +186 -0
- package/docs/en/interest_of_intlayer.md +2 -2
- package/docs/en/intlayer_with_nextjs_14.md +18 -1
- package/docs/en/intlayer_with_nextjs_15.md +18 -1
- package/docs/en/vs_code_extension.md +24 -114
- package/docs/en-GB/component_i18n.md +186 -0
- package/docs/en-GB/vs_code_extension.md +42 -103
- package/docs/es/component_i18n.md +182 -0
- package/docs/es/vs_code_extension.md +53 -114
- package/docs/fr/component_i18n.md +186 -0
- package/docs/fr/vs_code_extension.md +50 -111
- package/docs/hi/component_i18n.md +186 -0
- package/docs/hi/vs_code_extension.md +49 -110
- package/docs/it/component_i18n.md +186 -0
- package/docs/it/vs_code_extension.md +50 -111
- package/docs/ja/component_i18n.md +186 -0
- package/docs/ja/vs_code_extension.md +50 -111
- package/docs/ko/component_i18n.md +186 -0
- package/docs/ko/vs_code_extension.md +48 -109
- package/docs/pt/component_i18n.md +186 -0
- package/docs/pt/vs_code_extension.md +46 -107
- package/docs/ru/component_i18n.md +186 -0
- package/docs/ru/vs_code_extension.md +48 -109
- package/docs/tr/component_i18n.md +186 -0
- package/docs/tr/vs_code_extension.md +54 -115
- package/docs/zh/component_i18n.md +186 -0
- package/docs/zh/vs_code_extension.md +51 -105
- package/package.json +11 -11
- package/src/generated/blog.entry.ts +16 -0
- package/src/generated/docs.entry.ts +16 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
```bash
|
|
2
|
+
.
|
|
3
|
+
├── i18n.config.ts
|
|
4
|
+
└── src
|
|
5
|
+
├── locales
|
|
6
|
+
│ ├── en
|
|
7
|
+
│ │ ├── common.json
|
|
8
|
+
│ │ └── about.json
|
|
9
|
+
│ └── fr
|
|
10
|
+
│ ├── common.json
|
|
11
|
+
│ └── about.json
|
|
12
|
+
├── app
|
|
13
|
+
│ ├── i18n
|
|
14
|
+
│ │ └── server.ts
|
|
15
|
+
│ └── [locale]
|
|
16
|
+
│ ├── layout.tsx
|
|
17
|
+
│ └── about.tsx
|
|
18
|
+
└── components
|
|
19
|
+
├── I18nProvider.tsx
|
|
20
|
+
├── ClientComponent.tsx
|
|
21
|
+
└── ServerComponent.tsx
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ts fileName="i18n.config.ts"
|
|
25
|
+
export const locales = ['en', 'fr'] as const;
|
|
26
|
+
export type Locale = (typeof locales)[number];
|
|
27
|
+
|
|
28
|
+
export const defaultLocale: Locale = 'en';
|
|
29
|
+
|
|
30
|
+
export const rtlLocales = ['ar', 'he', 'fa', 'ur'] as const;
|
|
31
|
+
export const isRtl = (locale: string) =>
|
|
32
|
+
(rtlLocales as readonly string[]).includes(locale);
|
|
33
|
+
|
|
34
|
+
export function localizedPath(locale: string, path: string) {
|
|
35
|
+
return locale === defaultLocale ? path : '/' + locale + path;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ORIGIN = 'https://example.com';
|
|
39
|
+
export function abs(locale: string, path: string) {
|
|
40
|
+
return ORIGIN + localizedPath(locale, path);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ts fileName="src/app/i18n/server.ts"
|
|
45
|
+
import { createInstance } from 'i18next';
|
|
46
|
+
import { initReactI18next } from 'react-i18next/initReactI18next';
|
|
47
|
+
import resourcesToBackend from 'i18next-resources-to-backend';
|
|
48
|
+
import { defaultLocale } from '@/i18n.config';
|
|
49
|
+
|
|
50
|
+
// Load JSON resources from src/locales/<locale>/<namespace>.json
|
|
51
|
+
const backend = resourcesToBackend(
|
|
52
|
+
(locale: string, namespace: string) =>
|
|
53
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export async function initI18next(
|
|
57
|
+
locale: string,
|
|
58
|
+
namespaces: string[] = ['common']
|
|
59
|
+
) {
|
|
60
|
+
const i18n = createInstance();
|
|
61
|
+
await i18n
|
|
62
|
+
.use(initReactI18next)
|
|
63
|
+
.use(backend)
|
|
64
|
+
.init({
|
|
65
|
+
lng: locale,
|
|
66
|
+
fallbackLng: defaultLocale,
|
|
67
|
+
ns: namespaces,
|
|
68
|
+
defaultNS: 'common',
|
|
69
|
+
interpolation: { escapeValue: false },
|
|
70
|
+
react: { useSuspense: false },
|
|
71
|
+
});
|
|
72
|
+
return i18n;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```tsx fileName="src/components/I18nProvider.tsx"
|
|
77
|
+
'use client';
|
|
78
|
+
|
|
79
|
+
import * as React from 'react';
|
|
80
|
+
import { I18nextProvider } from 'react-i18next';
|
|
81
|
+
import { createInstance } from 'i18next';
|
|
82
|
+
import { initReactI18next } from 'react-i18next/initReactI18next';
|
|
83
|
+
import resourcesToBackend from 'i18next-resources-to-backend';
|
|
84
|
+
import { defaultLocale } from '@/i18n.config';
|
|
85
|
+
|
|
86
|
+
const backend = resourcesToBackend(
|
|
87
|
+
(locale: string, namespace: string) =>
|
|
88
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
type Props = {
|
|
92
|
+
locale: string;
|
|
93
|
+
namespaces?: string[];
|
|
94
|
+
resources?: Record<string, any>; // { ns: bundle }
|
|
95
|
+
children: React.ReactNode;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default function I18nProvider({
|
|
99
|
+
locale,
|
|
100
|
+
namespaces = ['common'],
|
|
101
|
+
resources,
|
|
102
|
+
children,
|
|
103
|
+
}: Props) {
|
|
104
|
+
const [i18n] = React.useState(() => {
|
|
105
|
+
const i = createInstance();
|
|
106
|
+
|
|
107
|
+
i.use(initReactI18next)
|
|
108
|
+
.use(backend)
|
|
109
|
+
.init({
|
|
110
|
+
lng: locale,
|
|
111
|
+
fallbackLng: defaultLocale,
|
|
112
|
+
ns: namespaces,
|
|
113
|
+
resources: resources ? { [locale]: resources } : undefined,
|
|
114
|
+
defaultNS: 'common',
|
|
115
|
+
interpolation: { escapeValue: false },
|
|
116
|
+
react: { useSuspense: false },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return i;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
127
|
+
import type { ReactNode } from 'react';
|
|
128
|
+
import { locales, defaultLocale, isRtl, type Locale } from '@/i18n.config';
|
|
129
|
+
|
|
130
|
+
export const dynamicParams = false;
|
|
131
|
+
|
|
132
|
+
export function generateStaticParams() {
|
|
133
|
+
return locales.map((locale) => ({ locale }));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default function LocaleLayout({
|
|
137
|
+
children,
|
|
138
|
+
params,
|
|
139
|
+
}: {
|
|
140
|
+
children: ReactNode;
|
|
141
|
+
params: { locale: string };
|
|
142
|
+
}) {
|
|
143
|
+
const locale: Locale = (locales as readonly string[]).includes(params.locale)
|
|
144
|
+
? (params.locale as any)
|
|
145
|
+
: defaultLocale;
|
|
146
|
+
|
|
147
|
+
const dir = isRtl(locale) ? 'rtl' : 'ltr';
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<html lang={locale} dir={dir}>
|
|
151
|
+
<body>{children}</body>
|
|
152
|
+
</html>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```tsx fileName="src/app/[locale]/about.tsx"
|
|
158
|
+
import I18nProvider from '@/components/I18nProvider';
|
|
159
|
+
import { initI18next } from '@/app/i18n/server';
|
|
160
|
+
import type { Locale } from '@/i18n.config';
|
|
161
|
+
import ClientComponent from '@/components/ClientComponent';
|
|
162
|
+
import ServerComponent from '@/components/ServerComponent';
|
|
163
|
+
|
|
164
|
+
// Force static rendering for the page
|
|
165
|
+
export const dynamic = 'force-static';
|
|
166
|
+
|
|
167
|
+
export default async function AboutPage({
|
|
168
|
+
params: { locale },
|
|
169
|
+
}: {
|
|
170
|
+
params: { locale: Locale };
|
|
171
|
+
}) {
|
|
172
|
+
const namespaces = ['common', 'about'] as const;
|
|
173
|
+
|
|
174
|
+
const i18n = await initI18next(locale, [...namespaces]);
|
|
175
|
+
const tAbout = i18n.getFixedT(locale, 'about');
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<I18nProvider locale={locale} namespaces={[...namespaces]}>
|
|
179
|
+
<main>
|
|
180
|
+
<h1>{tAbout('title')}</h1>
|
|
181
|
+
|
|
182
|
+
<ClientComponent />
|
|
183
|
+
<ServerComponent t={tAbout} locale={locale} count={0} />
|
|
184
|
+
</main>
|
|
185
|
+
</I18nProvider>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Translations (one JSON per namespace under `src/locales/...`)**
|
|
191
|
+
|
|
192
|
+
```json fileName="src/locales/en/about.json"
|
|
193
|
+
{
|
|
194
|
+
"title": "About",
|
|
195
|
+
"description": "About page description",
|
|
196
|
+
"counter": {
|
|
197
|
+
"label": "Counter",
|
|
198
|
+
"increment": "Increment"
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```json fileName="src/locales/fr/about.json"
|
|
204
|
+
{
|
|
205
|
+
"title": "À propos",
|
|
206
|
+
"description": "Description de la page À propos",
|
|
207
|
+
"counter": {
|
|
208
|
+
"label": "Compteur",
|
|
209
|
+
"increment": "Incrémenter"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Client component (loads only the required namespace)**
|
|
215
|
+
|
|
216
|
+
```tsx fileName="src/components/ClientComponent.tsx"
|
|
217
|
+
'use client';
|
|
218
|
+
|
|
219
|
+
import React, { useState } from 'react';
|
|
220
|
+
import { useTranslation } from 'react-i18next';
|
|
221
|
+
|
|
222
|
+
const ClientComponent = () => {
|
|
223
|
+
const { t, i18n } = useTranslation('about');
|
|
224
|
+
const [count, setCount] = useState(0);
|
|
225
|
+
|
|
226
|
+
const numberFormat = new Intl.NumberFormat(i18n.language);
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<div>
|
|
230
|
+
<p>{numberFormat.format(count)}</p>
|
|
231
|
+
<button
|
|
232
|
+
aria-label={t('counter.label')}
|
|
233
|
+
onClick={() => setCount((c) => c + 1)}
|
|
234
|
+
>
|
|
235
|
+
{t('counter.increment')}
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export default ClientComponent;
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> Ensure the page/provider includes only the namespaces you need (e.g. `about`).
|
|
245
|
+
> If you use React < 19, memoize heavy formatters like `Intl.NumberFormat`.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### Usage in a sync server component
|
|
250
|
+
|
|
251
|
+
This server component can be inserted under a client component. Because it might be nested under a client boundary, it should be synchronous. Compute translations and locale-dependent formatting in the parent (or pass the `t` function) and provide data via props.
|
|
252
|
+
|
|
253
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
254
|
+
type ServerComponentProps = {
|
|
255
|
+
t: (key: string) => string;
|
|
256
|
+
locale: string;
|
|
257
|
+
count: number;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
|
|
261
|
+
const formatted = new Intl.NumberFormat(locale).format(count);
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<div>
|
|
265
|
+
<p>{formatted}</p>
|
|
266
|
+
<button aria-label={t('counter.label')}>{t('counter.increment')}</button>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export default ServerComponent;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Metadata / Sitemap / Robots
|
|
275
|
+
|
|
276
|
+
Translating content is great. But people usually forget that the main goal of internationalization is to make your website more visible to the world. I18n is an incredible lever to improve your website visibility.
|
|
277
|
+
|
|
278
|
+
Here's a list of good practices regarding multilingual SEO.
|
|
279
|
+
|
|
280
|
+
- set hreflang meta tags in the `<head>` tag
|
|
281
|
+
> It helps search engines to understand what languages are available on the page
|
|
282
|
+
- list all pages translations in the sitemap.xml using `http://www.w3.org/1999/xhtml` XML schema
|
|
283
|
+
>
|
|
284
|
+
- do not forget to exclude prefixed pages from the robots.txt (e.g. `/dashboard`, and `/fr/dashboard`, `/es/dashboard`)
|
|
285
|
+
>
|
|
286
|
+
- use custom Link component to redirect to the most localized page (e.g. in french `<a href="/fr/about">A propos</a>` )
|
|
287
|
+
>
|
|
288
|
+
|
|
289
|
+
Developers often forget to properly reference their pages across locales.
|
|
290
|
+
|
|
291
|
+
```ts fileName="i18n.config.ts"
|
|
292
|
+
export const locales = ['en', 'fr'] as const;
|
|
293
|
+
export type Locale = (typeof locales)[number];
|
|
294
|
+
export const defaultLocale: Locale = 'en';
|
|
295
|
+
export const rtlLocales = ['ar', 'he', 'fa', 'ur'] as const;
|
|
296
|
+
export const isRtl = (locale: string) =>
|
|
297
|
+
(rtlLocales as readonly string[]).includes(locale);
|
|
298
|
+
export function localizedPath(locale: string, path: string) {
|
|
299
|
+
return locale === defaultLocale ? path : '/' + locale + path;
|
|
300
|
+
}
|
|
301
|
+
const ORIGIN = 'https://example.com';
|
|
302
|
+
export function abs(locale: string, path: string) {
|
|
303
|
+
return ORIGIN + localizedPath(locale, path);
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
308
|
+
import type { Metadata } from 'next';
|
|
309
|
+
import { locales, defaultLocale, localizedPath } from '@/i18n.config';
|
|
310
|
+
|
|
311
|
+
export async function generateMetadata({
|
|
312
|
+
params,
|
|
313
|
+
}: {
|
|
314
|
+
params: { locale: string };
|
|
315
|
+
}): Promise<Metadata> {
|
|
316
|
+
const { locale } = params;
|
|
317
|
+
|
|
318
|
+
// Import the correct JSON bundle from src/locales
|
|
319
|
+
const messages = (await import('@/locales/' + locale + '/about.json'))
|
|
320
|
+
.default;
|
|
321
|
+
|
|
322
|
+
const languages = Object.fromEntries(
|
|
323
|
+
locales.map((locale) => [locale, localizedPath(locale, '/about')])
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
title: messages.title,
|
|
328
|
+
description: messages.description,
|
|
329
|
+
alternates: {
|
|
330
|
+
canonical: localizedPath(locale, '/about'),
|
|
331
|
+
languages: { ...languages, 'x-default': '/about' },
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export default async function AboutPage() {
|
|
337
|
+
return <h1>About</h1>;
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
```ts fileName="src/app/sitemap.ts"
|
|
342
|
+
import type { MetadataRoute } from 'next';
|
|
343
|
+
import { locales, defaultLocale, abs } from '@/i18n.config';
|
|
344
|
+
|
|
345
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
346
|
+
const languages = Object.fromEntries(
|
|
347
|
+
locales.map((locale) => [locale, abs(locale, '/about')])
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
return [
|
|
351
|
+
{
|
|
352
|
+
url: abs(defaultLocale, '/about'),
|
|
353
|
+
lastModified: new Date(),
|
|
354
|
+
changeFrequency: 'monthly',
|
|
355
|
+
priority: 0.7,
|
|
356
|
+
alternates: { languages },
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```ts fileName="src/app/robots.ts"
|
|
363
|
+
import type { MetadataRoute } from 'next';
|
|
364
|
+
import { locales, defaultLocale, localizedPath } from '@/i18n.config';
|
|
365
|
+
|
|
366
|
+
const ORIGIN = 'https://example.com';
|
|
367
|
+
|
|
368
|
+
const expandAllLocales = (path: string) => [
|
|
369
|
+
localizedPath(defaultLocale, path),
|
|
370
|
+
...locales
|
|
371
|
+
.filter((locale) => locale !== defaultLocale)
|
|
372
|
+
.map((locale) => localizedPath(locale, path)),
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
export default function robots(): MetadataRoute.Robots {
|
|
376
|
+
const disallow = [
|
|
377
|
+
...expandAllLocales('/dashboard'),
|
|
378
|
+
...expandAllLocales('/admin'),
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
rules: { userAgent: '*', allow: ['/'], disallow },
|
|
383
|
+
host: ORIGIN,
|
|
384
|
+
sitemap: ORIGIN + '/sitemap.xml',
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### Middleware for locale routing
|
|
392
|
+
|
|
393
|
+
Add a middleware to handle locale detection and routing:
|
|
394
|
+
|
|
395
|
+
```ts fileName="src/middleware.ts"
|
|
396
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
397
|
+
import { defaultLocale, locales } from '@/i18n.config';
|
|
398
|
+
|
|
399
|
+
const PUBLIC_FILE = /\.[^/]+$/; // exclude files with extensions
|
|
400
|
+
|
|
401
|
+
export function middleware(request: NextRequest) {
|
|
402
|
+
const { pathname } = request.nextUrl;
|
|
403
|
+
|
|
404
|
+
if (
|
|
405
|
+
pathname.startsWith('/_next') ||
|
|
406
|
+
pathname.startsWith('/api') ||
|
|
407
|
+
pathname.startsWith('/static') ||
|
|
408
|
+
PUBLIC_FILE.test(pathname)
|
|
409
|
+
) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const hasLocale = locales.some(
|
|
414
|
+
(locale) =>
|
|
415
|
+
pathname === '/' + locale || pathname.startsWith('/' + locale + '/')
|
|
416
|
+
);
|
|
417
|
+
if (!hasLocale) {
|
|
418
|
+
const locale = defaultLocale;
|
|
419
|
+
const url = request.nextUrl.clone();
|
|
420
|
+
url.pathname = '/' + locale + (pathname === '/' ? '' : pathname);
|
|
421
|
+
return NextResponse.redirect(url);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export const config = {
|
|
426
|
+
matcher: [
|
|
427
|
+
// Match all paths except the ones starting with these and files with an extension
|
|
428
|
+
'/((?!api|_next|static|.*\\..*).*)',
|
|
429
|
+
],
|
|
430
|
+
};
|
|
431
|
+
```
|