@cosmicdrift/kumiko-headless 0.215.7 → 0.217.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.217.0",
|
|
4
4
|
"description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
39
|
+
"@cosmicdrift/kumiko-framework": "0.217.0",
|
|
40
40
|
"temporal-polyfill": "^0.3.2",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import type { LocaleResolver } from "../../contracts";
|
|
3
|
+
import { createLocaleRouter, wrapUrlLocaleResolver } from "../index";
|
|
3
4
|
|
|
4
5
|
type MoneyHorsePage = "home" | "features" | "rechner" | "budget" | "ltv";
|
|
5
6
|
|
|
@@ -76,7 +77,7 @@ describe("createLocaleRouter publicstatus config", () => {
|
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
describe("createLocaleRouter inverted default (website-style)", () => {
|
|
79
|
-
const router = createLocaleRouter({
|
|
80
|
+
const router = createLocaleRouter<"home">({
|
|
80
81
|
defaultLocale: "en",
|
|
81
82
|
prefixedLocales: ["de"],
|
|
82
83
|
prefixFor: () => "/de",
|
|
@@ -157,3 +158,64 @@ describe("createLocaleRouter homePage validation", () => {
|
|
|
157
158
|
expect(() => router.publicPath("features", "de")).toThrow(/no path for page/);
|
|
158
159
|
});
|
|
159
160
|
});
|
|
161
|
+
|
|
162
|
+
function staticBase(locale: string): LocaleResolver {
|
|
163
|
+
let current = locale;
|
|
164
|
+
const listeners = new Set<() => void>();
|
|
165
|
+
return {
|
|
166
|
+
translate: (key) => key,
|
|
167
|
+
locale: () => current,
|
|
168
|
+
timeZone: () => "UTC",
|
|
169
|
+
subscribe: (listener) => {
|
|
170
|
+
listeners.add(listener);
|
|
171
|
+
return () => {
|
|
172
|
+
listeners.delete(listener);
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
setLocale: (next) => {
|
|
176
|
+
current = next;
|
|
177
|
+
for (const listener of listeners) listener();
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
describe("wrapUrlLocaleResolver", () => {
|
|
183
|
+
test("registered path uses detectLang, not base locale", () => {
|
|
184
|
+
const wrapped = wrapUrlLocaleResolver(staticBase("de"), {
|
|
185
|
+
resolvePage: (pathname) => moneyHorseRouter.resolvePage(pathname),
|
|
186
|
+
detectLang: (pathname) => moneyHorseRouter.detectLang(pathname),
|
|
187
|
+
pathname: () => "/en/features",
|
|
188
|
+
});
|
|
189
|
+
expect(wrapped.locale()).toBe("en");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("home path uses URL locale even when base differs", () => {
|
|
193
|
+
const wrapped = wrapUrlLocaleResolver(staticBase("en"), {
|
|
194
|
+
resolvePage: (pathname) => moneyHorseRouter.resolvePage(pathname),
|
|
195
|
+
detectLang: (pathname) => moneyHorseRouter.detectLang(pathname),
|
|
196
|
+
pathname: () => "/",
|
|
197
|
+
});
|
|
198
|
+
expect(wrapped.locale()).toBe("de");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("unregistered path keeps base locale", () => {
|
|
202
|
+
const wrapped = wrapUrlLocaleResolver(staticBase("de"), {
|
|
203
|
+
resolvePage: (pathname) => moneyHorseRouter.resolvePage(pathname),
|
|
204
|
+
detectLang: (pathname) => moneyHorseRouter.detectLang(pathname),
|
|
205
|
+
pathname: () => "/app/credits",
|
|
206
|
+
});
|
|
207
|
+
expect(wrapped.locale()).toBe("de");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("setLocale passes through to base", () => {
|
|
211
|
+
const base = staticBase("de");
|
|
212
|
+
const wrapped = wrapUrlLocaleResolver(base, {
|
|
213
|
+
resolvePage: () => undefined,
|
|
214
|
+
detectLang: () => "en",
|
|
215
|
+
pathname: () => "/app",
|
|
216
|
+
});
|
|
217
|
+
wrapped.setLocale?.("en");
|
|
218
|
+
expect(base.locale()).toBe("en");
|
|
219
|
+
expect(wrapped.locale()).toBe("en");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { LocaleResolver } from "../contracts";
|
|
2
|
+
|
|
1
3
|
export type LocaleRouterConfig<TPage extends string> = {
|
|
2
4
|
/** Canonical default, usually "de". No URL prefix. */
|
|
3
5
|
readonly defaultLocale: string;
|
|
@@ -142,3 +144,38 @@ export function createLocaleRouter<TPage extends string>(
|
|
|
142
144
|
|
|
143
145
|
return { detectLang, publicPath, resolvePage, altLocalePath, sectionAnchor };
|
|
144
146
|
}
|
|
147
|
+
|
|
148
|
+
export type WrapUrlLocaleResolverOptions = {
|
|
149
|
+
resolvePage: (pathname: string) => unknown;
|
|
150
|
+
detectLang: (pathname: string) => string;
|
|
151
|
+
pathname?: () => string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
function defaultBrowserPathname(): string {
|
|
155
|
+
const loc = (globalThis as { location?: { pathname?: string } }).location;
|
|
156
|
+
return typeof loc?.pathname === "string" ? loc.pathname : "/";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Prefer URL-derived locale when `resolvePage` matches the current path;
|
|
161
|
+
* otherwise defer to `base.locale()`. Passes through setLocale and the rest of LocaleResolver.
|
|
162
|
+
*/
|
|
163
|
+
export function wrapUrlLocaleResolver(
|
|
164
|
+
base: LocaleResolver,
|
|
165
|
+
options: WrapUrlLocaleResolverOptions,
|
|
166
|
+
): LocaleResolver {
|
|
167
|
+
const getPathname = options.pathname ?? defaultBrowserPathname;
|
|
168
|
+
return {
|
|
169
|
+
translate: (key, params) => base.translate(key, params),
|
|
170
|
+
timeZone: () => base.timeZone(),
|
|
171
|
+
subscribe: (listener) => base.subscribe(listener),
|
|
172
|
+
setLocale: base.setLocale !== undefined ? (locale) => base.setLocale?.(locale) : undefined,
|
|
173
|
+
locale: () => {
|
|
174
|
+
const path = getPathname();
|
|
175
|
+
if (options.resolvePage(path) !== undefined) {
|
|
176
|
+
return options.detectLang(path);
|
|
177
|
+
}
|
|
178
|
+
return base.locale();
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|