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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +23 -0
  4. package/dist/client/create-client.cjs +87 -0
  5. package/dist/client/create-client.cjs.map +1 -0
  6. package/dist/client/create-client.d.cts +57 -0
  7. package/dist/client/create-client.d.ts +57 -0
  8. package/dist/client/create-client.js +85 -0
  9. package/dist/client/create-client.js.map +1 -0
  10. package/dist/client/index.cjs +4 -0
  11. package/dist/client/index.d.cts +2 -0
  12. package/dist/client/index.d.ts +2 -0
  13. package/dist/client/index.js +3 -0
  14. package/dist/index.cjs +7 -0
  15. package/dist/index.d.cts +5 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.js +3 -0
  18. package/dist/proxy/create-proxy.cjs +42 -0
  19. package/dist/proxy/create-proxy.cjs.map +1 -0
  20. package/dist/proxy/create-proxy.d.cts +25 -0
  21. package/dist/proxy/create-proxy.d.ts +25 -0
  22. package/dist/proxy/create-proxy.js +41 -0
  23. package/dist/proxy/create-proxy.js.map +1 -0
  24. package/dist/proxy/index.cjs +6 -0
  25. package/dist/proxy/index.d.cts +3 -0
  26. package/dist/proxy/index.d.ts +3 -0
  27. package/dist/proxy/index.js +3 -0
  28. package/dist/proxy/negotiate.cjs +40 -0
  29. package/dist/proxy/negotiate.cjs.map +1 -0
  30. package/dist/proxy/negotiate.d.cts +20 -0
  31. package/dist/proxy/negotiate.d.ts +20 -0
  32. package/dist/proxy/negotiate.js +39 -0
  33. package/dist/proxy/negotiate.js.map +1 -0
  34. package/dist/server/create-server.cjs +35 -0
  35. package/dist/server/create-server.cjs.map +1 -0
  36. package/dist/server/create-server.d.cts +32 -0
  37. package/dist/server/create-server.d.ts +32 -0
  38. package/dist/server/create-server.js +35 -0
  39. package/dist/server/create-server.js.map +1 -0
  40. package/dist/server/index.cjs +3 -0
  41. package/dist/server/index.d.cts +2 -0
  42. package/dist/server/index.d.ts +2 -0
  43. package/dist/server/index.js +2 -0
  44. package/dist/types/config.d.cts +76 -0
  45. package/dist/types/config.d.ts +76 -0
  46. package/dist/types/messages.d.cts +63 -0
  47. package/dist/types/messages.d.ts +63 -0
  48. package/dist/utils/config.cjs +37 -0
  49. package/dist/utils/config.cjs.map +1 -0
  50. package/dist/utils/config.d.cts +24 -0
  51. package/dist/utils/config.d.ts +24 -0
  52. package/dist/utils/config.js +36 -0
  53. package/dist/utils/config.js.map +1 -0
  54. package/dist/utils/dictionary.cjs +20 -0
  55. package/dist/utils/dictionary.cjs.map +1 -0
  56. package/dist/utils/dictionary.js +20 -0
  57. package/dist/utils/dictionary.js.map +1 -0
  58. package/dist/utils/interpolate.cjs +20 -0
  59. package/dist/utils/interpolate.cjs.map +1 -0
  60. package/dist/utils/interpolate.js +20 -0
  61. package/dist/utils/interpolate.js.map +1 -0
  62. package/dist/utils/locale.cjs +14 -0
  63. package/dist/utils/locale.cjs.map +1 -0
  64. package/dist/utils/locale.js +14 -0
  65. package/dist/utils/locale.js.map +1 -0
  66. package/dist/utils/paths.cjs +40 -0
  67. package/dist/utils/paths.cjs.map +1 -0
  68. package/dist/utils/paths.d.cts +19 -0
  69. package/dist/utils/paths.d.ts +19 -0
  70. package/dist/utils/paths.js +38 -0
  71. package/dist/utils/paths.js.map +1 -0
  72. package/dist/utils/translator.cjs +54 -0
  73. package/dist/utils/translator.cjs.map +1 -0
  74. package/dist/utils/translator.js +54 -0
  75. package/dist/utils/translator.js.map +1 -0
  76. 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"}
@@ -0,0 +1,4 @@
1
+ "use client";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_create_client = require("./create-client.cjs");
4
+ exports.createI18nClient = require_create_client.createI18nClient;
@@ -0,0 +1,2 @@
1
+ import { I18nClient, I18nProviderProps, LocaleLinkProps, createI18nClient } from "./create-client.cjs";
2
+ export { type I18nClient, type I18nProviderProps, type LocaleLinkProps, createI18nClient };
@@ -0,0 +1,2 @@
1
+ import { I18nClient, I18nProviderProps, LocaleLinkProps, createI18nClient } from "./create-client.js";
2
+ export { type I18nClient, type I18nProviderProps, type LocaleLinkProps, createI18nClient };
@@ -0,0 +1,3 @@
1
+ "use client";
2
+ import { createI18nClient } from "./create-client.js";
3
+ export { createI18nClient };
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;
@@ -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 };
@@ -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,3 @@
1
+ import { firstSegment, localizePath } from "./utils/paths.js";
2
+ import { DEFAULT_COOKIE_NAME, defineI18nConfig } from "./utils/config.js";
3
+ export { DEFAULT_COOKIE_NAME, defineI18nConfig, firstSegment, localizePath };
@@ -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