cloudflare-next-intl 0.6.34 → 0.7.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/README.md +66 -0
- package/dist/src/client/components/client_provider_static.d.ts +30 -0
- package/dist/src/client/components/client_provider_static.js +33 -0
- package/dist/src/general/cache_variables.bench.js +3 -1
- package/dist/src/general/general_functions.js +66 -3
- package/dist/src/server/components/server_provider_static.d.ts +53 -0
- package/dist/src/server/components/server_provider_static.js +108 -0
- package/dist/src/types/types.d.ts +20 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -96,6 +96,14 @@ export default async function Page() {
|
|
|
96
96
|
}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
`t(key)` always returns a `string`. For a message whose value is an array
|
|
100
|
+
or nested object (e.g. a list), use `t.raw(key)` to get it back as-is:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
const t = await getTranslations("Index");
|
|
104
|
+
const items = t.raw("items") as string[]; // messages.Index.items
|
|
105
|
+
```
|
|
106
|
+
|
|
99
107
|
### Client Components
|
|
100
108
|
|
|
101
109
|
```tsx
|
|
@@ -166,6 +174,64 @@ export default async function RootLayout({ children, params }) {
|
|
|
166
174
|
}
|
|
167
175
|
```
|
|
168
176
|
|
|
177
|
+
### Static export (`output: 'export'`) support
|
|
178
|
+
|
|
179
|
+
The regular `IntlProvider` (`cloudflare-next-intl/serverProvider`, re-exported
|
|
180
|
+
from the root) always has a code path to the firebase-auth client provider,
|
|
181
|
+
which imports a `"use server"` file (`clear_session_action`) for clearing
|
|
182
|
+
the session cookie on logout. Next.js registers a `"use server"` file into
|
|
183
|
+
the server-actions build the moment **any** `import()` in the compiled
|
|
184
|
+
module graph points to it — a runtime `if (config.firebaseAuth)` guard does
|
|
185
|
+
not remove the import *statement*, only skips executing it, so the file is
|
|
186
|
+
still registered even on apps that never configure `firebaseAuth`.
|
|
187
|
+
`output: 'export'` builds fail outright the instant any server action is
|
|
188
|
+
registered anywhere in the app, so this affects every app doing a static
|
|
189
|
+
export, not just ones using auth.
|
|
190
|
+
|
|
191
|
+
If your app is built with `output: 'export'` and does **not** configure
|
|
192
|
+
`firebaseAuth`, use `cloudflare-next-intl/serverProviderStatic` instead —
|
|
193
|
+
same signature (minus `staticSafe`, which only exists to control firebase
|
|
194
|
+
auth's server-side resolution), same locale/messages/cookie-consent
|
|
195
|
+
behavior, but its client provider has zero import anywhere pointing at the
|
|
196
|
+
firebase-auth client code, so there is nothing for Next's scanner to find:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
import { IntlProvider } from "cloudflare-next-intl/serverProviderStatic";
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
It throws at render time if `firebaseAuth` is configured — that combination
|
|
203
|
+
isn't supported by this variant; use the regular `serverProvider` for apps
|
|
204
|
+
that need Firebase Auth (which in turn means you can't statically export
|
|
205
|
+
those routes, per the constraint above).
|
|
206
|
+
|
|
207
|
+
If your app imports the regular `IntlProvider` from the package root and
|
|
208
|
+
can't change that import site (e.g. it's inside another package, or you
|
|
209
|
+
don't want to special-case your own source per build target), redirect it
|
|
210
|
+
at the bundler level instead — alias the **resolved absolute file path**
|
|
211
|
+
(not the package specifier) for both `server_provider.js` and
|
|
212
|
+
`client_provider.js` to their `_static` counterparts, only when building
|
|
213
|
+
for static export:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// next.config.ts
|
|
217
|
+
webpack(config) {
|
|
218
|
+
if (process.env.STATIC_EXPORT === "true") {
|
|
219
|
+
// realpath, not the raw node_modules path — webpack resolves
|
|
220
|
+
// symlinks (e.g. `npm link`) to their real target before matching
|
|
221
|
+
// aliases, so an alias keyed on the symlink path would never match.
|
|
222
|
+
const root = fs.realpathSync(path.resolve(__dirname, "node_modules/cloudflare-next-intl"));
|
|
223
|
+
config.resolve.alias[path.join(root, "dist/src/server/components/server_provider.js")] =
|
|
224
|
+
path.join(root, "dist/src/server/components/server_provider_static.js");
|
|
225
|
+
// client_hooks.ts (useLocale/useTranslations) imports LocaleContext
|
|
226
|
+
// from client_provider.js directly — a second, independent edge to
|
|
227
|
+
// the same firebase-auth chain that the alias above doesn't cover.
|
|
228
|
+
config.resolve.alias[path.join(root, "dist/src/client/components/client_provider.js")] =
|
|
229
|
+
path.join(root, "dist/src/client/components/client_provider_static.js");
|
|
230
|
+
}
|
|
231
|
+
return config;
|
|
232
|
+
},
|
|
233
|
+
```
|
|
234
|
+
|
|
169
235
|
### SEO metadata (hreflang/canonical)
|
|
170
236
|
|
|
171
237
|
```ts
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TranslationObject } from "../../types/types";
|
|
2
|
+
import type { CookieConsentAnalyticsConfig } from "../../types/types";
|
|
3
|
+
import type { CookieConsentDialogProps } from "../../cookie_consent/client/components/cookie_consent_dialog";
|
|
4
|
+
import type { PrivacyPolicyUpdateDialogProps } from "../../cookie_consent/client/components/privacy_policy_update_dialog";
|
|
5
|
+
interface LocaleContextType {
|
|
6
|
+
language: string;
|
|
7
|
+
messages: TranslationObject;
|
|
8
|
+
}
|
|
9
|
+
export declare const LocaleContext: import("react").Context<LocaleContextType | undefined>;
|
|
10
|
+
export default function LocationzationClientProvider({ language, messages, analyticsConfig, requiresConsent, autoWireDialogs, dialogProps, updateDialogProps, children }: {
|
|
11
|
+
language: string;
|
|
12
|
+
messages: TranslationObject;
|
|
13
|
+
/** Resolved server-side from `cookieConsent.analytics`/`getAnalytics` when `autoWireAnalytics` isn't `false`. */
|
|
14
|
+
analyticsConfig?: CookieConsentAnalyticsConfig;
|
|
15
|
+
/**
|
|
16
|
+
* Resolved server-side from `cookieConsent.getCountryCode`/`gdprCountries`.
|
|
17
|
+
* `false` means the visitor's country doesn't require the consent
|
|
18
|
+
* banner — `CookieConsentProvider` seeds consent as implicitly granted
|
|
19
|
+
* for a first-time visitor instead of `null`.
|
|
20
|
+
*/
|
|
21
|
+
requiresConsent?: boolean;
|
|
22
|
+
/** From `cookieConsent.autoWireDialogs` — renders `CookieConsentDialog`/`PrivacyPolicyUpdateDialog` automatically when `true` (default). */
|
|
23
|
+
autoWireDialogs?: boolean;
|
|
24
|
+
/** From `cookieConsent.dialogProps` — forwarded as-is to the auto-wired `CookieConsentDialog`. */
|
|
25
|
+
dialogProps?: CookieConsentDialogProps;
|
|
26
|
+
/** From `cookieConsent.updateDialogProps` — forwarded as-is to the auto-wired `PrivacyPolicyUpdateDialog`. */
|
|
27
|
+
updateDialogProps?: PrivacyPolicyUpdateDialogProps;
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
}): Component;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_variables";
|
|
4
|
+
import { createContext, useMemo } from "react";
|
|
5
|
+
import dynamic from "next/dynamic";
|
|
6
|
+
import config from "@intl-config";
|
|
7
|
+
import installConsoleErrorOverride from "../../error_handling/install_console_error_override";
|
|
8
|
+
import installGlobalErrorOverride from "../../error_handling/install_global_error_override";
|
|
9
|
+
export const LocaleContext = createContext(undefined);
|
|
10
|
+
// `output: 'export'`-safe: unlike `client_provider.tsx`, this file has no
|
|
11
|
+
// import anywhere in it pointing at `firebase_auth/client/auth_user_provider`
|
|
12
|
+
// (which itself pulls in the "use server" `clear_session_action`). Next's
|
|
13
|
+
// server-actions build step registers a "use server" file the moment any
|
|
14
|
+
// `import()` in the compiled module graph points to it, so removing the
|
|
15
|
+
// import entirely — not just skipping its use at runtime — is what keeps
|
|
16
|
+
// `output: 'export'` builds from failing. See `server_provider_static.tsx`
|
|
17
|
+
// for the full explanation.
|
|
18
|
+
const CookieConsentProvider = dynamic(() => import("../../cookie_consent/client/cookie_consent_provider"));
|
|
19
|
+
const CookieConsentAnalytics = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_analytics"));
|
|
20
|
+
const CookieConsentDialog = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_dialog"));
|
|
21
|
+
const PrivacyPolicyUpdateDialog = dynamic(() => import("../../cookie_consent/client/components/privacy_policy_update_dialog"));
|
|
22
|
+
export default function LocationzationClientProvider({ language, messages, analyticsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
|
|
23
|
+
setLocaleCache(language);
|
|
24
|
+
setMessageForLocaleCache(language, messages);
|
|
25
|
+
installConsoleErrorOverride(config, true);
|
|
26
|
+
installGlobalErrorOverride(config);
|
|
27
|
+
let providedChildren = children;
|
|
28
|
+
if (config.cookieConsent) {
|
|
29
|
+
providedChildren = _jsxs(CookieConsentProvider, { requiresConsent: requiresConsent, children: [providedChildren, analyticsConfig && _jsx(CookieConsentAnalytics, { config: analyticsConfig }), autoWireDialogs && _jsx(CookieConsentDialog, { ...dialogProps }), autoWireDialogs && _jsx(PrivacyPolicyUpdateDialog, { ...updateDialogProps })] });
|
|
30
|
+
}
|
|
31
|
+
const contextValue = useMemo(() => ({ language, messages }), [language, messages]);
|
|
32
|
+
return _jsx(LocaleContext.Provider, { value: contextValue, children: providedChildren });
|
|
33
|
+
}
|
|
@@ -13,7 +13,9 @@ describe('cache_variables', () => {
|
|
|
13
13
|
getMessageCache(undefined);
|
|
14
14
|
});
|
|
15
15
|
bench('setTranslationCache + getTranslationCache hit', () => {
|
|
16
|
-
|
|
16
|
+
const fn = (k) => k;
|
|
17
|
+
fn.raw = (k) => k;
|
|
18
|
+
setTranslationCache('en-common', fn);
|
|
17
19
|
getTranslationCache('en-common');
|
|
18
20
|
});
|
|
19
21
|
});
|
|
@@ -18,6 +18,7 @@ const errorAndReturnFallback = (message, cacheKey, locale, namespace, key) => {
|
|
|
18
18
|
].filter(Boolean); // Filter out empty parts
|
|
19
19
|
console.error(parts.join(' | '));
|
|
20
20
|
const fallbackFn = (k) => k; // Fallback function simply returns the key
|
|
21
|
+
fallbackFn.raw = (k) => k;
|
|
21
22
|
setTranslationCache(cacheKey, fallbackFn);
|
|
22
23
|
return fallbackFn;
|
|
23
24
|
};
|
|
@@ -29,10 +30,10 @@ export function getTranslationsImpl(locale, messages, namespace, cacheKey) {
|
|
|
29
30
|
// Traverse the translation object based on the namespace parts.
|
|
30
31
|
for (let i = 0; i < namespaceParts.length; i++) {
|
|
31
32
|
const part = namespaceParts[i];
|
|
32
|
-
const nextLevel = currentLevel[part];
|
|
33
|
+
const nextLevel = Array.isArray(currentLevel) ? undefined : currentLevel[part];
|
|
33
34
|
if (i === namespaceParts.length - 1) {
|
|
34
35
|
// Last part of the namespace, should resolve to an object (the base for translations).
|
|
35
|
-
if (typeof nextLevel === 'object' && nextLevel !== null) {
|
|
36
|
+
if (typeof nextLevel === 'object' && nextLevel !== null && !Array.isArray(nextLevel)) {
|
|
36
37
|
translationsBase = nextLevel;
|
|
37
38
|
}
|
|
38
39
|
else {
|
|
@@ -77,7 +78,7 @@ export function getTranslationsImpl(locale, messages, namespace, cacheKey) {
|
|
|
77
78
|
console.warn(`Translation key "${key}" in namespace "${namespace}" leads to a string prematurely at "${part}" for locale "${locale}".`);
|
|
78
79
|
return key; // Return the key as fallback
|
|
79
80
|
}
|
|
80
|
-
const value = currentTranslation[part];
|
|
81
|
+
const value = Array.isArray(currentTranslation) ? undefined : currentTranslation[part];
|
|
81
82
|
if (i === keyParts.length - 1) {
|
|
82
83
|
if (typeof value !== 'string') {
|
|
83
84
|
console.warn(`Translation key "${key}" in namespace "${namespace}" resolves to a non-string value for locale "${locale}". Expected string, got "${typeof value}".`);
|
|
@@ -104,6 +105,68 @@ export function getTranslationsImpl(locale, messages, namespace, cacheKey) {
|
|
|
104
105
|
console.warn(`Translation key "${key}" in namespace "${namespace}" is missing or not a string for locale "${locale}".`);
|
|
105
106
|
return key; // Return the key as fallback
|
|
106
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* `t.raw(key)` — the escape hatch for when a message value isn't a
|
|
110
|
+
* plain string.
|
|
111
|
+
*
|
|
112
|
+
* `t(key)` (the main `translateFunction` above) ALWAYS returns a
|
|
113
|
+
* `string`; if the value at `key` is an array or a nested object, it
|
|
114
|
+
* warns and falls back to returning `key` itself. Use `t.raw(key)`
|
|
115
|
+
* instead whenever your `messages/<locale>.json` stores a list or
|
|
116
|
+
* object under that key (e.g. a list of social links, a table of
|
|
117
|
+
* FAQ entries, a settings sub-object) — it returns the value exactly
|
|
118
|
+
* as it appears in the JSON, unmodified: string stays string, array
|
|
119
|
+
* stays array, object stays object.
|
|
120
|
+
*
|
|
121
|
+
* Mirrors `next-intl`'s `t.raw` API, so existing `next-intl` knowledge
|
|
122
|
+
* transfers directly.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // messages/en.json: { "Index": { "items": ["a", "b", "c"] } }
|
|
126
|
+
* const t = await getTranslations("Index");
|
|
127
|
+
* const items = t.raw("items") as string[]; // ["a", "b", "c"]
|
|
128
|
+
*
|
|
129
|
+
* @param key Dot-separated path into the resolved namespace, same
|
|
130
|
+
* format as `translateFunction`'s `key` (e.g. `"items"` or
|
|
131
|
+
* `"section.list"`).
|
|
132
|
+
* @returns The raw `TranslationEntry` at `key` (string | object | array),
|
|
133
|
+
* or `key` itself if the path doesn't resolve (missing key, or an
|
|
134
|
+
* intermediate segment isn't an object) — matching `translateFunction`'s
|
|
135
|
+
* fallback-to-key behavior on lookup failure.
|
|
136
|
+
*/
|
|
137
|
+
const rawFunction = (key) => {
|
|
138
|
+
const keyParts = key.split('.');
|
|
139
|
+
let currentTranslation = translationsBase;
|
|
140
|
+
for (let i = 0; i < keyParts.length; i++) {
|
|
141
|
+
const part = keyParts[i];
|
|
142
|
+
if (typeof currentTranslation === 'string' || Array.isArray(currentTranslation)) {
|
|
143
|
+
console.warn(`Translation key "${key}" in namespace "${namespace}" leads to a non-object prematurely at "${part}" for locale "${locale}".`);
|
|
144
|
+
return key;
|
|
145
|
+
}
|
|
146
|
+
const value = currentTranslation[part];
|
|
147
|
+
if (i === keyParts.length - 1) {
|
|
148
|
+
if (value === undefined) {
|
|
149
|
+
console.warn(`Translation key "${key}" in namespace "${namespace}" is missing for locale "${locale}".`);
|
|
150
|
+
return key;
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (typeof value === 'object' && value !== null) {
|
|
156
|
+
currentTranslation = value;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.warn(`Translation key "${key}" in namespace "${namespace}" has invalid structure at "${part}" for locale "${locale}". Expected object, got "${typeof value}".`);
|
|
160
|
+
return key;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return key;
|
|
165
|
+
};
|
|
166
|
+
// Attach `.raw` onto the same callable function object so callers get
|
|
167
|
+
// one value that works both as `t(key)` and `t.raw(key)`, exactly like
|
|
168
|
+
// `next-intl`'s translator shape.
|
|
169
|
+
translateFunction.raw = rawFunction;
|
|
107
170
|
setTranslationCache(cacheKeyValue, translateFunction);
|
|
108
171
|
return translateFunction;
|
|
109
172
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { TranslationObject } from "../../types/types";
|
|
2
|
+
/**
|
|
3
|
+
* `output: 'export'`-safe variant of `IntlProvider`, exported publicly as
|
|
4
|
+
* `cloudflare-next-intl/serverProviderStatic`. Identical to the regular
|
|
5
|
+
* `IntlProvider` (`cloudflare-next-intl/serverProvider`) except it renders
|
|
6
|
+
* `client_provider_static` instead of `client_provider` — a client provider
|
|
7
|
+
* with zero import of `firebase_auth/client/auth_user_provider`, and
|
|
8
|
+
* therefore zero reachability to the "use server" `clear_session_action`
|
|
9
|
+
* file that module pulls in.
|
|
10
|
+
*
|
|
11
|
+
* Next's server-actions build step registers a "use server" file the
|
|
12
|
+
* moment any `import()` in the compiled module graph points to it, even one
|
|
13
|
+
* guarded by a runtime `if` — the guard doesn't remove the import
|
|
14
|
+
* *statement*, only skips executing it. `output: 'export'` builds fail
|
|
15
|
+
* outright the instant any server action is registered anywhere in the app,
|
|
16
|
+
* so a config flag on the regular `IntlProvider` can't fix this: only a
|
|
17
|
+
* provider tree with the import textually absent can. Use this variant on
|
|
18
|
+
* any app built with `output: 'export'` that does not configure
|
|
19
|
+
* `firebaseAuth` — it does not support `firebaseAuth` at all (that config
|
|
20
|
+
* key must be omitted; `AuthUserProvider` is never rendered here regardless
|
|
21
|
+
* of the config value). Regular server-rendered/Cloudflare-Workers apps
|
|
22
|
+
* should keep using `cloudflare-next-intl/serverProvider`.
|
|
23
|
+
*
|
|
24
|
+
* Wrap this around your app once, near the root layout, below `[locale]`.
|
|
25
|
+
* It seeds the server-side locale/message caches (for `getLocale`/`getTranslations`)
|
|
26
|
+
* and also passes them to the client `LocaleContext` (for `useLocale`/`useTranslations`
|
|
27
|
+
* in client components).
|
|
28
|
+
*
|
|
29
|
+
* @param language The current route's locale (typically the `[locale]` route
|
|
30
|
+
* param). Must be one of your configured `locales` — calls `notFound()`
|
|
31
|
+
* otherwise.
|
|
32
|
+
* @param messages Optional pre-loaded messages for `language`. If omitted,
|
|
33
|
+
* they're loaded via `getMessage(language)`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* export default async function RootLayout({ children, params }) {
|
|
38
|
+
* const { locale } = await params;
|
|
39
|
+
* return (
|
|
40
|
+
* <html lang={locale}>
|
|
41
|
+
* <body>
|
|
42
|
+
* <IntlProvider language={locale}>{children}</IntlProvider>
|
|
43
|
+
* </body>
|
|
44
|
+
* </html>
|
|
45
|
+
* );
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export default function LocationzationProvider({ language, messages, children }: {
|
|
50
|
+
language: string;
|
|
51
|
+
messages?: TranslationObject;
|
|
52
|
+
children: React.ReactNode;
|
|
53
|
+
}): Promise<Component>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_variables";
|
|
3
|
+
import { getMessage } from "../functions/server";
|
|
4
|
+
import dynamic from "next/dynamic";
|
|
5
|
+
import { localesSet } from "../../config/middleware";
|
|
6
|
+
import config from "../../config/intl_config";
|
|
7
|
+
import resolveRequiresConsent from "../../cookie_consent/gdpr_countries";
|
|
8
|
+
import installConsoleErrorOverride from "../../error_handling/install_console_error_override";
|
|
9
|
+
import reportError from "../../error_handling/report_error";
|
|
10
|
+
const LocationzationClientProvider = dynamic(() => import("../../client/components/client_provider_static"));
|
|
11
|
+
/**
|
|
12
|
+
* `output: 'export'`-safe variant of `IntlProvider`, exported publicly as
|
|
13
|
+
* `cloudflare-next-intl/serverProviderStatic`. Identical to the regular
|
|
14
|
+
* `IntlProvider` (`cloudflare-next-intl/serverProvider`) except it renders
|
|
15
|
+
* `client_provider_static` instead of `client_provider` — a client provider
|
|
16
|
+
* with zero import of `firebase_auth/client/auth_user_provider`, and
|
|
17
|
+
* therefore zero reachability to the "use server" `clear_session_action`
|
|
18
|
+
* file that module pulls in.
|
|
19
|
+
*
|
|
20
|
+
* Next's server-actions build step registers a "use server" file the
|
|
21
|
+
* moment any `import()` in the compiled module graph points to it, even one
|
|
22
|
+
* guarded by a runtime `if` — the guard doesn't remove the import
|
|
23
|
+
* *statement*, only skips executing it. `output: 'export'` builds fail
|
|
24
|
+
* outright the instant any server action is registered anywhere in the app,
|
|
25
|
+
* so a config flag on the regular `IntlProvider` can't fix this: only a
|
|
26
|
+
* provider tree with the import textually absent can. Use this variant on
|
|
27
|
+
* any app built with `output: 'export'` that does not configure
|
|
28
|
+
* `firebaseAuth` — it does not support `firebaseAuth` at all (that config
|
|
29
|
+
* key must be omitted; `AuthUserProvider` is never rendered here regardless
|
|
30
|
+
* of the config value). Regular server-rendered/Cloudflare-Workers apps
|
|
31
|
+
* should keep using `cloudflare-next-intl/serverProvider`.
|
|
32
|
+
*
|
|
33
|
+
* Wrap this around your app once, near the root layout, below `[locale]`.
|
|
34
|
+
* It seeds the server-side locale/message caches (for `getLocale`/`getTranslations`)
|
|
35
|
+
* and also passes them to the client `LocaleContext` (for `useLocale`/`useTranslations`
|
|
36
|
+
* in client components).
|
|
37
|
+
*
|
|
38
|
+
* @param language The current route's locale (typically the `[locale]` route
|
|
39
|
+
* param). Must be one of your configured `locales` — calls `notFound()`
|
|
40
|
+
* otherwise.
|
|
41
|
+
* @param messages Optional pre-loaded messages for `language`. If omitted,
|
|
42
|
+
* they're loaded via `getMessage(language)`.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* export default async function RootLayout({ children, params }) {
|
|
47
|
+
* const { locale } = await params;
|
|
48
|
+
* return (
|
|
49
|
+
* <html lang={locale}>
|
|
50
|
+
* <body>
|
|
51
|
+
* <IntlProvider language={locale}>{children}</IntlProvider>
|
|
52
|
+
* </body>
|
|
53
|
+
* </html>
|
|
54
|
+
* );
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export default async function LocationzationProvider({ language, messages, children }) {
|
|
59
|
+
if (!localesSet.has(language)) {
|
|
60
|
+
const { notFound } = await import("next/navigation");
|
|
61
|
+
notFound();
|
|
62
|
+
}
|
|
63
|
+
if (config.firebaseAuth) {
|
|
64
|
+
throw new Error('[cloudflare-next-intl] `firebaseAuth` is configured but this route uses ' +
|
|
65
|
+
'`cloudflare-next-intl/serverProviderStatic`, which never renders the firebase-auth ' +
|
|
66
|
+
'client provider (by design — that\'s what keeps output: "export" builds free of the ' +
|
|
67
|
+
'"use server" clear_session_action). Use `cloudflare-next-intl/serverProvider` instead ' +
|
|
68
|
+
'for apps that configure `firebaseAuth`.');
|
|
69
|
+
}
|
|
70
|
+
if (language) {
|
|
71
|
+
setLocaleCache(language);
|
|
72
|
+
}
|
|
73
|
+
if (messages) {
|
|
74
|
+
setMessageForLocaleCache(language, messages);
|
|
75
|
+
}
|
|
76
|
+
const messagesValue = messages ?? await getMessage(language);
|
|
77
|
+
installConsoleErrorOverride(config);
|
|
78
|
+
let analyticsConfig;
|
|
79
|
+
let requiresConsent = true;
|
|
80
|
+
if (config.cookieConsent) {
|
|
81
|
+
const isDevEnvironment = process.env.NODE_ENV === 'development';
|
|
82
|
+
// `getCloudflareContext` under `next dev`'s Cloudflare dev shim can
|
|
83
|
+
// crash the local workerd process (a native RPC panic, not a
|
|
84
|
+
// catchable JS error — see cloudflare/workers-sdk#8687) merely by
|
|
85
|
+
// being called, regardless of what it resolves to. `getCountryCode`
|
|
86
|
+
// is caller-supplied and may be dev-safe, so only skip the
|
|
87
|
+
// `getCloudflareContext` path in dev; fail-safe to `true`
|
|
88
|
+
// (banner shown) same as an unresolved country would.
|
|
89
|
+
requiresConsent = !isDevEnvironment
|
|
90
|
+
? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling)
|
|
91
|
+
: false;
|
|
92
|
+
const analyticsAllowedInEnv = config.cookieConsent.enableAnalyticsInDevMode === true || !isDevEnvironment;
|
|
93
|
+
if (config.cookieConsent.autoWireAnalytics !== false && analyticsAllowedInEnv) {
|
|
94
|
+
if (config.cookieConsent.getAnalytics) {
|
|
95
|
+
try {
|
|
96
|
+
analyticsConfig = await config.cookieConsent.getAnalytics();
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
await reportError(config, { error, classOrMethodName: 'getAnalytics' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
analyticsConfig = config.cookieConsent.analytics;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, analyticsConfig: analyticsConfig, requiresConsent: requiresConsent, autoWireDialogs: config.cookieConsent?.autoWireDialogs !== false, dialogProps: config.cookieConsent?.dialogProps, updateDialogProps: config.cookieConsent?.updateDialogProps, children: children });
|
|
108
|
+
}
|
|
@@ -714,12 +714,30 @@ export interface CookieAttributes {
|
|
|
714
714
|
*/
|
|
715
715
|
secure?: boolean | undefined;
|
|
716
716
|
}
|
|
717
|
-
export type TranslationEntry = string | TranslationObject;
|
|
717
|
+
export type TranslationEntry = string | TranslationObject | TranslationEntry[];
|
|
718
718
|
export interface TranslationObject {
|
|
719
719
|
[key: string]: TranslationEntry;
|
|
720
720
|
}
|
|
721
721
|
export type ReturnType = string;
|
|
722
|
-
export
|
|
722
|
+
export interface TranslatorReturnType {
|
|
723
|
+
/** Looks up `key` and coerces it to a `string`. If the value at `key` isn't a plain string (it's an array or object), this warns and returns `key` itself — use {@link TranslatorReturnType.raw} for those cases instead. */
|
|
724
|
+
(key: string): ReturnType;
|
|
725
|
+
/**
|
|
726
|
+
* Escape hatch for non-string message values. `t(key)` always returns a
|
|
727
|
+
* `string` and can't represent arrays/objects; `t.raw(key)` returns the
|
|
728
|
+
* value exactly as stored in `messages/<locale>.json` — string, array,
|
|
729
|
+
* or nested object, unmodified. Use it whenever a message is a list
|
|
730
|
+
* (e.g. social links, FAQ entries) rather than plain text. Mirrors
|
|
731
|
+
* `next-intl`'s `t.raw`, so existing `next-intl` usage patterns apply
|
|
732
|
+
* as-is.
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* // messages/en.json: { "Index": { "items": ["a", "b", "c"] } }
|
|
736
|
+
* const t = await getTranslations("Index");
|
|
737
|
+
* const items = t.raw("items") as string[]; // ["a", "b", "c"]
|
|
738
|
+
*/
|
|
739
|
+
raw(key: string): TranslationEntry;
|
|
740
|
+
}
|
|
723
741
|
export type changeFrequency = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never' | undefined;
|
|
724
742
|
export type Alternates = {
|
|
725
743
|
languages?: Languages<string> | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudflare-next-intl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Optimized Next Intl Package Special for App Router and Cloudflare",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/src/server/components/server_provider.d.ts",
|
|
37
37
|
"import": "./dist/src/server/components/server_provider.js"
|
|
38
38
|
},
|
|
39
|
+
"./serverProviderStatic": {
|
|
40
|
+
"types": "./dist/src/server/components/server_provider_static.d.ts",
|
|
41
|
+
"import": "./dist/src/server/components/server_provider_static.js"
|
|
42
|
+
},
|
|
39
43
|
"./Link": {
|
|
40
44
|
"types": "./dist/src/server/components/link.d.ts",
|
|
41
45
|
"import": "./dist/src/server/components/link.js"
|