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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/global_jsx_helper.d.ts +4 -0
  4. package/dist/global_jsx_helper.js +1 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +1 -0
  7. package/dist/src/client/components/client_helper_script.d.ts +1 -0
  8. package/dist/src/client/components/client_helper_script.js +15 -0
  9. package/dist/src/client/components/client_provider.d.ts +12 -0
  10. package/dist/src/client/components/client_provider.js +10 -0
  11. package/dist/src/client/components/locale_link.d.ts +8 -0
  12. package/dist/src/client/components/locale_link.js +8 -0
  13. package/dist/src/client/components/locale_link_client.d.ts +3 -0
  14. package/dist/src/client/components/locale_link_client.js +31 -0
  15. package/dist/src/client/functions/get_cookie.d.ts +1 -0
  16. package/dist/src/client/functions/get_cookie.js +12 -0
  17. package/dist/src/client/functions/set_cookie.d.ts +5 -0
  18. package/dist/src/client/functions/set_cookie.js +11 -0
  19. package/dist/src/client/hooks/client_hooks.d.ts +3 -0
  20. package/dist/src/client/hooks/client_hooks.js +19 -0
  21. package/dist/src/client/hooks/use_path_name.d.ts +1 -0
  22. package/dist/src/client/hooks/use_path_name.js +14 -0
  23. package/dist/src/client/index.d.ts +4 -0
  24. package/dist/src/client/index.js +5 -0
  25. package/dist/src/config/cookie_key.d.ts +3 -0
  26. package/dist/src/config/cookie_key.js +3 -0
  27. package/dist/src/config/index.d.ts +4 -0
  28. package/dist/src/config/index.js +4 -0
  29. package/dist/src/config/init_config.d.ts +2 -0
  30. package/dist/src/config/init_config.js +3 -0
  31. package/dist/src/config/intl_config.d.ts +3 -0
  32. package/dist/src/config/intl_config.js +12 -0
  33. package/dist/src/config/intl_sitemap.d.ts +8 -0
  34. package/dist/src/config/intl_sitemap.js +28 -0
  35. package/dist/src/config/middleware.d.ts +4 -0
  36. package/dist/src/config/middleware.js +93 -0
  37. package/dist/src/general/cache_variables.d.ts +13 -0
  38. package/dist/src/general/cache_variables.js +32 -0
  39. package/dist/src/general/general_functions.d.ts +2 -0
  40. package/dist/src/general/general_functions.js +96 -0
  41. package/dist/src/general/get_layout_states.d.ts +0 -0
  42. package/dist/src/general/get_layout_states.js +35 -0
  43. package/dist/src/general/index.d.ts +2 -0
  44. package/dist/src/general/index.js +3 -0
  45. package/dist/src/general/metadata.d.ts +13 -0
  46. package/dist/src/general/metadata.js +24 -0
  47. package/dist/src/index.d.ts +6 -0
  48. package/dist/src/index.js +6 -0
  49. package/dist/src/server/components/helper_script.d.ts +1 -0
  50. package/dist/src/server/components/helper_script.js +104 -0
  51. package/dist/src/server/components/link.d.ts +5 -0
  52. package/dist/src/server/components/link.js +26 -0
  53. package/dist/src/server/components/server_provider.d.ts +6 -0
  54. package/dist/src/server/components/server_provider.js +20 -0
  55. package/dist/src/server/functions/get_user_locale.d.ts +3 -0
  56. package/dist/src/server/functions/get_user_locale.js +34 -0
  57. package/dist/src/server/functions/locale_static_params.d.ts +3 -0
  58. package/dist/src/server/functions/locale_static_params.js +4 -0
  59. package/dist/src/server/functions/server.d.ts +26 -0
  60. package/dist/src/server/functions/server.js +86 -0
  61. package/dist/src/server/functions/use_functions.d.ts +6 -0
  62. package/dist/src/server/functions/use_functions.js +20 -0
  63. package/dist/src/server/index.d.ts +5 -0
  64. package/dist/src/server/index.js +6 -0
  65. package/dist/src/theme_switcher/components/icons.d.ts +6 -0
  66. package/dist/src/theme_switcher/components/icons.js +7 -0
  67. package/dist/src/theme_switcher/components/theme_switcher.d.ts +5 -0
  68. package/dist/src/theme_switcher/components/theme_switcher.js +12 -0
  69. package/dist/src/theme_switcher/components/theme_switcher_button.d.ts +6 -0
  70. package/dist/src/theme_switcher/components/theme_switcher_button.js +25 -0
  71. package/dist/src/theme_switcher/index.d.ts +1 -0
  72. package/dist/src/theme_switcher/index.js +1 -0
  73. package/dist/src/types/index.d.ts +1 -0
  74. package/dist/src/types/index.js +1 -0
  75. package/dist/src/types/types.d.ts +151 -0
  76. package/dist/src/types/types.js +3 -0
  77. 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,4 @@
1
+ import type { JSX } from "react";
2
+ declare global {
3
+ type Component = JSX.Element;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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,3 @@
1
+ import type { LocaleLinkProps } from './locale_link';
2
+ declare const LocaleLinkClient: import("react").ForwardRefExoticComponent<Omit<LocaleLinkProps, "ref"> & import("react").RefAttributes<HTMLAnchorElement>>;
3
+ export default LocaleLinkClient;
@@ -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,5 @@
1
+ export default function setCookie({ name, value, maxAge }: {
2
+ name: string;
3
+ value: unknown;
4
+ maxAge?: number;
5
+ }): void;
@@ -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,3 @@
1
+ import type { TranslatorReturnType } from "../../types/types";
2
+ export declare function useLocale(): string;
3
+ export declare function useTranslations(namespace: string): TranslatorReturnType;
@@ -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,4 @@
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';
@@ -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,3 @@
1
+ export declare const localeCookieName = "__user_locale_key__";
2
+ export declare const isBotCookieKey = "__is_bot_key__";
3
+ export declare const isDarkCookieKey = "__is_dark_key__";
@@ -0,0 +1,3 @@
1
+ export const localeCookieName = '__user_locale_key__';
2
+ export const isBotCookieKey = '__is_bot_key__';
3
+ export const isDarkCookieKey = '__is_dark_key__';
@@ -0,0 +1,4 @@
1
+ export { isBotCookieKey, localeCookieName, isDarkCookieKey } from './cookie_key';
2
+ export { default as intlMiddleware } from './middleware';
3
+ export { setIntlConfig } from './init_config';
4
+ export { default as generateIntlSitemap } from './intl_sitemap';
@@ -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,3 @@
1
+ export function setIntlConfig(config) {
2
+ return config;
3
+ }
@@ -0,0 +1,3 @@
1
+ import type { LocalePrefixMode, Locales, RoutingConfig } from '../types/types';
2
+ declare const config: RoutingConfig<Locales, LocalePrefixMode>;
3
+ export default config;
@@ -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,4 @@
1
+ import type { NextRequest } from 'next/server';
2
+ import { NextResponse } from 'next/server';
3
+ export declare const localesSet: Set<string>;
4
+ export default function intlMiddleware(request: NextRequest): Promise<NextResponse<unknown>>;
@@ -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,2 @@
1
+ import type { TranslationObject, TranslatorReturnType } from "../types/types";
2
+ export declare function getTranslationsImpl(locale: string, messages: TranslationObject, namespace: string, cacheKey?: string): TranslatorReturnType;
@@ -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