wolmar-i18n 0.1.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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/client/create-client.cjs +87 -0
- package/dist/client/create-client.cjs.map +1 -0
- package/dist/client/create-client.d.cts +57 -0
- package/dist/client/create-client.d.ts +57 -0
- package/dist/client/create-client.js +85 -0
- package/dist/client/create-client.js.map +1 -0
- package/dist/client/index.cjs +4 -0
- package/dist/client/index.d.cts +2 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +3 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/proxy/create-proxy.cjs +42 -0
- package/dist/proxy/create-proxy.cjs.map +1 -0
- package/dist/proxy/create-proxy.d.cts +25 -0
- package/dist/proxy/create-proxy.d.ts +25 -0
- package/dist/proxy/create-proxy.js +41 -0
- package/dist/proxy/create-proxy.js.map +1 -0
- package/dist/proxy/index.cjs +6 -0
- package/dist/proxy/index.d.cts +3 -0
- package/dist/proxy/index.d.ts +3 -0
- package/dist/proxy/index.js +3 -0
- package/dist/proxy/negotiate.cjs +40 -0
- package/dist/proxy/negotiate.cjs.map +1 -0
- package/dist/proxy/negotiate.d.cts +20 -0
- package/dist/proxy/negotiate.d.ts +20 -0
- package/dist/proxy/negotiate.js +39 -0
- package/dist/proxy/negotiate.js.map +1 -0
- package/dist/server/create-server.cjs +35 -0
- package/dist/server/create-server.cjs.map +1 -0
- package/dist/server/create-server.d.cts +32 -0
- package/dist/server/create-server.d.ts +32 -0
- package/dist/server/create-server.js +35 -0
- package/dist/server/create-server.js.map +1 -0
- package/dist/server/index.cjs +3 -0
- package/dist/server/index.d.cts +2 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/types/config.d.cts +76 -0
- package/dist/types/config.d.ts +76 -0
- package/dist/types/messages.d.cts +63 -0
- package/dist/types/messages.d.ts +63 -0
- package/dist/utils/config.cjs +37 -0
- package/dist/utils/config.cjs.map +1 -0
- package/dist/utils/config.d.cts +24 -0
- package/dist/utils/config.d.ts +24 -0
- package/dist/utils/config.js +36 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/dictionary.cjs +20 -0
- package/dist/utils/dictionary.cjs.map +1 -0
- package/dist/utils/dictionary.js +20 -0
- package/dist/utils/dictionary.js.map +1 -0
- package/dist/utils/interpolate.cjs +20 -0
- package/dist/utils/interpolate.cjs.map +1 -0
- package/dist/utils/interpolate.js +20 -0
- package/dist/utils/interpolate.js.map +1 -0
- package/dist/utils/locale.cjs +14 -0
- package/dist/utils/locale.cjs.map +1 -0
- package/dist/utils/locale.js +14 -0
- package/dist/utils/locale.js.map +1 -0
- package/dist/utils/paths.cjs +40 -0
- package/dist/utils/paths.cjs.map +1 -0
- package/dist/utils/paths.d.cts +19 -0
- package/dist/utils/paths.d.ts +19 -0
- package/dist/utils/paths.js +38 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/translator.cjs +54 -0
- package/dist/utils/translator.cjs.map +1 -0
- package/dist/utils/translator.js +54 -0
- package/dist/utils/translator.js.map +1 -0
- package/package.json +95 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tobias Wolmar
|
|
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,158 @@
|
|
|
1
|
+
# wolmar-i18n
|
|
2
|
+
|
|
3
|
+
Internationalization for the **Next.js App Router**. Locale lives in the URL, translations live in
|
|
4
|
+
plain TypeScript objects, and every key is typed.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
t("home.title") // "Welcome"
|
|
8
|
+
t("home.greeting", { name: "Tobias" }) // "Hi, Tobias!" — params are required by the type
|
|
9
|
+
t("home.ttile") // ✗ compile error
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
App Router only. Next.js `>=16` (for `proxy.ts`), React `>=19`, and a `[locale]` segment.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm i wolmar-i18n
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
**1. Dictionaries.** The default locale's file is the schema; the rest must match its shape.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// locales/en.ts
|
|
28
|
+
const dictionary = {
|
|
29
|
+
home: { title: "Welcome", greeting: "Hi, {name}!" },
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export default dictionary;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// locales/da.ts
|
|
37
|
+
import type { MessageSchema } from "wolmar-i18n";
|
|
38
|
+
import type { Dictionary } from "@/i18n/config";
|
|
39
|
+
|
|
40
|
+
const dictionary = {
|
|
41
|
+
home: { title: "Velkommen", greeting: "Hej, {name}!" },
|
|
42
|
+
} as const satisfies MessageSchema<Dictionary>;
|
|
43
|
+
|
|
44
|
+
export default dictionary;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A missing key is a compile error, so half-translated pages cannot ship.
|
|
48
|
+
|
|
49
|
+
**2. Config.**
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// i18n/config.ts
|
|
53
|
+
import { defineI18nConfig } from "wolmar-i18n";
|
|
54
|
+
|
|
55
|
+
export const i18n = defineI18nConfig({ locales: ["en", "da"], defaultLocale: "en" });
|
|
56
|
+
|
|
57
|
+
export const { hasLocale } = i18n;
|
|
58
|
+
export type Dictionary = (typeof import("@/locales/en"))["default"];
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**3. Server and client helpers.**
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// i18n/server.ts
|
|
65
|
+
import "server-only";
|
|
66
|
+
import { createServerI18n } from "wolmar-i18n/server";
|
|
67
|
+
import { i18n, type Dictionary } from "./config";
|
|
68
|
+
|
|
69
|
+
export const { getDictionary, getTranslations } = createServerI18n<
|
|
70
|
+
typeof i18n.locales,
|
|
71
|
+
Dictionary
|
|
72
|
+
>(i18n, {
|
|
73
|
+
en: async () => (await import("@/locales/en")).default,
|
|
74
|
+
da: async () => (await import("@/locales/da")).default,
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// i18n/client.ts
|
|
80
|
+
"use client";
|
|
81
|
+
import { createI18nClient } from "wolmar-i18n/client";
|
|
82
|
+
import { i18n, type Dictionary } from "./config";
|
|
83
|
+
|
|
84
|
+
export const { I18nProvider, useTranslations, useCurrentLocale, useChangeLocale, LocaleLink } =
|
|
85
|
+
createI18nClient<typeof i18n.locales, Dictionary>(i18n);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**4. Proxy**, at the project root next to `app/`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// proxy.ts
|
|
92
|
+
import { createI18nProxy } from "wolmar-i18n/proxy";
|
|
93
|
+
import { i18n } from "@/i18n/config";
|
|
94
|
+
|
|
95
|
+
export const proxy = createI18nProxy(i18n);
|
|
96
|
+
|
|
97
|
+
export const config = {
|
|
98
|
+
matcher: ["/((?!api|_next|.*\\..*|favicon.ico|robots.txt).*)"],
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**5. Layout**, under `app/[locale]/`.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
export const generateStaticParams = i18n.staticParams;
|
|
106
|
+
|
|
107
|
+
export default async function LocaleLayout({ children, params }) {
|
|
108
|
+
const { locale } = await params;
|
|
109
|
+
if (!hasLocale(locale)) notFound();
|
|
110
|
+
|
|
111
|
+
const dictionary = await getDictionary(locale);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<html lang={locale}>
|
|
115
|
+
<body>
|
|
116
|
+
<I18nProvider dictionary={dictionary}>{children}</I18nProvider>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Usage
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
// Server Component
|
|
127
|
+
const t = await getTranslations(locale, "home");
|
|
128
|
+
<h1>{t("title")}</h1>;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// Client Component — no props, at any depth
|
|
133
|
+
"use client";
|
|
134
|
+
const t = useTranslations("home");
|
|
135
|
+
<h1>{t("title")}</h1>;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## API
|
|
139
|
+
|
|
140
|
+
| Entry point | Exports |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `wolmar-i18n` | `defineI18nConfig`, `localizePath`, `firstSegment`, types |
|
|
143
|
+
| `wolmar-i18n/server` | `createServerI18n` → `getDictionary`, `getTranslations` |
|
|
144
|
+
| `wolmar-i18n/client` | `createI18nClient` → `I18nProvider`, `useTranslations`, `useCurrentLocale`, `useChangeLocale`, `LocaleLink` |
|
|
145
|
+
| `wolmar-i18n/proxy` | `createI18nProxy`, `matchLocale`, `parseAcceptLanguage` |
|
|
146
|
+
|
|
147
|
+
The proxy resolves the locale in this order: **cookie → URL prefix → `Accept-Language` → default**.
|
|
148
|
+
A saved choice outranks the link, so a visitor who picked Danish stays on Danish.
|
|
149
|
+
|
|
150
|
+
## Notes
|
|
151
|
+
|
|
152
|
+
- Dictionaries need `as const`, or TypeScript cannot see `{placeholder}` tokens and stops requiring params.
|
|
153
|
+
- A missing key renders as its own dotted path and warns in development only.
|
|
154
|
+
- No plurals, ICU syntax or `Intl` formatters — interpolation is plain `{name}` replacement.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT © Tobias Wolmar
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
exports.__toESM = __toESM;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
3
|
+
const require_paths = require("../utils/paths.cjs");
|
|
4
|
+
const require_translator = require("../utils/translator.cjs");
|
|
5
|
+
let next_link_js = require("next/link.js");
|
|
6
|
+
next_link_js = require_runtime.__toESM(next_link_js);
|
|
7
|
+
let next_navigation_js = require("next/navigation.js");
|
|
8
|
+
let react = require("react");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/client/create-client.tsx
|
|
11
|
+
/**
|
|
12
|
+
* A function to check whether an href should be given a locale prefix.
|
|
13
|
+
* @param href The href to check.
|
|
14
|
+
* @returns True for an app-internal route, false for an absolute URL, a
|
|
15
|
+
* protocol-relative URL, a bare fragment, or a path naming a file.
|
|
16
|
+
*/
|
|
17
|
+
function isLocalizable(href) {
|
|
18
|
+
if (!href.startsWith("/") || href.startsWith("//")) return false;
|
|
19
|
+
const [path = ""] = href.split(/[?#]/);
|
|
20
|
+
return !/\.[a-z0-9]+$/i.test(path);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A function to build the client-side helpers.
|
|
24
|
+
* @param config The config returned by `defineI18nConfig`.
|
|
25
|
+
* @returns The provider, the translation hook, and the locale-aware navigation
|
|
26
|
+
* helpers, all bound to the config.
|
|
27
|
+
* @example
|
|
28
|
+
* "use client";
|
|
29
|
+
* export const { I18nProvider, useTranslations, useChangeLocale, LocaleLink } =
|
|
30
|
+
* createI18nClient<typeof i18n.locales, Dictionary>(i18n);
|
|
31
|
+
*/
|
|
32
|
+
function createI18nClient(config) {
|
|
33
|
+
const I18nContext = (0, react.createContext)(null);
|
|
34
|
+
I18nContext.displayName = "I18nContext";
|
|
35
|
+
function I18nProvider({ dictionary, children }) {
|
|
36
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(I18nContext, {
|
|
37
|
+
value: dictionary,
|
|
38
|
+
children
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function useTranslations(namespace) {
|
|
42
|
+
const dictionary = (0, react.useContext)(I18nContext);
|
|
43
|
+
if (!dictionary) throw new Error("useTranslations must be called below an <I18nProvider>.");
|
|
44
|
+
return (0, react.useMemo)(() => require_translator.createTranslator(dictionary, namespace), [dictionary, namespace]);
|
|
45
|
+
}
|
|
46
|
+
function useCurrentLocale() {
|
|
47
|
+
const segment = require_paths.firstSegment((0, next_navigation_js.usePathname)());
|
|
48
|
+
return config.hasLocale(segment) ? segment : config.defaultLocale;
|
|
49
|
+
}
|
|
50
|
+
function useChangeLocale() {
|
|
51
|
+
const router = (0, next_navigation_js.useRouter)();
|
|
52
|
+
const pathname = (0, next_navigation_js.usePathname)();
|
|
53
|
+
return (0, react.useCallback)((locale) => {
|
|
54
|
+
const secure = window.location.protocol === "https:" ? ";secure" : "";
|
|
55
|
+
document.cookie = `${config.cookieName}=${encodeURIComponent(locale)};path=/;max-age=31536000;samesite=lax${secure}`;
|
|
56
|
+
const target = config.localizePath(config.stripLocale(pathname), locale);
|
|
57
|
+
const { search, hash } = window.location;
|
|
58
|
+
router.push(`${target}${search}${hash}`, { scroll: false });
|
|
59
|
+
}, [pathname, router]);
|
|
60
|
+
}
|
|
61
|
+
function localizeHref(href, locale) {
|
|
62
|
+
if (typeof href === "string") return isLocalizable(href) ? config.localizePath(href, locale) : href;
|
|
63
|
+
if (typeof href.pathname === "string" && isLocalizable(href.pathname)) return {
|
|
64
|
+
...href,
|
|
65
|
+
pathname: config.localizePath(href.pathname, locale)
|
|
66
|
+
};
|
|
67
|
+
return href;
|
|
68
|
+
}
|
|
69
|
+
function LocaleLink({ locale, href, ...rest }) {
|
|
70
|
+
const current = useCurrentLocale();
|
|
71
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(next_link_js.default, {
|
|
72
|
+
href: localizeHref(href, locale ?? current),
|
|
73
|
+
...rest
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
I18nProvider,
|
|
78
|
+
useTranslations,
|
|
79
|
+
useCurrentLocale,
|
|
80
|
+
useChangeLocale,
|
|
81
|
+
LocaleLink
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
exports.createI18nClient = createI18nClient;
|
|
86
|
+
|
|
87
|
+
//# sourceMappingURL=create-client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-client.cjs","names":["createTranslator","firstSegment","Link"],"sources":["../../src/client/create-client.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { createContext, useCallback, useContext, useMemo } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport type {\n I18nConfig,\n LocaleList,\n MessageSchema,\n NamespacePath,\n Scoped,\n Translator,\n} from \"../types\";\nimport { firstSegment } from \"../utils/paths\";\nimport { createTranslator } from \"../utils/translator\";\n\ntype LinkHref = ComponentProps<typeof Link>[\"href\"];\n\nexport type LocaleLinkProps<TLocales extends LocaleList = LocaleList> = Omit<\n ComponentProps<typeof Link>,\n \"locale\"\n> & {\n /**\n * Render the link for a specific locale instead of the current one.\n */\n locale?: TLocales[number];\n};\n\nexport type I18nProviderProps<TDictionary> = {\n /**\n * The dictionary client components read from. Everything passed here is\n * serialized into the RSC payload.\n */\n dictionary: MessageSchema<TDictionary>;\n children: ReactNode;\n};\n\nexport interface I18nClient<TLocales extends LocaleList, TDictionary> {\n /**\n * Supplies the dictionary to every client component below it. The active\n * locale is not passed — `useCurrentLocale` reads it from the URL.\n */\n I18nProvider(props: I18nProviderProps<TDictionary>): ReactNode;\n /**\n * The client `t` function, optionally scoped to a namespace.\n */\n useTranslations<\n TNamespace extends NamespacePath<TDictionary> | undefined = undefined,\n >(\n namespace?: TNamespace,\n ): Translator<Scoped<TDictionary, TNamespace>>;\n /**\n * Reads the active locale off the URL, so it works outside the provider too.\n */\n useCurrentLocale(): TLocales[number];\n /**\n * Returns `(locale) => void`: persists the cookie and swaps the prefix.\n */\n useChangeLocale(): (locale: TLocales[number]) => void;\n /**\n * `next/link` that prefixes every internal href with the active locale.\n */\n LocaleLink(props: LocaleLinkProps<TLocales>): ReactNode;\n}\n\n/**\n * A function to check whether an href should be given a locale prefix.\n * @param href The href to check.\n * @returns True for an app-internal route, false for an absolute URL, a\n * protocol-relative URL, a bare fragment, or a path naming a file.\n */\nfunction isLocalizable(href: string): boolean {\n if (!href.startsWith(\"/\") || href.startsWith(\"//\")) return false;\n\n const [path = \"\"] = href.split(/[?#]/);\n\n return !/\\.[a-z0-9]+$/i.test(path);\n}\n\n/**\n * A function to build the client-side helpers.\n * @param config The config returned by `defineI18nConfig`.\n * @returns The provider, the translation hook, and the locale-aware navigation\n * helpers, all bound to the config.\n * @example\n * \"use client\";\n * export const { I18nProvider, useTranslations, useChangeLocale, LocaleLink } =\n * createI18nClient<typeof i18n.locales, Dictionary>(i18n);\n */\nexport function createI18nClient<TLocales extends LocaleList, TDictionary>(\n config: I18nConfig<TLocales>,\n): I18nClient<TLocales, TDictionary> {\n const I18nContext = createContext<MessageSchema<TDictionary> | null>(null);\n I18nContext.displayName = \"I18nContext\";\n\n function I18nProvider({\n dictionary,\n children,\n }: I18nProviderProps<TDictionary>): ReactNode {\n return <I18nContext value={dictionary}>{children}</I18nContext>;\n }\n\n function useTranslations<\n TNamespace extends NamespacePath<TDictionary> | undefined = undefined,\n >(namespace?: TNamespace): Translator<Scoped<TDictionary, TNamespace>> {\n const dictionary = useContext(I18nContext);\n\n if (!dictionary) {\n throw new Error(\n \"useTranslations must be called below an <I18nProvider>.\",\n );\n }\n\n return useMemo(\n () =>\n createTranslator<Scoped<TDictionary, TNamespace>>(\n dictionary,\n namespace as string | undefined,\n ),\n [dictionary, namespace],\n );\n }\n\n function useCurrentLocale(): TLocales[number] {\n const segment = firstSegment(usePathname());\n\n return config.hasLocale(segment) ? segment : config.defaultLocale;\n }\n\n function useChangeLocale(): (locale: TLocales[number]) => void {\n const router = useRouter();\n const pathname = usePathname();\n\n return useCallback(\n (locale: TLocales[number]) => {\n const secure = window.location.protocol === \"https:\" ? \";secure\" : \"\";\n\n document.cookie = `${config.cookieName}=${encodeURIComponent(\n locale,\n )};path=/;max-age=31536000;samesite=lax${secure}`;\n\n const target = config.localizePath(\n config.stripLocale(pathname),\n locale,\n );\n\n const { search, hash } = window.location;\n\n router.push(`${target}${search}${hash}`, { scroll: false });\n },\n [pathname, router],\n );\n }\n\n function localizeHref(href: LinkHref, locale: TLocales[number]): LinkHref {\n if (typeof href === \"string\") {\n return isLocalizable(href) ? config.localizePath(href, locale) : href;\n }\n\n if (typeof href.pathname === \"string\" && isLocalizable(href.pathname)) {\n return { ...href, pathname: config.localizePath(href.pathname, locale) };\n }\n\n return href;\n }\n\n function LocaleLink({\n locale,\n href,\n ...rest\n }: LocaleLinkProps<TLocales>): ReactNode {\n const current = useCurrentLocale();\n\n return <Link href={localizeHref(href, locale ?? current)} {...rest} />;\n }\n\n return {\n I18nProvider,\n useTranslations,\n useCurrentLocale,\n useChangeLocale,\n LocaleLink,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwEA,SAAS,cAAc,MAAuB;AAC5C,KAAI,CAAC,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW,KAAK,CAAE,QAAO;CAE3D,MAAM,CAAC,OAAO,MAAM,KAAK,MAAM,OAAO;AAEtC,QAAO,CAAC,gBAAgB,KAAK,KAAK;;;;;;;;;;;;AAapC,SAAgB,iBACd,QACmC;CACnC,MAAM,eAAA,GAAA,MAAA,eAA+D,KAAK;AAC1E,aAAY,cAAc;CAE1B,SAAS,aAAa,EACpB,YACA,YAC4C;AAC5C,SAAO,iBAAA,GAAA,kBAAA,KAAC,aAAD;GAAa,OAAO;GAAa;GAAuB,CAAA;;CAGjE,SAAS,gBAEP,WAAqE;EACrE,MAAM,cAAA,GAAA,MAAA,YAAwB,YAAY;AAE1C,MAAI,CAAC,WACH,OAAM,IAAI,MACR,0DACD;AAGH,UAAA,GAAA,MAAA,eAEIA,mBAAAA,iBACE,YACA,UACD,EACH,CAAC,YAAY,UAAU,CACxB;;CAGH,SAAS,mBAAqC;EAC5C,MAAM,UAAUC,cAAAA,cAAAA,GAAAA,mBAAAA,cAA0B,CAAC;AAE3C,SAAO,OAAO,UAAU,QAAQ,GAAG,UAAU,OAAO;;CAGtD,SAAS,kBAAsD;EAC7D,MAAM,UAAA,GAAA,mBAAA,YAAoB;EAC1B,MAAM,YAAA,GAAA,mBAAA,cAAwB;AAE9B,UAAA,GAAA,MAAA,cACG,WAA6B;GAC5B,MAAM,SAAS,OAAO,SAAS,aAAa,WAAW,YAAY;AAEnE,YAAS,SAAS,GAAG,OAAO,WAAW,GAAG,mBACxC,OACD,CAAC,uCAAuC;GAEzC,MAAM,SAAS,OAAO,aACpB,OAAO,YAAY,SAAS,EAC5B,OACD;GAED,MAAM,EAAE,QAAQ,SAAS,OAAO;AAEhC,UAAO,KAAK,GAAG,SAAS,SAAS,QAAQ,EAAE,QAAQ,OAAO,CAAC;KAE7D,CAAC,UAAU,OAAO,CACnB;;CAGH,SAAS,aAAa,MAAgB,QAAoC;AACxE,MAAI,OAAO,SAAS,SAClB,QAAO,cAAc,KAAK,GAAG,OAAO,aAAa,MAAM,OAAO,GAAG;AAGnE,MAAI,OAAO,KAAK,aAAa,YAAY,cAAc,KAAK,SAAS,CACnE,QAAO;GAAE,GAAG;GAAM,UAAU,OAAO,aAAa,KAAK,UAAU,OAAO;GAAE;AAG1E,SAAO;;CAGT,SAAS,WAAW,EAClB,QACA,MACA,GAAG,QACoC;EACvC,MAAM,UAAU,kBAAkB;AAElC,SAAO,iBAAA,GAAA,kBAAA,KAACC,aAAAA,SAAD;GAAM,MAAM,aAAa,MAAM,UAAU,QAAQ;GAAE,GAAI;GAAQ,CAAA;;AAGxE,QAAO;EACL;EACA;EACA;EACA;EACA;EACD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { MessageSchema, NamespacePath, Scoped, Translator } from "../types/messages.cjs";
|
|
2
|
+
import { I18nConfig, LocaleList } from "../types/config.cjs";
|
|
3
|
+
import Link from "next/link.js";
|
|
4
|
+
import { ComponentProps, ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
//#region src/client/create-client.d.ts
|
|
7
|
+
type LocaleLinkProps<TLocales extends LocaleList = LocaleList> = Omit<ComponentProps<typeof Link>, "locale"> & {
|
|
8
|
+
/**
|
|
9
|
+
* Render the link for a specific locale instead of the current one.
|
|
10
|
+
*/
|
|
11
|
+
locale?: TLocales[number];
|
|
12
|
+
};
|
|
13
|
+
type I18nProviderProps<TDictionary> = {
|
|
14
|
+
/**
|
|
15
|
+
* The dictionary client components read from. Everything passed here is
|
|
16
|
+
* serialized into the RSC payload.
|
|
17
|
+
*/
|
|
18
|
+
dictionary: MessageSchema<TDictionary>;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
};
|
|
21
|
+
interface I18nClient<TLocales extends LocaleList, TDictionary> {
|
|
22
|
+
/**
|
|
23
|
+
* Supplies the dictionary to every client component below it. The active
|
|
24
|
+
* locale is not passed — `useCurrentLocale` reads it from the URL.
|
|
25
|
+
*/
|
|
26
|
+
I18nProvider(props: I18nProviderProps<TDictionary>): ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* The client `t` function, optionally scoped to a namespace.
|
|
29
|
+
*/
|
|
30
|
+
useTranslations<TNamespace extends NamespacePath<TDictionary> | undefined = undefined>(namespace?: TNamespace): Translator<Scoped<TDictionary, TNamespace>>;
|
|
31
|
+
/**
|
|
32
|
+
* Reads the active locale off the URL, so it works outside the provider too.
|
|
33
|
+
*/
|
|
34
|
+
useCurrentLocale(): TLocales[number];
|
|
35
|
+
/**
|
|
36
|
+
* Returns `(locale) => void`: persists the cookie and swaps the prefix.
|
|
37
|
+
*/
|
|
38
|
+
useChangeLocale(): (locale: TLocales[number]) => void;
|
|
39
|
+
/**
|
|
40
|
+
* `next/link` that prefixes every internal href with the active locale.
|
|
41
|
+
*/
|
|
42
|
+
LocaleLink(props: LocaleLinkProps<TLocales>): ReactNode;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A function to build the client-side helpers.
|
|
46
|
+
* @param config The config returned by `defineI18nConfig`.
|
|
47
|
+
* @returns The provider, the translation hook, and the locale-aware navigation
|
|
48
|
+
* helpers, all bound to the config.
|
|
49
|
+
* @example
|
|
50
|
+
* "use client";
|
|
51
|
+
* export const { I18nProvider, useTranslations, useChangeLocale, LocaleLink } =
|
|
52
|
+
* createI18nClient<typeof i18n.locales, Dictionary>(i18n);
|
|
53
|
+
*/
|
|
54
|
+
declare function createI18nClient<TLocales extends LocaleList, TDictionary>(config: I18nConfig<TLocales>): I18nClient<TLocales, TDictionary>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { I18nClient, I18nProviderProps, LocaleLinkProps, createI18nClient };
|
|
57
|
+
//# sourceMappingURL=create-client.d.cts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { MessageSchema, NamespacePath, Scoped, Translator } from "../types/messages.js";
|
|
2
|
+
import { I18nConfig, LocaleList } from "../types/config.js";
|
|
3
|
+
import Link from "next/link.js";
|
|
4
|
+
import { ComponentProps, ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
//#region src/client/create-client.d.ts
|
|
7
|
+
type LocaleLinkProps<TLocales extends LocaleList = LocaleList> = Omit<ComponentProps<typeof Link>, "locale"> & {
|
|
8
|
+
/**
|
|
9
|
+
* Render the link for a specific locale instead of the current one.
|
|
10
|
+
*/
|
|
11
|
+
locale?: TLocales[number];
|
|
12
|
+
};
|
|
13
|
+
type I18nProviderProps<TDictionary> = {
|
|
14
|
+
/**
|
|
15
|
+
* The dictionary client components read from. Everything passed here is
|
|
16
|
+
* serialized into the RSC payload.
|
|
17
|
+
*/
|
|
18
|
+
dictionary: MessageSchema<TDictionary>;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
};
|
|
21
|
+
interface I18nClient<TLocales extends LocaleList, TDictionary> {
|
|
22
|
+
/**
|
|
23
|
+
* Supplies the dictionary to every client component below it. The active
|
|
24
|
+
* locale is not passed — `useCurrentLocale` reads it from the URL.
|
|
25
|
+
*/
|
|
26
|
+
I18nProvider(props: I18nProviderProps<TDictionary>): ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* The client `t` function, optionally scoped to a namespace.
|
|
29
|
+
*/
|
|
30
|
+
useTranslations<TNamespace extends NamespacePath<TDictionary> | undefined = undefined>(namespace?: TNamespace): Translator<Scoped<TDictionary, TNamespace>>;
|
|
31
|
+
/**
|
|
32
|
+
* Reads the active locale off the URL, so it works outside the provider too.
|
|
33
|
+
*/
|
|
34
|
+
useCurrentLocale(): TLocales[number];
|
|
35
|
+
/**
|
|
36
|
+
* Returns `(locale) => void`: persists the cookie and swaps the prefix.
|
|
37
|
+
*/
|
|
38
|
+
useChangeLocale(): (locale: TLocales[number]) => void;
|
|
39
|
+
/**
|
|
40
|
+
* `next/link` that prefixes every internal href with the active locale.
|
|
41
|
+
*/
|
|
42
|
+
LocaleLink(props: LocaleLinkProps<TLocales>): ReactNode;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A function to build the client-side helpers.
|
|
46
|
+
* @param config The config returned by `defineI18nConfig`.
|
|
47
|
+
* @returns The provider, the translation hook, and the locale-aware navigation
|
|
48
|
+
* helpers, all bound to the config.
|
|
49
|
+
* @example
|
|
50
|
+
* "use client";
|
|
51
|
+
* export const { I18nProvider, useTranslations, useChangeLocale, LocaleLink } =
|
|
52
|
+
* createI18nClient<typeof i18n.locales, Dictionary>(i18n);
|
|
53
|
+
*/
|
|
54
|
+
declare function createI18nClient<TLocales extends LocaleList, TDictionary>(config: I18nConfig<TLocales>): I18nClient<TLocales, TDictionary>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { I18nClient, I18nProviderProps, LocaleLinkProps, createI18nClient };
|
|
57
|
+
//# sourceMappingURL=create-client.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { firstSegment } from "../utils/paths.js";
|
|
3
|
+
import { createTranslator } from "../utils/translator.js";
|
|
4
|
+
import Link from "next/link.js";
|
|
5
|
+
import { usePathname, useRouter } from "next/navigation.js";
|
|
6
|
+
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
//#region src/client/create-client.tsx
|
|
9
|
+
/**
|
|
10
|
+
* A function to check whether an href should be given a locale prefix.
|
|
11
|
+
* @param href The href to check.
|
|
12
|
+
* @returns True for an app-internal route, false for an absolute URL, a
|
|
13
|
+
* protocol-relative URL, a bare fragment, or a path naming a file.
|
|
14
|
+
*/
|
|
15
|
+
function isLocalizable(href) {
|
|
16
|
+
if (!href.startsWith("/") || href.startsWith("//")) return false;
|
|
17
|
+
const [path = ""] = href.split(/[?#]/);
|
|
18
|
+
return !/\.[a-z0-9]+$/i.test(path);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A function to build the client-side helpers.
|
|
22
|
+
* @param config The config returned by `defineI18nConfig`.
|
|
23
|
+
* @returns The provider, the translation hook, and the locale-aware navigation
|
|
24
|
+
* helpers, all bound to the config.
|
|
25
|
+
* @example
|
|
26
|
+
* "use client";
|
|
27
|
+
* export const { I18nProvider, useTranslations, useChangeLocale, LocaleLink } =
|
|
28
|
+
* createI18nClient<typeof i18n.locales, Dictionary>(i18n);
|
|
29
|
+
*/
|
|
30
|
+
function createI18nClient(config) {
|
|
31
|
+
const I18nContext = createContext(null);
|
|
32
|
+
I18nContext.displayName = "I18nContext";
|
|
33
|
+
function I18nProvider({ dictionary, children }) {
|
|
34
|
+
return /* @__PURE__ */ jsx(I18nContext, {
|
|
35
|
+
value: dictionary,
|
|
36
|
+
children
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function useTranslations(namespace) {
|
|
40
|
+
const dictionary = useContext(I18nContext);
|
|
41
|
+
if (!dictionary) throw new Error("useTranslations must be called below an <I18nProvider>.");
|
|
42
|
+
return useMemo(() => createTranslator(dictionary, namespace), [dictionary, namespace]);
|
|
43
|
+
}
|
|
44
|
+
function useCurrentLocale() {
|
|
45
|
+
const segment = firstSegment(usePathname());
|
|
46
|
+
return config.hasLocale(segment) ? segment : config.defaultLocale;
|
|
47
|
+
}
|
|
48
|
+
function useChangeLocale() {
|
|
49
|
+
const router = useRouter();
|
|
50
|
+
const pathname = usePathname();
|
|
51
|
+
return useCallback((locale) => {
|
|
52
|
+
const secure = window.location.protocol === "https:" ? ";secure" : "";
|
|
53
|
+
document.cookie = `${config.cookieName}=${encodeURIComponent(locale)};path=/;max-age=31536000;samesite=lax${secure}`;
|
|
54
|
+
const target = config.localizePath(config.stripLocale(pathname), locale);
|
|
55
|
+
const { search, hash } = window.location;
|
|
56
|
+
router.push(`${target}${search}${hash}`, { scroll: false });
|
|
57
|
+
}, [pathname, router]);
|
|
58
|
+
}
|
|
59
|
+
function localizeHref(href, locale) {
|
|
60
|
+
if (typeof href === "string") return isLocalizable(href) ? config.localizePath(href, locale) : href;
|
|
61
|
+
if (typeof href.pathname === "string" && isLocalizable(href.pathname)) return {
|
|
62
|
+
...href,
|
|
63
|
+
pathname: config.localizePath(href.pathname, locale)
|
|
64
|
+
};
|
|
65
|
+
return href;
|
|
66
|
+
}
|
|
67
|
+
function LocaleLink({ locale, href, ...rest }) {
|
|
68
|
+
const current = useCurrentLocale();
|
|
69
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
70
|
+
href: localizeHref(href, locale ?? current),
|
|
71
|
+
...rest
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
I18nProvider,
|
|
76
|
+
useTranslations,
|
|
77
|
+
useCurrentLocale,
|
|
78
|
+
useChangeLocale,
|
|
79
|
+
LocaleLink
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { createI18nClient };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=create-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-client.js","names":[],"sources":["../../src/client/create-client.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { createContext, useCallback, useContext, useMemo } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport type {\n I18nConfig,\n LocaleList,\n MessageSchema,\n NamespacePath,\n Scoped,\n Translator,\n} from \"../types\";\nimport { firstSegment } from \"../utils/paths\";\nimport { createTranslator } from \"../utils/translator\";\n\ntype LinkHref = ComponentProps<typeof Link>[\"href\"];\n\nexport type LocaleLinkProps<TLocales extends LocaleList = LocaleList> = Omit<\n ComponentProps<typeof Link>,\n \"locale\"\n> & {\n /**\n * Render the link for a specific locale instead of the current one.\n */\n locale?: TLocales[number];\n};\n\nexport type I18nProviderProps<TDictionary> = {\n /**\n * The dictionary client components read from. Everything passed here is\n * serialized into the RSC payload.\n */\n dictionary: MessageSchema<TDictionary>;\n children: ReactNode;\n};\n\nexport interface I18nClient<TLocales extends LocaleList, TDictionary> {\n /**\n * Supplies the dictionary to every client component below it. The active\n * locale is not passed — `useCurrentLocale` reads it from the URL.\n */\n I18nProvider(props: I18nProviderProps<TDictionary>): ReactNode;\n /**\n * The client `t` function, optionally scoped to a namespace.\n */\n useTranslations<\n TNamespace extends NamespacePath<TDictionary> | undefined = undefined,\n >(\n namespace?: TNamespace,\n ): Translator<Scoped<TDictionary, TNamespace>>;\n /**\n * Reads the active locale off the URL, so it works outside the provider too.\n */\n useCurrentLocale(): TLocales[number];\n /**\n * Returns `(locale) => void`: persists the cookie and swaps the prefix.\n */\n useChangeLocale(): (locale: TLocales[number]) => void;\n /**\n * `next/link` that prefixes every internal href with the active locale.\n */\n LocaleLink(props: LocaleLinkProps<TLocales>): ReactNode;\n}\n\n/**\n * A function to check whether an href should be given a locale prefix.\n * @param href The href to check.\n * @returns True for an app-internal route, false for an absolute URL, a\n * protocol-relative URL, a bare fragment, or a path naming a file.\n */\nfunction isLocalizable(href: string): boolean {\n if (!href.startsWith(\"/\") || href.startsWith(\"//\")) return false;\n\n const [path = \"\"] = href.split(/[?#]/);\n\n return !/\\.[a-z0-9]+$/i.test(path);\n}\n\n/**\n * A function to build the client-side helpers.\n * @param config The config returned by `defineI18nConfig`.\n * @returns The provider, the translation hook, and the locale-aware navigation\n * helpers, all bound to the config.\n * @example\n * \"use client\";\n * export const { I18nProvider, useTranslations, useChangeLocale, LocaleLink } =\n * createI18nClient<typeof i18n.locales, Dictionary>(i18n);\n */\nexport function createI18nClient<TLocales extends LocaleList, TDictionary>(\n config: I18nConfig<TLocales>,\n): I18nClient<TLocales, TDictionary> {\n const I18nContext = createContext<MessageSchema<TDictionary> | null>(null);\n I18nContext.displayName = \"I18nContext\";\n\n function I18nProvider({\n dictionary,\n children,\n }: I18nProviderProps<TDictionary>): ReactNode {\n return <I18nContext value={dictionary}>{children}</I18nContext>;\n }\n\n function useTranslations<\n TNamespace extends NamespacePath<TDictionary> | undefined = undefined,\n >(namespace?: TNamespace): Translator<Scoped<TDictionary, TNamespace>> {\n const dictionary = useContext(I18nContext);\n\n if (!dictionary) {\n throw new Error(\n \"useTranslations must be called below an <I18nProvider>.\",\n );\n }\n\n return useMemo(\n () =>\n createTranslator<Scoped<TDictionary, TNamespace>>(\n dictionary,\n namespace as string | undefined,\n ),\n [dictionary, namespace],\n );\n }\n\n function useCurrentLocale(): TLocales[number] {\n const segment = firstSegment(usePathname());\n\n return config.hasLocale(segment) ? segment : config.defaultLocale;\n }\n\n function useChangeLocale(): (locale: TLocales[number]) => void {\n const router = useRouter();\n const pathname = usePathname();\n\n return useCallback(\n (locale: TLocales[number]) => {\n const secure = window.location.protocol === \"https:\" ? \";secure\" : \"\";\n\n document.cookie = `${config.cookieName}=${encodeURIComponent(\n locale,\n )};path=/;max-age=31536000;samesite=lax${secure}`;\n\n const target = config.localizePath(\n config.stripLocale(pathname),\n locale,\n );\n\n const { search, hash } = window.location;\n\n router.push(`${target}${search}${hash}`, { scroll: false });\n },\n [pathname, router],\n );\n }\n\n function localizeHref(href: LinkHref, locale: TLocales[number]): LinkHref {\n if (typeof href === \"string\") {\n return isLocalizable(href) ? config.localizePath(href, locale) : href;\n }\n\n if (typeof href.pathname === \"string\" && isLocalizable(href.pathname)) {\n return { ...href, pathname: config.localizePath(href.pathname, locale) };\n }\n\n return href;\n }\n\n function LocaleLink({\n locale,\n href,\n ...rest\n }: LocaleLinkProps<TLocales>): ReactNode {\n const current = useCurrentLocale();\n\n return <Link href={localizeHref(href, locale ?? current)} {...rest} />;\n }\n\n return {\n I18nProvider,\n useTranslations,\n useCurrentLocale,\n useChangeLocale,\n LocaleLink,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAwEA,SAAS,cAAc,MAAuB;AAC5C,KAAI,CAAC,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW,KAAK,CAAE,QAAO;CAE3D,MAAM,CAAC,OAAO,MAAM,KAAK,MAAM,OAAO;AAEtC,QAAO,CAAC,gBAAgB,KAAK,KAAK;;;;;;;;;;;;AAapC,SAAgB,iBACd,QACmC;CACnC,MAAM,cAAc,cAAiD,KAAK;AAC1E,aAAY,cAAc;CAE1B,SAAS,aAAa,EACpB,YACA,YAC4C;AAC5C,SAAO,oBAAC,aAAD;GAAa,OAAO;GAAa;GAAuB,CAAA;;CAGjE,SAAS,gBAEP,WAAqE;EACrE,MAAM,aAAa,WAAW,YAAY;AAE1C,MAAI,CAAC,WACH,OAAM,IAAI,MACR,0DACD;AAGH,SAAO,cAEH,iBACE,YACA,UACD,EACH,CAAC,YAAY,UAAU,CACxB;;CAGH,SAAS,mBAAqC;EAC5C,MAAM,UAAU,aAAa,aAAa,CAAC;AAE3C,SAAO,OAAO,UAAU,QAAQ,GAAG,UAAU,OAAO;;CAGtD,SAAS,kBAAsD;EAC7D,MAAM,SAAS,WAAW;EAC1B,MAAM,WAAW,aAAa;AAE9B,SAAO,aACJ,WAA6B;GAC5B,MAAM,SAAS,OAAO,SAAS,aAAa,WAAW,YAAY;AAEnE,YAAS,SAAS,GAAG,OAAO,WAAW,GAAG,mBACxC,OACD,CAAC,uCAAuC;GAEzC,MAAM,SAAS,OAAO,aACpB,OAAO,YAAY,SAAS,EAC5B,OACD;GAED,MAAM,EAAE,QAAQ,SAAS,OAAO;AAEhC,UAAO,KAAK,GAAG,SAAS,SAAS,QAAQ,EAAE,QAAQ,OAAO,CAAC;KAE7D,CAAC,UAAU,OAAO,CACnB;;CAGH,SAAS,aAAa,MAAgB,QAAoC;AACxE,MAAI,OAAO,SAAS,SAClB,QAAO,cAAc,KAAK,GAAG,OAAO,aAAa,MAAM,OAAO,GAAG;AAGnE,MAAI,OAAO,KAAK,aAAa,YAAY,cAAc,KAAK,SAAS,CACnE,QAAO;GAAE,GAAG;GAAM,UAAU,OAAO,aAAa,KAAK,UAAU,OAAO;GAAE;AAG1E,SAAO;;CAGT,SAAS,WAAW,EAClB,QACA,MACA,GAAG,QACoC;EACvC,MAAM,UAAU,kBAAkB;AAElC,SAAO,oBAAC,MAAD;GAAM,MAAM,aAAa,MAAM,UAAU,QAAQ;GAAE,GAAI;GAAQ,CAAA;;AAGxE,QAAO;EACL;EACA;EACA;EACA;EACA;EACD"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_paths = require("./utils/paths.cjs");
|
|
3
|
+
const require_config = require("./utils/config.cjs");
|
|
4
|
+
exports.DEFAULT_COOKIE_NAME = require_config.DEFAULT_COOKIE_NAME;
|
|
5
|
+
exports.defineI18nConfig = require_config.defineI18nConfig;
|
|
6
|
+
exports.firstSegment = require_paths.firstSegment;
|
|
7
|
+
exports.localizePath = require_paths.localizePath;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { MessageArgs, MessageAt, MessagePath, MessageSchema, NamespacePath, Scoped, Translator } from "./types/messages.cjs";
|
|
2
|
+
import { DictionaryLoaders, I18nConfig, I18nOptions, LocaleList, LocaleOf } from "./types/config.cjs";
|
|
3
|
+
import { DEFAULT_COOKIE_NAME, defineI18nConfig } from "./utils/config.cjs";
|
|
4
|
+
import { firstSegment, localizePath } from "./utils/paths.cjs";
|
|
5
|
+
export { DEFAULT_COOKIE_NAME, type DictionaryLoaders, type I18nConfig, type I18nOptions, type LocaleList, type LocaleOf, type MessageArgs, type MessageAt, type MessagePath, type MessageSchema, type NamespacePath, type Scoped, type Translator, defineI18nConfig, firstSegment, localizePath };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { MessageArgs, MessageAt, MessagePath, MessageSchema, NamespacePath, Scoped, Translator } from "./types/messages.js";
|
|
2
|
+
import { DictionaryLoaders, I18nConfig, I18nOptions, LocaleList, LocaleOf } from "./types/config.js";
|
|
3
|
+
import { DEFAULT_COOKIE_NAME, defineI18nConfig } from "./utils/config.js";
|
|
4
|
+
import { firstSegment, localizePath } from "./utils/paths.js";
|
|
5
|
+
export { DEFAULT_COOKIE_NAME, type DictionaryLoaders, type I18nConfig, type I18nOptions, type LocaleList, type LocaleOf, type MessageArgs, type MessageAt, type MessagePath, type MessageSchema, type NamespacePath, type Scoped, type Translator, defineI18nConfig, firstSegment, localizePath };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_paths = require("../utils/paths.cjs");
|
|
3
|
+
const require_negotiate = require("./negotiate.cjs");
|
|
4
|
+
let next_server_js = require("next/server.js");
|
|
5
|
+
//#region src/proxy/create-proxy.ts
|
|
6
|
+
/**
|
|
7
|
+
* A function to build the Next.js proxy that keeps every URL locale-prefixed.
|
|
8
|
+
*
|
|
9
|
+
* A saved locale outranks the link: someone whose cookie says `da` opening
|
|
10
|
+
* `/en/projects` is redirected to `/da/projects`. Choosing a locale is a
|
|
11
|
+
* decision; receiving a link is not.
|
|
12
|
+
*
|
|
13
|
+
* Resolution order: cookie, path prefix, `Accept-Language`, default locale.
|
|
14
|
+
*
|
|
15
|
+
* @param config The config returned by `defineI18nConfig`.
|
|
16
|
+
* @returns The proxy function to export from the app's `proxy.ts`.
|
|
17
|
+
* @example
|
|
18
|
+
* export const proxy = createI18nProxy(i18n);
|
|
19
|
+
* export const config = {
|
|
20
|
+
* matcher: ["/((?!api|_next|.*\\..*|favicon.ico|robots.txt).*)"],
|
|
21
|
+
* };
|
|
22
|
+
*/
|
|
23
|
+
function createI18nProxy(config) {
|
|
24
|
+
return function proxy(request) {
|
|
25
|
+
const { pathname } = request.nextUrl;
|
|
26
|
+
const prefix = require_paths.firstSegment(pathname);
|
|
27
|
+
const isPrefixed = config.hasLocale(prefix);
|
|
28
|
+
const cookie = request.cookies.get(config.cookieName)?.value;
|
|
29
|
+
const locale = (config.hasLocale(cookie) ? cookie : void 0) ?? (isPrefixed ? prefix : void 0) ?? require_negotiate.matchLocale(request.headers.get("accept-language"), config.locales) ?? config.defaultLocale;
|
|
30
|
+
if (isPrefixed && prefix === locale) return next_server_js.NextResponse.next();
|
|
31
|
+
const barePath = isPrefixed ? config.stripLocale(pathname) : pathname;
|
|
32
|
+
const url = request.nextUrl.clone();
|
|
33
|
+
url.pathname = config.localizePath(barePath, locale);
|
|
34
|
+
const response = next_server_js.NextResponse.redirect(url, 307);
|
|
35
|
+
response.headers.set("Vary", "Accept-Language, Cookie");
|
|
36
|
+
return response;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
exports.createI18nProxy = createI18nProxy;
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=create-proxy.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-proxy.cjs","names":["firstSegment","matchLocale","NextResponse"],"sources":["../../src/proxy/create-proxy.ts"],"sourcesContent":["import { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\nimport type { I18nConfig, LocaleList } from \"../types\";\nimport { firstSegment } from \"../utils/paths\";\nimport { matchLocale } from \"./negotiate\";\n\n/**\n * A function to build the Next.js proxy that keeps every URL locale-prefixed.\n *\n * A saved locale outranks the link: someone whose cookie says `da` opening\n * `/en/projects` is redirected to `/da/projects`. Choosing a locale is a\n * decision; receiving a link is not.\n *\n * Resolution order: cookie, path prefix, `Accept-Language`, default locale.\n *\n * @param config The config returned by `defineI18nConfig`.\n * @returns The proxy function to export from the app's `proxy.ts`.\n * @example\n * export const proxy = createI18nProxy(i18n);\n * export const config = {\n * matcher: [\"/((?!api|_next|.*\\\\..*|favicon.ico|robots.txt).*)\"],\n * };\n */\nexport function createI18nProxy<TLocales extends LocaleList>(\n config: I18nConfig<TLocales>,\n): (request: NextRequest) => NextResponse {\n return function proxy(request: NextRequest): NextResponse {\n const { pathname } = request.nextUrl;\n\n const prefix = firstSegment(pathname);\n const isPrefixed = config.hasLocale(prefix);\n\n const cookie = request.cookies.get(config.cookieName)?.value;\n const saved = config.hasLocale(cookie) ? cookie : undefined;\n\n const locale =\n saved ??\n (isPrefixed ? prefix : undefined) ??\n matchLocale(request.headers.get(\"accept-language\"), config.locales) ??\n config.defaultLocale;\n\n if (isPrefixed && prefix === locale) return NextResponse.next();\n\n const barePath = isPrefixed ? config.stripLocale(pathname) : pathname;\n\n const url = request.nextUrl.clone();\n url.pathname = config.localizePath(barePath, locale);\n\n const response = NextResponse.redirect(url, 307);\n\n response.headers.set(\"Vary\", \"Accept-Language, Cookie\");\n\n return response;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,gBACd,QACwC;AACxC,QAAO,SAAS,MAAM,SAAoC;EACxD,MAAM,EAAE,aAAa,QAAQ;EAE7B,MAAM,SAASA,cAAAA,aAAa,SAAS;EACrC,MAAM,aAAa,OAAO,UAAU,OAAO;EAE3C,MAAM,SAAS,QAAQ,QAAQ,IAAI,OAAO,WAAW,EAAE;EAGvD,MAAM,UAFQ,OAAO,UAAU,OAAO,GAAG,SAAS,KAAA,OAI/C,aAAa,SAAS,KAAA,MACvBC,kBAAAA,YAAY,QAAQ,QAAQ,IAAI,kBAAkB,EAAE,OAAO,QAAQ,IACnE,OAAO;AAET,MAAI,cAAc,WAAW,OAAQ,QAAOC,eAAAA,aAAa,MAAM;EAE/D,MAAM,WAAW,aAAa,OAAO,YAAY,SAAS,GAAG;EAE7D,MAAM,MAAM,QAAQ,QAAQ,OAAO;AACnC,MAAI,WAAW,OAAO,aAAa,UAAU,OAAO;EAEpD,MAAM,WAAWA,eAAAA,aAAa,SAAS,KAAK,IAAI;AAEhD,WAAS,QAAQ,IAAI,QAAQ,0BAA0B;AAEvD,SAAO"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { I18nConfig, LocaleList } from "../types/config.cjs";
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server.js";
|
|
3
|
+
|
|
4
|
+
//#region src/proxy/create-proxy.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A function to build the Next.js proxy that keeps every URL locale-prefixed.
|
|
7
|
+
*
|
|
8
|
+
* A saved locale outranks the link: someone whose cookie says `da` opening
|
|
9
|
+
* `/en/projects` is redirected to `/da/projects`. Choosing a locale is a
|
|
10
|
+
* decision; receiving a link is not.
|
|
11
|
+
*
|
|
12
|
+
* Resolution order: cookie, path prefix, `Accept-Language`, default locale.
|
|
13
|
+
*
|
|
14
|
+
* @param config The config returned by `defineI18nConfig`.
|
|
15
|
+
* @returns The proxy function to export from the app's `proxy.ts`.
|
|
16
|
+
* @example
|
|
17
|
+
* export const proxy = createI18nProxy(i18n);
|
|
18
|
+
* export const config = {
|
|
19
|
+
* matcher: ["/((?!api|_next|.*\\..*|favicon.ico|robots.txt).*)"],
|
|
20
|
+
* };
|
|
21
|
+
*/
|
|
22
|
+
declare function createI18nProxy<TLocales extends LocaleList>(config: I18nConfig<TLocales>): (request: NextRequest) => NextResponse;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { createI18nProxy };
|
|
25
|
+
//# sourceMappingURL=create-proxy.d.cts.map
|