cloudflare-next-intl 0.2.2 → 0.3.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/README.md +2 -3
- package/dist/src/client/components/client_provider.d.ts +3 -1
- package/dist/src/client/components/client_provider.js +16 -3
- package/dist/src/client/functions/get_cookie.bench.d.ts +1 -0
- package/dist/src/client/functions/get_cookie.bench.js +11 -0
- package/dist/src/client/functions/set_cookie.bench.d.ts +1 -0
- package/dist/src/client/functions/set_cookie.bench.js +10 -0
- package/dist/src/client/functions/set_cookie.d.ts +4 -2
- package/dist/src/client/functions/set_cookie.js +3 -1
- package/dist/src/client/hooks/client_hooks.d.ts +5 -0
- package/dist/src/client/hooks/client_hooks.js +7 -2
- package/dist/src/client/index.js +3 -0
- package/dist/src/config/cookie_key.bench.d.ts +1 -0
- package/dist/src/config/cookie_key.bench.js +9 -0
- package/dist/src/config/init_config.bench.d.ts +1 -0
- package/dist/src/config/init_config.bench.js +7 -0
- package/dist/src/config/intl_config.js +4 -1
- package/dist/src/config/intl_sitemap.bench.d.ts +1 -0
- package/dist/src/config/intl_sitemap.bench.js +18 -0
- package/dist/src/config/intl_sitemap.js +1 -1
- package/dist/src/config/middleware.bench.d.ts +1 -0
- package/dist/src/config/middleware.bench.js +34 -0
- package/dist/src/config/middleware.js +39 -8
- package/dist/src/firebase_auth/client/auth_actions.bench.d.ts +1 -0
- package/dist/src/firebase_auth/client/auth_actions.bench.js +36 -0
- package/dist/src/firebase_auth/client/auth_actions.d.ts +46 -0
- package/dist/src/firebase_auth/client/auth_actions.js +102 -0
- package/dist/src/firebase_auth/client/auth_user_cache.bench.d.ts +1 -0
- package/dist/src/firebase_auth/client/auth_user_cache.bench.js +11 -0
- package/dist/src/firebase_auth/client/auth_user_cache.d.ts +4 -0
- package/dist/src/firebase_auth/client/auth_user_cache.js +14 -0
- package/dist/src/firebase_auth/client/auth_user_provider.d.ts +36 -0
- package/dist/src/firebase_auth/client/auth_user_provider.js +148 -0
- package/dist/src/firebase_auth/client/firebase_client.bench.d.ts +1 -0
- package/dist/src/firebase_auth/client/firebase_client.bench.js +28 -0
- package/dist/src/firebase_auth/client/firebase_client.d.ts +18 -0
- package/dist/src/firebase_auth/client/firebase_client.js +40 -0
- package/dist/src/firebase_auth/client/use_auth_user.d.ts +12 -0
- package/dist/src/firebase_auth/client/use_auth_user.js +20 -0
- package/dist/src/firebase_auth/error_messages/default_messages.en.bench.d.ts +1 -0
- package/dist/src/firebase_auth/error_messages/default_messages.en.bench.js +10 -0
- package/dist/src/firebase_auth/error_messages/default_messages.en.d.ts +1 -0
- package/dist/src/firebase_auth/error_messages/default_messages.en.js +14 -0
- package/dist/src/firebase_auth/error_messages/firebase_auth_error_helper.bench.d.ts +1 -0
- package/dist/src/firebase_auth/error_messages/firebase_auth_error_helper.bench.js +15 -0
- package/dist/src/firebase_auth/error_messages/firebase_auth_error_helper.d.ts +6 -0
- package/dist/src/firebase_auth/error_messages/firebase_auth_error_helper.js +42 -0
- package/dist/src/firebase_auth/index.d.ts +9 -0
- package/dist/src/firebase_auth/index.js +13 -0
- package/dist/src/firebase_auth/middleware/update_session.bench.d.ts +1 -0
- package/dist/src/firebase_auth/middleware/update_session.bench.js +37 -0
- package/dist/src/firebase_auth/middleware/update_session.d.ts +22 -0
- package/dist/src/firebase_auth/middleware/update_session.js +213 -0
- package/dist/src/firebase_auth/require_config.bench.d.ts +1 -0
- package/dist/src/firebase_auth/require_config.bench.js +24 -0
- package/dist/src/firebase_auth/require_config.d.ts +8 -0
- package/dist/src/firebase_auth/require_config.js +13 -0
- package/dist/src/firebase_auth/server/auth_user_server_provider.d.ts +24 -0
- package/dist/src/firebase_auth/server/auth_user_server_provider.js +51 -0
- package/dist/src/firebase_auth/server/firebase_server.bench.d.ts +1 -0
- package/dist/src/firebase_auth/server/firebase_server.bench.js +27 -0
- package/dist/src/firebase_auth/server/firebase_server.d.ts +15 -0
- package/dist/src/firebase_auth/server/firebase_server.js +50 -0
- package/dist/src/firebase_auth/server/use_auth_user_server.bench.d.ts +1 -0
- package/dist/src/firebase_auth/server/use_auth_user_server.bench.js +14 -0
- package/dist/src/firebase_auth/server/use_auth_user_server.d.ts +19 -0
- package/dist/src/firebase_auth/server/use_auth_user_server.js +19 -0
- package/dist/src/firebase_auth/types.d.ts +22 -0
- package/dist/src/firebase_auth/types.js +1 -0
- package/dist/src/general/cache_variables.bench.d.ts +1 -0
- package/dist/src/general/cache_variables.bench.js +19 -0
- package/dist/src/general/cache_variables.d.ts +1 -0
- package/dist/src/general/cache_variables.js +4 -6
- package/dist/src/general/general_functions.bench.d.ts +1 -0
- package/dist/src/general/general_functions.bench.js +18 -0
- package/dist/src/general/general_functions.js +4 -0
- package/dist/src/general/index.js +0 -1
- package/dist/src/general/metadata.bench.d.ts +1 -0
- package/dist/src/general/metadata.bench.js +15 -0
- package/dist/src/general/metadata.js +1 -1
- package/dist/src/server/components/server_provider.js +7 -1
- package/dist/src/server/functions/get_user_locale.bench.d.ts +1 -0
- package/dist/src/server/functions/get_user_locale.bench.js +13 -0
- package/dist/src/server/functions/get_user_locale.js +7 -4
- package/dist/src/server/functions/server.bench.d.ts +1 -0
- package/dist/src/server/functions/server.bench.js +39 -0
- package/dist/src/server/functions/server.js +9 -5
- package/dist/src/server/functions/use_functions.bench.d.ts +1 -0
- package/dist/src/server/functions/use_functions.bench.js +23 -0
- package/dist/src/server/functions/use_functions.d.ts +5 -0
- package/dist/src/server/functions/use_functions.js +19 -4
- package/dist/src/server/index.js +4 -1
- package/dist/src/test_utils/mock_next_server.d.ts +5 -0
- package/dist/src/test_utils/mock_next_server.js +10 -0
- package/dist/src/types/types.d.ts +48 -1
- package/llms.txt +47 -0
- package/package.json +46 -8
package/README.md
CHANGED
|
@@ -65,9 +65,8 @@ const nextConfig: NextConfig = {
|
|
|
65
65
|
export default nextConfig;
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
If this alias is missing, `cloudflare-next-intl
|
|
69
|
-
|
|
70
|
-
at startup.
|
|
68
|
+
If this alias is missing, any import from `cloudflare-next-intl` throws an
|
|
69
|
+
error naming the missing `@intl-config` alias at startup.
|
|
71
70
|
|
|
72
71
|
### 3. Wire up the middleware
|
|
73
72
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { TranslationObject } from "../../types/types";
|
|
2
|
+
import type { SerializedAuthUser } from "../../firebase_auth/types";
|
|
2
3
|
interface LocaleContextType {
|
|
3
4
|
language: string;
|
|
4
5
|
messages: TranslationObject;
|
|
5
6
|
}
|
|
6
7
|
export declare const LocaleContext: import("react").Context<LocaleContextType | undefined>;
|
|
7
|
-
export default function LocationzationClientProvider({ language, messages, children }: {
|
|
8
|
+
export default function LocationzationClientProvider({ language, messages, initialAuthUser, children }: {
|
|
8
9
|
language: string;
|
|
9
10
|
messages: TranslationObject;
|
|
11
|
+
initialAuthUser?: SerializedAuthUser | null;
|
|
10
12
|
children: React.ReactNode;
|
|
11
13
|
}): Component;
|
|
12
14
|
export {};
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_variables";
|
|
4
|
-
import { createContext } from "react";
|
|
4
|
+
import { createContext, useMemo } from "react";
|
|
5
|
+
import dynamic from "next/dynamic";
|
|
6
|
+
import config from "@intl-config";
|
|
5
7
|
export const LocaleContext = createContext(undefined);
|
|
6
|
-
export default function LocationzationClientProvider({ language, messages, children }) {
|
|
8
|
+
export default function LocationzationClientProvider({ language, messages, initialAuthUser = null, children }) {
|
|
7
9
|
setLocaleCache(language);
|
|
8
10
|
setMessageForLocaleCache(language, messages);
|
|
9
|
-
|
|
11
|
+
// `LocaleContext.Provider` stays the outermost element here — the
|
|
12
|
+
// client `AuthUserProvider` (and its descendants calling
|
|
13
|
+
// usePathname()/useLocale()) must render as a CHILD of it, not a
|
|
14
|
+
// sibling wrapping it, or those hooks would throw for running outside
|
|
15
|
+
// the provider.
|
|
16
|
+
let providedChildren = children;
|
|
17
|
+
if (config.firebaseAuth) {
|
|
18
|
+
const AuthUserProvider = dynamic(() => import("../../firebase_auth/client/auth_user_provider"));
|
|
19
|
+
providedChildren = _jsx(AuthUserProvider, { initialUser: initialAuthUser, children: children });
|
|
20
|
+
}
|
|
21
|
+
const contextValue = useMemo(() => ({ language, messages }), [language, messages]);
|
|
22
|
+
return _jsx(LocaleContext.Provider, { value: contextValue, children: providedChildren });
|
|
10
23
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import getCookie from './get_cookie';
|
|
3
|
+
describe('getCookie', () => {
|
|
4
|
+
document.cookie = 'bench-cookie=bar%20baz';
|
|
5
|
+
bench('reads and decodes an existing cookie', () => {
|
|
6
|
+
getCookie('bench-cookie');
|
|
7
|
+
});
|
|
8
|
+
bench('returns null for a missing cookie', () => {
|
|
9
|
+
getCookie('missing-cookie');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import setCookie from './set_cookie';
|
|
3
|
+
describe('setCookie', () => {
|
|
4
|
+
bench('writes a cookie with the default maxAge', () => {
|
|
5
|
+
setCookie({ name: 'theme', value: 'dark' });
|
|
6
|
+
});
|
|
7
|
+
bench('writes a cookie with a custom maxAge', () => {
|
|
8
|
+
setCookie({ name: 'theme', value: 'light', maxAge: 60 });
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @param name Cookie name.
|
|
8
8
|
* @param value Cookie value; stringified via template literal (no encoding
|
|
9
|
-
* applied — avoid values containing `;`).
|
|
9
|
+
* applied — avoid values containing `;`). Must be a primitive
|
|
10
|
+
* (`string`/`number`/`boolean`) — objects would silently serialize to
|
|
11
|
+
* `"[object Object]"`.
|
|
10
12
|
* @param maxAge Seconds until expiry. Defaults to 1 year.
|
|
11
13
|
*
|
|
12
14
|
* Always `path=/; SameSite=Lax`. Swallows errors (e.g. in restrictive
|
|
@@ -14,6 +16,6 @@
|
|
|
14
16
|
*/
|
|
15
17
|
export default function setCookie({ name, value, maxAge }: {
|
|
16
18
|
name: string;
|
|
17
|
-
value:
|
|
19
|
+
value: string | number | boolean;
|
|
18
20
|
maxAge?: number;
|
|
19
21
|
}): void;
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @param name Cookie name.
|
|
9
9
|
* @param value Cookie value; stringified via template literal (no encoding
|
|
10
|
-
* applied — avoid values containing `;`).
|
|
10
|
+
* applied — avoid values containing `;`). Must be a primitive
|
|
11
|
+
* (`string`/`number`/`boolean`) — objects would silently serialize to
|
|
12
|
+
* `"[object Object]"`.
|
|
11
13
|
* @param maxAge Seconds until expiry. Defaults to 1 year.
|
|
12
14
|
*
|
|
13
15
|
* Always `path=/; SameSite=Lax`. Swallows errors (e.g. in restrictive
|
|
@@ -9,6 +9,8 @@ import type { TranslatorReturnType } from "../../types/types";
|
|
|
9
9
|
*
|
|
10
10
|
* @returns The current locale (e.g. `"en"`).
|
|
11
11
|
* @throws If rendered outside `IntlProvider`.
|
|
12
|
+
* @example
|
|
13
|
+
* const locale = useLocale(); // "en"
|
|
12
14
|
*/
|
|
13
15
|
export declare function useLocale(): string;
|
|
14
16
|
/**
|
|
@@ -19,5 +21,8 @@ export declare function useLocale(): string;
|
|
|
19
21
|
* @returns A `(key: string) => string` translation function, memoized on
|
|
20
22
|
* `[language, messages, namespace]`.
|
|
21
23
|
* @throws If rendered outside `IntlProvider`.
|
|
24
|
+
* @example
|
|
25
|
+
* const t = useTranslations("Index");
|
|
26
|
+
* return <h1>{t("title")}</h1>;
|
|
22
27
|
*/
|
|
23
28
|
export declare function useTranslations(namespace: string): TranslatorReturnType;
|
|
@@ -12,11 +12,13 @@ import { getTranslationsImpl } from "../../general/general_functions";
|
|
|
12
12
|
*
|
|
13
13
|
* @returns The current locale (e.g. `"en"`).
|
|
14
14
|
* @throws If rendered outside `IntlProvider`.
|
|
15
|
+
* @example
|
|
16
|
+
* const locale = useLocale(); // "en"
|
|
15
17
|
*/
|
|
16
18
|
export function useLocale() {
|
|
17
19
|
const context = useContext(LocaleContext);
|
|
18
20
|
if (context === undefined) {
|
|
19
|
-
throw new Error('useLocale must be used within
|
|
21
|
+
throw new Error('useLocale must be used within an IntlProvider');
|
|
20
22
|
}
|
|
21
23
|
return context.language;
|
|
22
24
|
}
|
|
@@ -28,11 +30,14 @@ export function useLocale() {
|
|
|
28
30
|
* @returns A `(key: string) => string` translation function, memoized on
|
|
29
31
|
* `[language, messages, namespace]`.
|
|
30
32
|
* @throws If rendered outside `IntlProvider`.
|
|
33
|
+
* @example
|
|
34
|
+
* const t = useTranslations("Index");
|
|
35
|
+
* return <h1>{t("title")}</h1>;
|
|
31
36
|
*/
|
|
32
37
|
export function useTranslations(namespace) {
|
|
33
38
|
const context = useContext(LocaleContext);
|
|
34
39
|
if (context === undefined) {
|
|
35
|
-
throw new Error('useTranslations must be used within
|
|
40
|
+
throw new Error('useTranslations must be used within an IntlProvider');
|
|
36
41
|
}
|
|
37
42
|
const { language, messages } = context;
|
|
38
43
|
return useMemo(() => getTranslationsImpl(language, messages, namespace), [language, messages, namespace]);
|
package/dist/src/client/index.js
CHANGED
|
@@ -2,4 +2,7 @@ export { default as LocaleLink } from './components/locale_link';
|
|
|
2
2
|
export { default as usePathname } from './hooks/use_path_name';
|
|
3
3
|
export { default as setCookieClient } from './functions/set_cookie';
|
|
4
4
|
export { default as getCookieClient } from './functions/get_cookie';
|
|
5
|
+
// Intentionally not re-exported here: useLocale/useTranslations are public
|
|
6
|
+
// via the "cloudflare-next-intl/use" subpath instead, which resolves to the
|
|
7
|
+
// react-server or client implementation automatically via export conditions.
|
|
5
8
|
// export { useLocale, useTranslations } from './hooks/client_hooks';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { localeCookieName, isBotCookieKey, isDarkCookieKey } from './cookie_key';
|
|
3
|
+
describe('cookie_key constants', () => {
|
|
4
|
+
bench('reads the exported constant references', () => {
|
|
5
|
+
void localeCookieName;
|
|
6
|
+
void isBotCookieKey;
|
|
7
|
+
void isDarkCookieKey;
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { setIntlConfig } from './init_config';
|
|
3
|
+
describe('setIntlConfig', () => {
|
|
4
|
+
bench('identity pass-through of a routing config object', () => {
|
|
5
|
+
setIntlConfig({ locales: ['en', 'fr'], defaultLocale: 'en' });
|
|
6
|
+
});
|
|
7
|
+
});
|
|
@@ -5,7 +5,10 @@ function getConfig() {
|
|
|
5
5
|
return intlConfig;
|
|
6
6
|
}
|
|
7
7
|
else {
|
|
8
|
-
throw Error('
|
|
8
|
+
throw Error('cloudflare-next-intl: the `@intl-config` alias is not set. ' +
|
|
9
|
+
'Create a config file (e.g. `src/i18n/intl_config.ts`) that default-exports ' +
|
|
10
|
+
'your `RoutingConfig` (see `setIntlConfig`), then point `@intl-config` at it via ' +
|
|
11
|
+
'your bundler/tsconfig path alias — see the README "Setup" section, step 2.');
|
|
9
12
|
}
|
|
10
13
|
}
|
|
11
14
|
const config = getConfig();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import generateIntlSitemap from './intl_sitemap';
|
|
3
|
+
const smallRoutes = [
|
|
4
|
+
{ link: '/about', lastModified: '2024-01-01' },
|
|
5
|
+
{ link: '/contact', lastModified: '2024-01-01' },
|
|
6
|
+
];
|
|
7
|
+
const largeRoutes = Array.from({ length: 100 }, (_, i) => ({
|
|
8
|
+
link: `/route-${i}`,
|
|
9
|
+
lastModified: '2024-01-01',
|
|
10
|
+
}));
|
|
11
|
+
describe('generateIntlSitemap', () => {
|
|
12
|
+
bench('small site: 2 routes x 2 locales', () => {
|
|
13
|
+
generateIntlSitemap({ intlSitemap: smallRoutes, url: `https://example.com/${Math.random()}` });
|
|
14
|
+
});
|
|
15
|
+
bench('large site: 100 routes x 2 locales', () => {
|
|
16
|
+
generateIntlSitemap({ intlSitemap: largeRoutes, url: `https://example.com/${Math.random()}` });
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -21,7 +21,7 @@ function generateIntlSitemapIml({ intlSitemap, url }) {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
sitemap.sort((a, b) => a.url.
|
|
24
|
+
sitemap.sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
|
|
25
25
|
return sitemap;
|
|
26
26
|
}
|
|
27
27
|
const generateIntlSitemap = cache(generateIntlSitemapIml);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { bench, describe, vi } from 'vitest';
|
|
3
|
+
import intlMiddleware from './middleware';
|
|
4
|
+
import { makeTestRequest } from '../test_utils/mock_next_server';
|
|
5
|
+
describe('intlMiddleware', () => {
|
|
6
|
+
bench('warm path: valid locale cookie present', async () => {
|
|
7
|
+
const req = makeTestRequest('https://example.com/en/page', { cookies: { __user_locale_key__: 'en' } });
|
|
8
|
+
await intlMiddleware(req);
|
|
9
|
+
});
|
|
10
|
+
bench('cold path: no locale cookie, accept-language parsing', async () => {
|
|
11
|
+
const req = makeTestRequest('https://example.com/page', { headers: { 'accept-language': 'de-DE,de;q=0.9' } });
|
|
12
|
+
await intlMiddleware(req);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
vi.doMock('./intl_config', () => ({
|
|
16
|
+
default: {
|
|
17
|
+
locales: ['en'],
|
|
18
|
+
defaultLocale: 'en',
|
|
19
|
+
firebaseAuth: { middlewareEnabled: true, redirectAuthPath: '/login', homePath: '/', isAuthPath: () => true },
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
describe('intlMiddleware with firebaseAuth configured', () => {
|
|
23
|
+
bench('first call: pays the dynamic import of update_session.ts', async () => {
|
|
24
|
+
vi.resetModules();
|
|
25
|
+
const { default: freshIntlMiddleware } = await import('./middleware');
|
|
26
|
+
const req = makeTestRequest('https://example.com/en/page', { cookies: { __user_locale_key__: 'en' } });
|
|
27
|
+
await freshIntlMiddleware(req);
|
|
28
|
+
});
|
|
29
|
+
bench('repeat call: memoized dynamic import (no re-import cost)', async () => {
|
|
30
|
+
const { default: intlMiddlewareWithAuth } = await import('./middleware');
|
|
31
|
+
const req = makeTestRequest('https://example.com/en/page', { cookies: { __user_locale_key__: 'en' } });
|
|
32
|
+
await intlMiddlewareWithAuth(req);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -19,6 +19,12 @@ async function getIsBotValue(userAgent) {
|
|
|
19
19
|
}
|
|
20
20
|
const getIsBotValueCache = cache(getIsBotValue);
|
|
21
21
|
export const localesSet = new Set(config.locales);
|
|
22
|
+
// Memoized after the first call — `import()` is not free even when the
|
|
23
|
+
// module is already in the runtime's module cache (a microtask round-trip
|
|
24
|
+
// plus a Promise allocation), and this middleware runs on every request.
|
|
25
|
+
// Still fully lazy: consumers who never set `config.firebaseAuth` never
|
|
26
|
+
// reach the branch that assigns this, so they never pay the import at all.
|
|
27
|
+
let updateSessionModule;
|
|
22
28
|
/**
|
|
23
29
|
* This middleware function runs for every incoming request. Handles locale
|
|
24
30
|
* detection/routing, then optionally defers to your own custom logic.
|
|
@@ -50,15 +56,23 @@ export default async function intlMiddleware(request, options) {
|
|
|
50
56
|
const { pathname, search, hash } = request.nextUrl;
|
|
51
57
|
let urlLocale;
|
|
52
58
|
let pathWithoutLocale;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
59
|
+
// Avoids split('/').filter(Boolean) array allocation on every request:
|
|
60
|
+
// scan for the first segment's bounds directly.
|
|
61
|
+
const segmentStart = pathname.charCodeAt(0) === 47 /* '/' */ ? 1 : 0;
|
|
62
|
+
let segmentEnd = pathname.indexOf('/', segmentStart);
|
|
63
|
+
if (segmentEnd === -1)
|
|
64
|
+
segmentEnd = pathname.length;
|
|
65
|
+
const languageValue = pathname.slice(segmentStart, segmentEnd);
|
|
56
66
|
// Check if the first segment of the path is one of the supported locales
|
|
57
|
-
if (
|
|
67
|
+
if (languageValue && localesSet.has(languageValue)) {
|
|
58
68
|
urlLocale = languageValue;
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
69
|
+
let rest = pathname.slice(segmentEnd);
|
|
70
|
+
if (rest.endsWith('/'))
|
|
71
|
+
rest = rest.slice(0, -1);
|
|
72
|
+
if (rest.includes('//')) {
|
|
73
|
+
rest = '/' + rest.split('/').filter(Boolean).join('/');
|
|
74
|
+
}
|
|
75
|
+
pathWithoutLocale = rest || '/';
|
|
62
76
|
}
|
|
63
77
|
else {
|
|
64
78
|
// No locale prefix in the URL. The actual pathname is the full original pathname.
|
|
@@ -106,10 +120,27 @@ export default async function intlMiddleware(request, options) {
|
|
|
106
120
|
}
|
|
107
121
|
}
|
|
108
122
|
response.headers.set('Content-Language', effectiveLocaleForRequest);
|
|
123
|
+
response.headers.set('x-pathname', pathWithoutLocale);
|
|
124
|
+
// Auto-wires the firebase_auth submodule's redirect/session-refresh
|
|
125
|
+
// logic when `firebaseAuth` is configured — dynamic import so this
|
|
126
|
+
// file never pulls in firebase_auth/** (and transitively firebase/*)
|
|
127
|
+
// for consumers who never set `firebaseAuth` at all. Runs last, so
|
|
128
|
+
// it composes onto (rather than discards) the locale cookie, bot
|
|
129
|
+
// cookie, and Content-Language header already finalized above.
|
|
130
|
+
// Skipped entirely when a locale redirect is already happening
|
|
131
|
+
// (`isRedirect`) — the locale redirect itself is the response, same
|
|
132
|
+
// as `middlewareHandler` is also skipped on this path by default.
|
|
133
|
+
if (!isRedirect && config.firebaseAuth && config.firebaseAuth.middlewareEnabled !== false) {
|
|
134
|
+
if (!updateSessionModule) {
|
|
135
|
+
updateSessionModule = await import('../firebase_auth/middleware/update_session');
|
|
136
|
+
}
|
|
137
|
+
response = await updateSessionModule.default(request, response, effectiveLocaleForRequest);
|
|
138
|
+
}
|
|
109
139
|
return response;
|
|
110
140
|
}
|
|
111
141
|
catch (e) {
|
|
112
|
-
console.error(
|
|
142
|
+
console.error('[cloudflare-next-intl] intlMiddleware failed and fell back to passing the request through unmodified ' +
|
|
143
|
+
'(no locale rewrite/redirect, no firebase_auth session refresh). Underlying error:', e);
|
|
113
144
|
return NextResponse.next({
|
|
114
145
|
request,
|
|
115
146
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { bench, describe, vi } from 'vitest';
|
|
2
|
+
const fa = {
|
|
3
|
+
apiKey: 'key',
|
|
4
|
+
authDomain: 'domain',
|
|
5
|
+
projectId: 'proj',
|
|
6
|
+
appId: 'app',
|
|
7
|
+
redirectAuthPath: '/login',
|
|
8
|
+
homePath: '/',
|
|
9
|
+
isAuthPath: () => false,
|
|
10
|
+
};
|
|
11
|
+
vi.mock('@intl-config', () => ({ default: { firebaseAuth: fa } }));
|
|
12
|
+
vi.mock('./firebase_client', () => ({
|
|
13
|
+
getFirebaseAuthClient: vi.fn(async () => ({ auth: {} })),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('../error_messages/firebase_auth_error_helper', () => ({
|
|
16
|
+
default: vi.fn(() => 'translated error'),
|
|
17
|
+
}));
|
|
18
|
+
const signInWithEmailAndPassword = vi.fn(async (_auth, _email, _password) => undefined);
|
|
19
|
+
vi.mock('firebase/auth', () => ({
|
|
20
|
+
signInWithEmailAndPassword: (auth, email, password) => signInWithEmailAndPassword(auth, email, password),
|
|
21
|
+
createUserWithEmailAndPassword: vi.fn(async () => undefined),
|
|
22
|
+
sendPasswordResetEmail: vi.fn(async () => undefined),
|
|
23
|
+
}));
|
|
24
|
+
function makeFormData(fields) {
|
|
25
|
+
const fd = new FormData();
|
|
26
|
+
for (const [key, value] of Object.entries(fields))
|
|
27
|
+
fd.set(key, value);
|
|
28
|
+
return fd;
|
|
29
|
+
}
|
|
30
|
+
describe('createLoginAction', () => {
|
|
31
|
+
bench('creates and invokes a login action (successful sign-in)', async () => {
|
|
32
|
+
const { createLoginAction } = await import('./auth_actions');
|
|
33
|
+
const action = createLoginAction('en', {});
|
|
34
|
+
await action({}, makeFormData({ email: 'a@b.com', password: 'pw' }));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AuthActionMessages, AuthFormState } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a login server action for React's `useActionState` form hook.
|
|
4
|
+
* The returned function has the `(prevState, formData) => Promise<AuthFormState>`
|
|
5
|
+
* shape `useActionState` expects — pass it directly as the action.
|
|
6
|
+
*
|
|
7
|
+
* `formData` must contain `email` and `password` fields.
|
|
8
|
+
*
|
|
9
|
+
* @param locale Used to localize the returned error message.
|
|
10
|
+
* @param messages Localized action messages (currently unused by this action).
|
|
11
|
+
* @returns A form action: `{ success: true }` on success, `{ error }` on failure.
|
|
12
|
+
* @example
|
|
13
|
+
* const [state, action] = useActionState(createLoginAction(locale, messages), {});
|
|
14
|
+
* <form action={action}>...</form>
|
|
15
|
+
*/
|
|
16
|
+
export declare function createLoginAction(locale: string, messages: AuthActionMessages): (_prevState: AuthFormState, formData: FormData) => Promise<AuthFormState>;
|
|
17
|
+
/**
|
|
18
|
+
* Builds a sign-up server action for React's `useActionState` form hook.
|
|
19
|
+
* Same shape as {@link createLoginAction}.
|
|
20
|
+
*
|
|
21
|
+
* `formData` must contain `email`, `password`, and `confirmPassword` fields.
|
|
22
|
+
*
|
|
23
|
+
* @param locale Used to localize the returned error message.
|
|
24
|
+
* @param messages `messages.mismatch` is returned as the error if
|
|
25
|
+
* `password` !== `confirmPassword`; other fields on `AuthActionMessages`
|
|
26
|
+
* are not read by this action.
|
|
27
|
+
* @returns A form action: `{ success: true }` on success, `{ error }` on failure.
|
|
28
|
+
* @example
|
|
29
|
+
* const [state, action] = useActionState(createSignUpAction(locale, messages), {});
|
|
30
|
+
* <form action={action}>...</form>
|
|
31
|
+
*/
|
|
32
|
+
export declare function createSignUpAction(locale: string, messages: AuthActionMessages): (_prevState: AuthFormState, formData: FormData) => Promise<AuthFormState>;
|
|
33
|
+
/**
|
|
34
|
+
* Builds a forgot-password server action for React's `useActionState` form
|
|
35
|
+
* hook. Same shape as {@link createLoginAction}.
|
|
36
|
+
*
|
|
37
|
+
* `formData` must contain an `email` field.
|
|
38
|
+
*
|
|
39
|
+
* @param locale Used to localize the returned error message.
|
|
40
|
+
* @param messages Localized action messages (currently unused by this action).
|
|
41
|
+
* @returns A form action: `{ success: true }` on success, `{ error }` on failure.
|
|
42
|
+
* @example
|
|
43
|
+
* const [state, action] = useActionState(createForgotPasswordAction(locale, messages), {});
|
|
44
|
+
* <form action={action}>...</form>
|
|
45
|
+
*/
|
|
46
|
+
export declare function createForgotPasswordAction(locale: string, messages: AuthActionMessages): (_prevState: AuthFormState, formData: FormData) => Promise<AuthFormState>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import config from '@intl-config';
|
|
3
|
+
import requireFirebaseAuthConfig from '../require_config';
|
|
4
|
+
import { getFirebaseAuthClient } from './firebase_client';
|
|
5
|
+
import firebaseAuthErrorMessage from '../error_messages/firebase_auth_error_helper';
|
|
6
|
+
function readCredentials(formData) {
|
|
7
|
+
return {
|
|
8
|
+
email: (formData.get('email')?.toString() ?? '').trim(),
|
|
9
|
+
password: (formData.get('password')?.toString() ?? '').trim(),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds a login server action for React's `useActionState` form hook.
|
|
14
|
+
* The returned function has the `(prevState, formData) => Promise<AuthFormState>`
|
|
15
|
+
* shape `useActionState` expects — pass it directly as the action.
|
|
16
|
+
*
|
|
17
|
+
* `formData` must contain `email` and `password` fields.
|
|
18
|
+
*
|
|
19
|
+
* @param locale Used to localize the returned error message.
|
|
20
|
+
* @param messages Localized action messages (currently unused by this action).
|
|
21
|
+
* @returns A form action: `{ success: true }` on success, `{ error }` on failure.
|
|
22
|
+
* @example
|
|
23
|
+
* const [state, action] = useActionState(createLoginAction(locale, messages), {});
|
|
24
|
+
* <form action={action}>...</form>
|
|
25
|
+
*/
|
|
26
|
+
export function createLoginAction(locale, messages) {
|
|
27
|
+
return async function loginAction(_prevState, formData) {
|
|
28
|
+
requireFirebaseAuthConfig(config.firebaseAuth);
|
|
29
|
+
const { auth } = await getFirebaseAuthClient();
|
|
30
|
+
const { signInWithEmailAndPassword } = await import('firebase/auth');
|
|
31
|
+
const { email, password } = readCredentials(formData);
|
|
32
|
+
try {
|
|
33
|
+
await signInWithEmailAndPassword(auth, email, password);
|
|
34
|
+
return { success: true };
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
return { error: firebaseAuthErrorMessage(locale, e) };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Builds a sign-up server action for React's `useActionState` form hook.
|
|
43
|
+
* Same shape as {@link createLoginAction}.
|
|
44
|
+
*
|
|
45
|
+
* `formData` must contain `email`, `password`, and `confirmPassword` fields.
|
|
46
|
+
*
|
|
47
|
+
* @param locale Used to localize the returned error message.
|
|
48
|
+
* @param messages `messages.mismatch` is returned as the error if
|
|
49
|
+
* `password` !== `confirmPassword`; other fields on `AuthActionMessages`
|
|
50
|
+
* are not read by this action.
|
|
51
|
+
* @returns A form action: `{ success: true }` on success, `{ error }` on failure.
|
|
52
|
+
* @example
|
|
53
|
+
* const [state, action] = useActionState(createSignUpAction(locale, messages), {});
|
|
54
|
+
* <form action={action}>...</form>
|
|
55
|
+
*/
|
|
56
|
+
export function createSignUpAction(locale, messages) {
|
|
57
|
+
return async function signUpAction(_prevState, formData) {
|
|
58
|
+
requireFirebaseAuthConfig(config.firebaseAuth);
|
|
59
|
+
const { auth } = await getFirebaseAuthClient();
|
|
60
|
+
const { createUserWithEmailAndPassword } = await import('firebase/auth');
|
|
61
|
+
const { email, password } = readCredentials(formData);
|
|
62
|
+
const confirmPassword = (formData.get('confirmPassword')?.toString() ?? '').trim();
|
|
63
|
+
if (messages.mismatch && password !== confirmPassword) {
|
|
64
|
+
return { error: messages.mismatch };
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
await createUserWithEmailAndPassword(auth, email, password);
|
|
68
|
+
return { success: true };
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
return { error: firebaseAuthErrorMessage(locale, e) };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Builds a forgot-password server action for React's `useActionState` form
|
|
77
|
+
* hook. Same shape as {@link createLoginAction}.
|
|
78
|
+
*
|
|
79
|
+
* `formData` must contain an `email` field.
|
|
80
|
+
*
|
|
81
|
+
* @param locale Used to localize the returned error message.
|
|
82
|
+
* @param messages Localized action messages (currently unused by this action).
|
|
83
|
+
* @returns A form action: `{ success: true }` on success, `{ error }` on failure.
|
|
84
|
+
* @example
|
|
85
|
+
* const [state, action] = useActionState(createForgotPasswordAction(locale, messages), {});
|
|
86
|
+
* <form action={action}>...</form>
|
|
87
|
+
*/
|
|
88
|
+
export function createForgotPasswordAction(locale, messages) {
|
|
89
|
+
return async function forgotPasswordAction(_prevState, formData) {
|
|
90
|
+
requireFirebaseAuthConfig(config.firebaseAuth);
|
|
91
|
+
const { auth } = await getFirebaseAuthClient();
|
|
92
|
+
const { sendPasswordResetEmail } = await import('firebase/auth');
|
|
93
|
+
const email = (formData.get('email')?.toString() ?? '').trim();
|
|
94
|
+
try {
|
|
95
|
+
await sendPasswordResetEmail(auth, email);
|
|
96
|
+
return { success: true };
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
return { error: firebaseAuthErrorMessage(locale, e) };
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { setAuthUserCache, getAuthUserCache, isAuthUserLoadingCache } from './auth_user_cache';
|
|
3
|
+
describe('auth_user_cache', () => {
|
|
4
|
+
bench('setAuthUserCache + getAuthUserCache round-trip', () => {
|
|
5
|
+
setAuthUserCache({ uid: 'bench-user' });
|
|
6
|
+
getAuthUserCache();
|
|
7
|
+
});
|
|
8
|
+
bench('isAuthUserLoadingCache read', () => {
|
|
9
|
+
isAuthUserLoadingCache();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Module-scope cache so non-React code can read the current client auth
|
|
2
|
+
// user synchronously, without needing to be inside AuthUserProvider's tree.
|
|
3
|
+
let cachedUser = null;
|
|
4
|
+
let cachedLoading = true;
|
|
5
|
+
export function setAuthUserCache(user) {
|
|
6
|
+
cachedUser = user;
|
|
7
|
+
cachedLoading = false;
|
|
8
|
+
}
|
|
9
|
+
export function getAuthUserCache() {
|
|
10
|
+
return cachedUser;
|
|
11
|
+
}
|
|
12
|
+
export function isAuthUserLoadingCache() {
|
|
13
|
+
return cachedLoading;
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AuthUser, SerializedAuthUser } from '../types';
|
|
2
|
+
export interface AuthUserContextType {
|
|
3
|
+
/** Current Firebase user, or `null` if signed out (or not yet resolved while `loading`). */
|
|
4
|
+
user: AuthUser | null;
|
|
5
|
+
/** `true` until the initial auth state has resolved on the client. */
|
|
6
|
+
loading: boolean;
|
|
7
|
+
/** Force-refreshes the current user's ID token/claims and re-syncs the session cookie. */
|
|
8
|
+
reloadUser: () => Promise<void>;
|
|
9
|
+
/** Sends a verification email to the currently signed-in user. */
|
|
10
|
+
sendVerificationEmail: () => Promise<void>;
|
|
11
|
+
/** Signs out, clears the session cookie, and redirects to `firebaseAuth.redirectAuthPath`. */
|
|
12
|
+
logout: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare const AuthUserContext: import("react").Context<AuthUserContextType | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Client-side auth-state provider for `firebase_auth`. Wrap your root layout
|
|
17
|
+
* (or a client boundary below it) with this to make `useAuthUser()`
|
|
18
|
+
* (`cloudflare-next-intl/useFirebaseAuthUser`, client variant) resolve
|
|
19
|
+
* `{ user, loading }` from the live Firebase `onIdTokenChanged` listener,
|
|
20
|
+
* and to get automatic session-cookie sync + redirect-on-sign-out/verify
|
|
21
|
+
* behavior driven by `firebaseAuth.isAuthPath` / `whiteListPaths` /
|
|
22
|
+
* `redirectAuthPath` / `verifyEmailPath` on your `RoutingConfig`.
|
|
23
|
+
*
|
|
24
|
+
* Requires `firebaseAuth` to be set on the config passed to `setIntlConfig`
|
|
25
|
+
* — throws via {@link requireFirebaseAuthConfig} otherwise.
|
|
26
|
+
*
|
|
27
|
+
* @param initialUser Server-resolved user (e.g. from
|
|
28
|
+
* `useFirebaseAuthUser`'s `react-server` variant) to avoid a
|
|
29
|
+
* loading flash on first paint; pass `null`/omit if unavailable.
|
|
30
|
+
* @example
|
|
31
|
+
* <AuthUserProvider initialUser={initialUser}>{children}</AuthUserProvider>
|
|
32
|
+
*/
|
|
33
|
+
export default function AuthUserProvider({ initialUser, children }: {
|
|
34
|
+
initialUser?: SerializedAuthUser | null;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
}): import("react").JSX.Element;
|