cloudflare-next-intl 0.1.1
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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/global_jsx_helper.d.ts +4 -0
- package/dist/global_jsx_helper.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/client/components/client_helper_script.d.ts +1 -0
- package/dist/src/client/components/client_helper_script.js +15 -0
- package/dist/src/client/components/client_provider.d.ts +12 -0
- package/dist/src/client/components/client_provider.js +10 -0
- package/dist/src/client/components/locale_link.d.ts +8 -0
- package/dist/src/client/components/locale_link.js +8 -0
- package/dist/src/client/components/locale_link_client.d.ts +3 -0
- package/dist/src/client/components/locale_link_client.js +31 -0
- package/dist/src/client/functions/get_cookie.d.ts +1 -0
- package/dist/src/client/functions/get_cookie.js +12 -0
- package/dist/src/client/functions/set_cookie.d.ts +5 -0
- package/dist/src/client/functions/set_cookie.js +11 -0
- package/dist/src/client/hooks/client_hooks.d.ts +3 -0
- package/dist/src/client/hooks/client_hooks.js +19 -0
- package/dist/src/client/hooks/use_path_name.d.ts +1 -0
- package/dist/src/client/hooks/use_path_name.js +14 -0
- package/dist/src/client/index.d.ts +4 -0
- package/dist/src/client/index.js +5 -0
- package/dist/src/config/cookie_key.d.ts +3 -0
- package/dist/src/config/cookie_key.js +3 -0
- package/dist/src/config/index.d.ts +4 -0
- package/dist/src/config/index.js +4 -0
- package/dist/src/config/init_config.d.ts +2 -0
- package/dist/src/config/init_config.js +3 -0
- package/dist/src/config/intl_config.d.ts +3 -0
- package/dist/src/config/intl_config.js +12 -0
- package/dist/src/config/intl_sitemap.d.ts +8 -0
- package/dist/src/config/intl_sitemap.js +28 -0
- package/dist/src/config/middleware.d.ts +4 -0
- package/dist/src/config/middleware.js +93 -0
- package/dist/src/general/cache_variables.d.ts +13 -0
- package/dist/src/general/cache_variables.js +32 -0
- package/dist/src/general/general_functions.d.ts +2 -0
- package/dist/src/general/general_functions.js +96 -0
- package/dist/src/general/get_layout_states.d.ts +0 -0
- package/dist/src/general/get_layout_states.js +35 -0
- package/dist/src/general/index.d.ts +2 -0
- package/dist/src/general/index.js +3 -0
- package/dist/src/general/metadata.d.ts +13 -0
- package/dist/src/general/metadata.js +24 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +6 -0
- package/dist/src/server/components/helper_script.d.ts +1 -0
- package/dist/src/server/components/helper_script.js +104 -0
- package/dist/src/server/components/link.d.ts +5 -0
- package/dist/src/server/components/link.js +26 -0
- package/dist/src/server/components/server_provider.d.ts +6 -0
- package/dist/src/server/components/server_provider.js +20 -0
- package/dist/src/server/functions/get_user_locale.d.ts +3 -0
- package/dist/src/server/functions/get_user_locale.js +34 -0
- package/dist/src/server/functions/locale_static_params.d.ts +3 -0
- package/dist/src/server/functions/locale_static_params.js +4 -0
- package/dist/src/server/functions/server.d.ts +26 -0
- package/dist/src/server/functions/server.js +86 -0
- package/dist/src/server/functions/use_functions.d.ts +6 -0
- package/dist/src/server/functions/use_functions.js +20 -0
- package/dist/src/server/index.d.ts +5 -0
- package/dist/src/server/index.js +6 -0
- package/dist/src/theme_switcher/components/icons.d.ts +6 -0
- package/dist/src/theme_switcher/components/icons.js +7 -0
- package/dist/src/theme_switcher/components/theme_switcher.d.ts +5 -0
- package/dist/src/theme_switcher/components/theme_switcher.js +12 -0
- package/dist/src/theme_switcher/components/theme_switcher_button.d.ts +6 -0
- package/dist/src/theme_switcher/components/theme_switcher_button.js +25 -0
- package/dist/src/theme_switcher/index.d.ts +1 -0
- package/dist/src/theme_switcher/index.js +1 -0
- package/dist/src/types/index.d.ts +1 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/types/types.d.ts +151 -0
- package/dist/src/types/types.js +3 -0
- package/package.json +155 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Demian
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# cloudflare-next-intl
|
|
2
|
+
|
|
3
|
+
Optimized internationalization (i18n) package specialized for Next.js App Router
|
|
4
|
+
and Cloudflare environment.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **Optimized for Cloudflare**: Designed to work seamlessly with Cloudflare
|
|
9
|
+
Pages and Workers.
|
|
10
|
+
- **Server Components Support**: Full support for Next.js App Router and Server
|
|
11
|
+
Components.
|
|
12
|
+
- **Fast and Efficient**: Low overhead and minimal bundle size.
|
|
13
|
+
- **Tree-shaking**: Properly architected for optimal tree-shaking.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install cloudflare-next-intl
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Configuration
|
|
24
|
+
|
|
25
|
+
Set up your internationalization configuration:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { setIntlConfig } from "cloudflare-next-intl/setIntlConfig";
|
|
29
|
+
|
|
30
|
+
export default setIntlConfig({
|
|
31
|
+
locales: ["en", "de"],
|
|
32
|
+
defaultLocale: "en",
|
|
33
|
+
// ... other config
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Server Components
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { getTranslations } from "cloudflare-next-intl/server";
|
|
41
|
+
|
|
42
|
+
export default async function Page() {
|
|
43
|
+
const t = await getTranslations("Index");
|
|
44
|
+
return <h1>{t("title")}</h1>;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Client Components
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
"use client";
|
|
52
|
+
|
|
53
|
+
import { LocaleLink } from "cloudflare-next-intl/client";
|
|
54
|
+
|
|
55
|
+
export function Navigation() {
|
|
56
|
+
return (
|
|
57
|
+
<LocaleLink href="/about">
|
|
58
|
+
About Us
|
|
59
|
+
</LocaleLink>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './src';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './src';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ClientHelperScript(): null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { isDarkCookieKey } from "../../config";
|
|
4
|
+
import getCookie from "../functions/get_cookie";
|
|
5
|
+
export default function ClientHelperScript() {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const isDark = getCookie(isDarkCookieKey);
|
|
8
|
+
const classList = document.documentElement.classList;
|
|
9
|
+
const isDarkBool = isDark === 'true';
|
|
10
|
+
if (classList.contains('dark') !== isDarkBool) {
|
|
11
|
+
classList.toggle('dark', isDarkBool);
|
|
12
|
+
}
|
|
13
|
+
}, []);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TranslationObject } from "../../types/types";
|
|
2
|
+
interface LocaleContextType {
|
|
3
|
+
language: string;
|
|
4
|
+
messages: TranslationObject;
|
|
5
|
+
}
|
|
6
|
+
export declare const LocaleContext: import("react").Context<LocaleContextType | undefined>;
|
|
7
|
+
export default function LocationzationClientProvider({ language, messages, children }: {
|
|
8
|
+
language: string;
|
|
9
|
+
messages: TranslationObject;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}): Component;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_variables";
|
|
4
|
+
import { createContext } from "react";
|
|
5
|
+
export const LocaleContext = createContext(undefined);
|
|
6
|
+
export default function LocationzationClientProvider({ language, messages, children }) {
|
|
7
|
+
setLocaleCache(language);
|
|
8
|
+
setMessageForLocaleCache(language, messages);
|
|
9
|
+
return _jsx(LocaleContext.Provider, { value: { language, messages }, children: children });
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type LinkProps } from 'next/link';
|
|
2
|
+
import { type ComponentProps } from 'react';
|
|
3
|
+
type NextLinkProps = Omit<ComponentProps<'a'>, keyof LinkProps> & Omit<LinkProps, 'locale' | 'href' | 'prefetch' | 'onNavigate' | 'hrefLang' | 'replace' | 'scroll'>;
|
|
4
|
+
export type LocaleLinkProps = NextLinkProps & {
|
|
5
|
+
locale: string;
|
|
6
|
+
};
|
|
7
|
+
declare const LocaleLink: import("react").ForwardRefExoticComponent<Omit<LocaleLinkProps, "ref"> & import("react").RefAttributes<HTMLAnchorElement>>;
|
|
8
|
+
export default LocaleLink;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, Suspense, } from 'react';
|
|
3
|
+
import LocaleLinkClient from './locale_link_client';
|
|
4
|
+
function LocaleLinkComponent(params, ref) {
|
|
5
|
+
return _jsx(Suspense, { fallback: _jsx("a", { ...params, ref: ref, className: params.className + ' pointer-events-none' }), children: _jsx(LocaleLinkClient, { ref: ref, ...params }) });
|
|
6
|
+
}
|
|
7
|
+
const LocaleLink = forwardRef(LocaleLinkComponent);
|
|
8
|
+
export default LocaleLink;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useEffect, useState, } from 'react';
|
|
4
|
+
import config from '../../config/intl_config';
|
|
5
|
+
import usePathname from '../hooks/use_path_name';
|
|
6
|
+
import { localeCookieName } from '../../config/cookie_key';
|
|
7
|
+
import setCookie from '../functions/set_cookie';
|
|
8
|
+
import { useSearchParams } from 'next/navigation';
|
|
9
|
+
function ClientLocaleLinkComponent({ locale, className, ...rest }, ref) {
|
|
10
|
+
const pathname = usePathname();
|
|
11
|
+
const searchParams = useSearchParams();
|
|
12
|
+
const [hash, setHash] = useState('');
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setHash(window.location.hash);
|
|
15
|
+
}, [pathname, searchParams]);
|
|
16
|
+
const isDefaultLocale = locale === config.defaultLocale;
|
|
17
|
+
const localePrefix = isDefaultLocale ? '' : `/${locale}`;
|
|
18
|
+
const search = searchParams.toString();
|
|
19
|
+
// Fix for the root path to avoid a trailing slash like `/fr/`
|
|
20
|
+
const newPathname = pathname === '/' && (localePrefix) ? '' : pathname;
|
|
21
|
+
const href = `${localePrefix}${newPathname}${search ? `?${search}` : ''}${hash}`;
|
|
22
|
+
function handleNavigate(e) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setCookie({ name: localeCookieName, value: locale });
|
|
25
|
+
window.location.replace(href);
|
|
26
|
+
}
|
|
27
|
+
;
|
|
28
|
+
return _jsx("a", { ref: ref, hrefLang: locale, className: className, ...rest, href: href, onClick: handleNavigate });
|
|
29
|
+
}
|
|
30
|
+
const LocaleLinkClient = forwardRef(ClientLocaleLinkComponent);
|
|
31
|
+
export default LocaleLinkClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function getCookie(name: string): string | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
export default function getCookie(name) {
|
|
3
|
+
try {
|
|
4
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
|
5
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
6
|
+
}
|
|
7
|
+
catch (e) {
|
|
8
|
+
console.error(`Get cookie on client side error: ${e}`);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
export default function setCookie({ name, value, maxAge }) {
|
|
3
|
+
try {
|
|
4
|
+
const cookieString = `${name}=${value}; path=/; max-age=${maxAge ?? 31536000}; SameSite=Lax;`;
|
|
5
|
+
document.cookie = cookieString;
|
|
6
|
+
}
|
|
7
|
+
catch (e) {
|
|
8
|
+
console.error(`Set cookie on client side error: ${e}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useContext, useMemo } from "react";
|
|
3
|
+
import { LocaleContext } from "../components/client_provider";
|
|
4
|
+
import { getTranslationsImpl } from "../../general/general_functions";
|
|
5
|
+
export function useLocale() {
|
|
6
|
+
const context = useContext(LocaleContext);
|
|
7
|
+
if (context === undefined) {
|
|
8
|
+
throw new Error('useLocale must be used within a LocaleContext');
|
|
9
|
+
}
|
|
10
|
+
return context.language;
|
|
11
|
+
}
|
|
12
|
+
export function useTranslations(namespace) {
|
|
13
|
+
const context = useContext(LocaleContext);
|
|
14
|
+
if (context === undefined) {
|
|
15
|
+
throw new Error('useTranslations must be used within a LocaleContext');
|
|
16
|
+
}
|
|
17
|
+
const { language, messages } = context;
|
|
18
|
+
return useMemo(() => getTranslationsImpl(language, messages, namespace), [language, messages, namespace]);
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function usePathname(): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { usePathname as nextUsePathname } from "next/navigation";
|
|
3
|
+
import { useLocale } from "./client_hooks";
|
|
4
|
+
export default function usePathname() {
|
|
5
|
+
const pathname = nextUsePathname();
|
|
6
|
+
const locale = useLocale();
|
|
7
|
+
const path = pathname.replace(`/${locale}`, '');
|
|
8
|
+
if (path) {
|
|
9
|
+
return path;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
return '/';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as LocaleLink } from './components/locale_link';
|
|
2
|
+
export { default as usePathname } from './hooks/use_path_name';
|
|
3
|
+
export { default as setCookieClient } from './functions/set_cookie';
|
|
4
|
+
export { default as getCookieClient } from './functions/get_cookie';
|
|
5
|
+
// export { useLocale, useTranslations } from './hooks/client_hooks';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { isBotCookieKey, localeCookieName, isDarkCookieKey } from './cookie_key'; // Export specific middleware function
|
|
2
|
+
export { default as intlMiddleware } from './middleware'; // Export specific middleware function
|
|
3
|
+
export { setIntlConfig } from './init_config';
|
|
4
|
+
export { default as generateIntlSitemap } from './intl_sitemap';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { LocalePrefixMode, Locales, RoutingConfig } from '../types/types';
|
|
2
|
+
export declare function setIntlConfig<const AppLocales extends Locales, const AppLocalePrefixMode extends LocalePrefixMode = 'as-needed'>(config: RoutingConfig<AppLocales, AppLocalePrefixMode>): RoutingConfig<AppLocales, AppLocalePrefixMode>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import intlConfig from '@intl-config';
|
|
2
|
+
function getConfig() {
|
|
3
|
+
const value = intlConfig;
|
|
4
|
+
if (value) {
|
|
5
|
+
return intlConfig;
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
throw Error('Please set config file and set path to it in next.config as in the example');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const config = getConfig();
|
|
12
|
+
export default config;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MetadataRoute } from 'next';
|
|
2
|
+
import type { IntlSitemap } from '../types/types';
|
|
3
|
+
declare function generateIntlSitemapIml({ intlSitemap, url }: {
|
|
4
|
+
intlSitemap: IntlSitemap[];
|
|
5
|
+
url: string;
|
|
6
|
+
}): MetadataRoute.Sitemap;
|
|
7
|
+
declare const generateIntlSitemap: typeof generateIntlSitemapIml;
|
|
8
|
+
export default generateIntlSitemap;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { languages } from '../general/metadata';
|
|
2
|
+
import config from './intl_config';
|
|
3
|
+
import { cache } from 'react';
|
|
4
|
+
function generateAlternates(url, link) {
|
|
5
|
+
return {
|
|
6
|
+
languages: languages(url, link),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function generateIntlSitemapIml({ intlSitemap, url }) {
|
|
10
|
+
const sitemap = [];
|
|
11
|
+
for (const customRoute of intlSitemap) {
|
|
12
|
+
const linkPart = customRoute.link == '/' ? undefined : customRoute.link;
|
|
13
|
+
const alternates = generateAlternates(url, linkPart);
|
|
14
|
+
for (const locale of config.locales) {
|
|
15
|
+
const localeValue = locale === config.defaultLocale ? '' : `/${locale}`;
|
|
16
|
+
const localeUrl = url + localeValue;
|
|
17
|
+
sitemap.push({
|
|
18
|
+
...customRoute,
|
|
19
|
+
alternates: alternates,
|
|
20
|
+
url: localeUrl + (linkPart ?? ''),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
sitemap.sort((a, b) => a.url.localeCompare(b.url));
|
|
25
|
+
return sitemap;
|
|
26
|
+
}
|
|
27
|
+
const generateIntlSitemap = cache(generateIntlSitemapIml);
|
|
28
|
+
export default generateIntlSitemap;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { languageDetecotr } from '../server/functions/get_user_locale';
|
|
3
|
+
import config from './intl_config';
|
|
4
|
+
import { isBotCookieKey, localeCookieName } from './cookie_key';
|
|
5
|
+
import { cache } from 'react';
|
|
6
|
+
const sameSite = false;
|
|
7
|
+
const defaultCookieOption = {
|
|
8
|
+
path: '/', // Cookie is valid for the entire domain
|
|
9
|
+
maxAge: 2592000, // Store cookie for 30 days (in seconds).
|
|
10
|
+
httpOnly: false,
|
|
11
|
+
secure: false, // Send cookie only over HTTPS in production
|
|
12
|
+
sameSite: sameSite, // Protection against CSRF attacks. 'strict' or 'lax' are good choices.
|
|
13
|
+
};
|
|
14
|
+
async function getIsBotValue(userAgent) {
|
|
15
|
+
if (userAgent === null)
|
|
16
|
+
return false;
|
|
17
|
+
const { isBot } = await import('next/dist/server/web/spec-extension/user-agent');
|
|
18
|
+
return isBot(userAgent ?? '');
|
|
19
|
+
}
|
|
20
|
+
const getIsBotValueCache = cache(getIsBotValue);
|
|
21
|
+
export const localesSet = new Set(config.locales);
|
|
22
|
+
// This middleware function runs for every incoming request
|
|
23
|
+
export default async function intlMiddleware(request) {
|
|
24
|
+
try {
|
|
25
|
+
let initialChosenLocale;
|
|
26
|
+
const existingLocaleCookie = request.cookies.get(localeCookieName)?.value;
|
|
27
|
+
let isSEOBot = undefined;
|
|
28
|
+
// 1. The most performant step: Check if a locale cookie is already set
|
|
29
|
+
// Also, verify if the value from this cookie is actually supported
|
|
30
|
+
if (existingLocaleCookie && localesSet.has(existingLocaleCookie)) {
|
|
31
|
+
initialChosenLocale = existingLocaleCookie;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const userAgent = request.headers.get('user-agent');
|
|
35
|
+
isSEOBot = await getIsBotValueCache(userAgent);
|
|
36
|
+
initialChosenLocale = isSEOBot ? config.defaultLocale : languageDetecotr(request.headers.get('accept-language'));
|
|
37
|
+
}
|
|
38
|
+
const { pathname, search, hash } = request.nextUrl;
|
|
39
|
+
let urlLocale;
|
|
40
|
+
let pathWithoutLocale;
|
|
41
|
+
const pathSegments = pathname.split('/').filter(Boolean); // e.g., ['', 'en', 'about'] -> ['en', 'about']
|
|
42
|
+
const firstSegment = pathSegments[0];
|
|
43
|
+
const languageValue = firstSegment;
|
|
44
|
+
// Check if the first segment of the path is one of the supported locales
|
|
45
|
+
if (pathSegments.length > 0 && localesSet.has(languageValue)) {
|
|
46
|
+
urlLocale = languageValue;
|
|
47
|
+
pathWithoutLocale = '/' + pathSegments.slice(1).join('/'); // Remove the locale segment
|
|
48
|
+
if (pathWithoutLocale === '')
|
|
49
|
+
pathWithoutLocale = '/'; // Ensure it's '/' for root after removing locale
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// No locale prefix in the URL. The actual pathname is the full original pathname.
|
|
53
|
+
pathWithoutLocale = pathname;
|
|
54
|
+
}
|
|
55
|
+
const effectiveLocaleForRequest = urlLocale ?? initialChosenLocale;
|
|
56
|
+
let response;
|
|
57
|
+
if (!urlLocale) {
|
|
58
|
+
const targetPath = `/${effectiveLocaleForRequest}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`;
|
|
59
|
+
const targetUrl = new URL(`${targetPath}${search}${hash}`, request.url);
|
|
60
|
+
if (initialChosenLocale === config.defaultLocale) {
|
|
61
|
+
response = NextResponse.rewrite(targetUrl, { request });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
response = NextResponse.redirect(targetUrl, request);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
response = NextResponse.next({
|
|
69
|
+
request,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (!existingLocaleCookie ||
|
|
73
|
+
existingLocaleCookie !== effectiveLocaleForRequest) {
|
|
74
|
+
response.cookies.set(localeCookieName, effectiveLocaleForRequest, defaultCookieOption);
|
|
75
|
+
if (isSEOBot !== undefined) {
|
|
76
|
+
response.cookies.set(isBotCookieKey, isSEOBot.toString(), {
|
|
77
|
+
...defaultCookieOption,
|
|
78
|
+
maxAge: 31536000, // 1 year
|
|
79
|
+
secure: process.env.NODE_ENV === 'production',
|
|
80
|
+
httpOnly: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
response.headers.set('Content-Language', effectiveLocaleForRequest);
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
console.error(`Middleware Error ${e}`);
|
|
89
|
+
return NextResponse.next({
|
|
90
|
+
request,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TranslationObject, TranslatorReturnType } from "../types/types";
|
|
2
|
+
/**
|
|
3
|
+
* Sets the current locale.
|
|
4
|
+
* @param locale The language to set.
|
|
5
|
+
*/
|
|
6
|
+
export declare function setLocaleCache(locale: string): void;
|
|
7
|
+
export declare function setLocaleAsync(params: Promise<{
|
|
8
|
+
locale: string;
|
|
9
|
+
}>): Promise<void>;
|
|
10
|
+
export declare function getLocaleCache(): string | undefined;
|
|
11
|
+
export declare function setMessageForLocaleCache(locale: string, message: TranslationObject): void;
|
|
12
|
+
export declare function getMessageCache(locale?: string): TranslationObject | undefined;
|
|
13
|
+
export declare function setTranslationCache(cache: string, value: TranslatorReturnType): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Caches for loaded translation objects and memoized translation functions.
|
|
2
|
+
const loadedTranslations = new Map();
|
|
3
|
+
const translationFunctionsCache = new Map();
|
|
4
|
+
let currentLanguage = undefined; // Renamed 'language' to 'currentLanguage' for clarity
|
|
5
|
+
/**
|
|
6
|
+
* Sets the current locale.
|
|
7
|
+
* @param locale The language to set.
|
|
8
|
+
*/
|
|
9
|
+
export function setLocaleCache(locale) {
|
|
10
|
+
currentLanguage = locale;
|
|
11
|
+
}
|
|
12
|
+
export async function setLocaleAsync(params) {
|
|
13
|
+
const { locale } = await params;
|
|
14
|
+
setLocaleCache(locale);
|
|
15
|
+
}
|
|
16
|
+
export function getLocaleCache() {
|
|
17
|
+
return currentLanguage;
|
|
18
|
+
}
|
|
19
|
+
export function setMessageForLocaleCache(locale, message) {
|
|
20
|
+
loadedTranslations.set(locale, message);
|
|
21
|
+
}
|
|
22
|
+
export function getMessageCache(locale) {
|
|
23
|
+
if (locale && loadedTranslations.has(locale)) {
|
|
24
|
+
return loadedTranslations.get(locale);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function setTranslationCache(cache, value) {
|
|
31
|
+
translationFunctionsCache.set(cache, value);
|
|
32
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { setTranslationCache } from "./cache_variables";
|
|
2
|
+
/**
|
|
3
|
+
* Logs a warning message and returns a fallback translation function.
|
|
4
|
+
* This function helps in debugging missing translations or incorrect structures.
|
|
5
|
+
* @param message The main warning message.
|
|
6
|
+
* @param cacheKey The key used for caching the translation function.
|
|
7
|
+
* @param locale The effective locale.
|
|
8
|
+
* @param namespace The namespace being accessed.
|
|
9
|
+
* @param key The specific translation key being looked up.
|
|
10
|
+
* @returns A fallback function that returns the key itself.
|
|
11
|
+
*/
|
|
12
|
+
const errorAndReturnFallback = (message, cacheKey, locale, namespace, key) => {
|
|
13
|
+
const parts = [
|
|
14
|
+
message,
|
|
15
|
+
namespace ? `Namespace: "${namespace}"` : '',
|
|
16
|
+
key ? `Key: "${key}"` : '',
|
|
17
|
+
`Locale: "${locale}"`,
|
|
18
|
+
].filter(Boolean); // Filter out empty parts
|
|
19
|
+
console.error(parts.join(' | '));
|
|
20
|
+
const fallbackFn = (k) => k; // Fallback function simply returns the key
|
|
21
|
+
setTranslationCache(cacheKey, fallbackFn);
|
|
22
|
+
return fallbackFn;
|
|
23
|
+
};
|
|
24
|
+
export function getTranslationsImpl(locale, messages, namespace, cacheKey) {
|
|
25
|
+
const cacheKeyValue = cacheKey ?? `${locale}-${namespace}`;
|
|
26
|
+
const namespaceParts = namespace.split('.');
|
|
27
|
+
let currentLevel = messages;
|
|
28
|
+
let translationsBase;
|
|
29
|
+
// Traverse the translation object based on the namespace parts.
|
|
30
|
+
for (let i = 0; i < namespaceParts.length; i++) {
|
|
31
|
+
const part = namespaceParts[i];
|
|
32
|
+
const nextLevel = currentLevel[part];
|
|
33
|
+
if (i === namespaceParts.length - 1) {
|
|
34
|
+
// Last part of the namespace, should resolve to an object (the base for translations).
|
|
35
|
+
if (typeof nextLevel === 'object' && nextLevel !== null) {
|
|
36
|
+
translationsBase = nextLevel;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Namespace does not resolve to an object as expected.
|
|
40
|
+
return errorAndReturnFallback(`Namespace "${namespace}" does not resolve to an object.`, cacheKeyValue, locale, namespace);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Intermediate part of the namespace, must be an object.
|
|
45
|
+
if (typeof nextLevel === 'object' && nextLevel !== null) {
|
|
46
|
+
currentLevel = nextLevel;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Invalid structure in the middle of the namespace path.
|
|
50
|
+
return errorAndReturnFallback(`Namespace "${namespace}" has invalid structure at "${part}". Expected object, got "${typeof nextLevel}".`, cacheKeyValue, locale, namespace);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// If after traversal, no base translations object was found.
|
|
55
|
+
if (!translationsBase) {
|
|
56
|
+
return errorAndReturnFallback(`Translations for namespace "${namespace}" could not be found.`, cacheKeyValue, locale, namespace);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The actual translation function for a given key within the resolved namespace.
|
|
60
|
+
* @param key The dot-separated translation key (e.g., "title", "description.long").
|
|
61
|
+
* @returns The translated string or the key itself if not found/invalid.
|
|
62
|
+
*/
|
|
63
|
+
const translateFunction = (key) => {
|
|
64
|
+
const keyParts = key.split('.');
|
|
65
|
+
let currentTranslation = translationsBase;
|
|
66
|
+
// Traverse the resolved translations base using the key parts.
|
|
67
|
+
for (let i = 0; i < keyParts.length; i++) {
|
|
68
|
+
const part = keyParts[i];
|
|
69
|
+
if (typeof currentTranslation === 'string') {
|
|
70
|
+
// Translation key path prematurely leads to a string.
|
|
71
|
+
console.warn(`Translation key "${key}" in namespace "${namespace}" leads to a string prematurely at "${part}" for locale "${locale}".`);
|
|
72
|
+
return key; // Return the key as fallback
|
|
73
|
+
}
|
|
74
|
+
const value = currentTranslation[part];
|
|
75
|
+
if (i === keyParts.length - 1) {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Intermediate part of the key, must be an object.
|
|
80
|
+
if (typeof value === 'object' && value !== null) {
|
|
81
|
+
currentTranslation = value;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Invalid structure in the middle of the translation key path.
|
|
85
|
+
console.warn(`Translation key "${key}" in namespace "${namespace}" has invalid structure at "${part}" for locale "${locale}". Expected object, got "${typeof value}".`);
|
|
86
|
+
return key; // Return the key as fallback
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// If the loop completes and no string translation was found (e.g., key missing or not a string).
|
|
91
|
+
console.warn(`Translation key "${key}" in namespace "${namespace}" is missing or not a string for locale "${locale}".`);
|
|
92
|
+
return key; // Return the key as fallback
|
|
93
|
+
};
|
|
94
|
+
setTranslationCache(cacheKeyValue, translateFunction);
|
|
95
|
+
return translateFunction;
|
|
96
|
+
}
|
|
File without changes
|