@cosmicdrift/kumiko-renderer 0.321.0 → 0.322.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-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.322.0",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.322.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.322.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.322.0",
|
|
21
21
|
"react": "^19.2.6",
|
|
22
22
|
"temporal-polyfill": "^0.3.2",
|
|
23
23
|
"zod": "^4.4.3"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"jsdom": "^29.1.1",
|
|
30
30
|
"react-dom": "^19.2.6",
|
|
31
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.322.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
@@ -88,6 +88,34 @@ describe("useTranslation — lookup order", () => {
|
|
|
88
88
|
});
|
|
89
89
|
expect(result.current("greet", { name: "Marc" })).toBe("Hallo Marc!");
|
|
90
90
|
});
|
|
91
|
+
test("resolves a plural entry's CLDR category from a fallback bundle — matches the server's output", () => {
|
|
92
|
+
const bundles: TranslationsByLocale[] = [
|
|
93
|
+
{
|
|
94
|
+
de: {
|
|
95
|
+
"notifications.unread": {
|
|
96
|
+
one: "{count} ungelesene Nachricht",
|
|
97
|
+
other: "{count} ungelesene Nachrichten",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
en: {
|
|
101
|
+
"notifications.unread": {
|
|
102
|
+
one: "{count} unread message",
|
|
103
|
+
other: "{count} unread messages",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
const { result: en } = renderHook(() => useTranslation(), {
|
|
109
|
+
wrapper: wrap(createStaticLocaleResolver({ locale: "en" }), bundles),
|
|
110
|
+
});
|
|
111
|
+
expect(en.current("notifications.unread", { count: 1 })).toBe("1 unread message");
|
|
112
|
+
expect(en.current("notifications.unread", { count: 5 })).toBe("5 unread messages");
|
|
113
|
+
|
|
114
|
+
const { result: de } = renderHook(() => useTranslation(), {
|
|
115
|
+
wrapper: wrap(createStaticLocaleResolver({ locale: "de" }), bundles),
|
|
116
|
+
});
|
|
117
|
+
expect(de.current("notifications.unread", { count: 1 })).toBe("1 ungelesene Nachricht");
|
|
118
|
+
});
|
|
91
119
|
});
|
|
92
120
|
describe("useTranslation — formality", () => {
|
|
93
121
|
const bundles: TranslationsByLocale[] = [
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
// with boolean facets coerced from URL-state strings to real booleans.
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test } from "bun:test";
|
|
10
|
-
import
|
|
11
|
-
EntityDefinition,
|
|
12
|
-
EntityListScreenDefinition,
|
|
13
|
-
ProjectionListScreenDefinition,
|
|
10
|
+
import {
|
|
11
|
+
type EntityDefinition,
|
|
12
|
+
type EntityListScreenDefinition,
|
|
13
|
+
type ProjectionListScreenDefinition,
|
|
14
|
+
translationValueOtherText,
|
|
14
15
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
15
16
|
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
16
17
|
import { act, render, waitFor } from "@testing-library/react";
|
|
@@ -360,8 +361,10 @@ describe("projectionList filter + facets (fw#2224)", () => {
|
|
|
360
361
|
if (en === undefined) throw new Error("missing en default translations");
|
|
361
362
|
const save = de?.["kumiko.actions.save"] ?? en["kumiko.actions.save"];
|
|
362
363
|
const cancel = de?.["kumiko.actions.cancel"] ?? en["kumiko.actions.cancel"];
|
|
363
|
-
expect(facet?.label).toBe(save);
|
|
364
|
-
expect(facet?.options?.[0]?.label).toBe(
|
|
364
|
+
expect(facet?.label).toBe(save === undefined ? undefined : translationValueOtherText(save));
|
|
365
|
+
expect(facet?.options?.[0]?.label).toBe(
|
|
366
|
+
cancel === undefined ? undefined : translationValueOtherText(cancel),
|
|
367
|
+
);
|
|
365
368
|
});
|
|
366
369
|
|
|
367
370
|
test("a reference facet loads its options from the referenced entity's list query, and selecting one sends an id filter", async () => {
|
package/src/i18n.tsx
CHANGED
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
// resolver's `subscribe()` — app code can switch language mid-session
|
|
15
15
|
// without a reload.
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
type Formality,
|
|
19
|
+
formalLocaleTag,
|
|
20
|
+
resolveTranslationValue,
|
|
21
|
+
type TranslationValue,
|
|
22
|
+
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
18
23
|
import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
|
|
19
24
|
import {
|
|
20
25
|
createContext,
|
|
@@ -25,19 +30,22 @@ import {
|
|
|
25
30
|
useSyncExternalStore,
|
|
26
31
|
} from "react";
|
|
27
32
|
|
|
28
|
-
/** Map
|
|
29
|
-
*
|
|
30
|
-
|
|
33
|
+
/** Map of i18n key → TranslationValue. Strings may contain `{name}`
|
|
34
|
+
* placeholders (same semantics as i18next t); plural entries resolve via
|
|
35
|
+
* `resolveTranslationValue`. */
|
|
36
|
+
export type TranslationBundle = Readonly<Record<string, TranslationValue>>;
|
|
31
37
|
|
|
32
38
|
/** Map von Locale-Code (BCP-47, z.B. `"de"`, `"en-US"`) → Bundle. */
|
|
33
39
|
export type TranslationsByLocale = Readonly<Record<string, TranslationBundle>>;
|
|
34
40
|
|
|
35
|
-
/** Key-first shape for `r.translations({ keys })` — each key maps locale →
|
|
36
|
-
export type TranslationsByKey = Readonly<
|
|
41
|
+
/** Key-first shape for `r.translations({ keys })` — each key maps locale → TranslationValue. */
|
|
42
|
+
export type TranslationsByKey = Readonly<
|
|
43
|
+
Record<string, Readonly<Record<string, TranslationValue>>>
|
|
44
|
+
>;
|
|
37
45
|
|
|
38
46
|
/** Pivot key-first server translations to locale-first client bundles. */
|
|
39
47
|
export function translationsByLocaleFromKeys(source: TranslationsByKey): TranslationsByLocale {
|
|
40
|
-
const out: Record<string, Record<string,
|
|
48
|
+
const out: Record<string, Record<string, TranslationValue>> = {};
|
|
41
49
|
for (const [key, byLocale] of Object.entries(source)) {
|
|
42
50
|
for (const [locale, value] of Object.entries(byLocale)) {
|
|
43
51
|
out[locale] ??= {};
|
|
@@ -55,7 +63,7 @@ export function mergeTranslations(
|
|
|
55
63
|
override: TranslationsByLocale,
|
|
56
64
|
): TranslationsByLocale {
|
|
57
65
|
const locales = new Set([...Object.keys(base), ...Object.keys(override)]);
|
|
58
|
-
const merged: Record<string, Record<string,
|
|
66
|
+
const merged: Record<string, Record<string, TranslationValue>> = {};
|
|
59
67
|
for (const locale of locales) {
|
|
60
68
|
merged[locale] = { ...(base[locale] ?? {}), ...(override[locale] ?? {}) };
|
|
61
69
|
}
|
|
@@ -199,7 +207,7 @@ function translateWithFallbacks(
|
|
|
199
207
|
for (const bundle of ctx.fallbackBundles) {
|
|
200
208
|
for (const localeToTry of localesToTry) {
|
|
201
209
|
const value = bundle[localeToTry]?.[key];
|
|
202
|
-
if (value !== undefined) return
|
|
210
|
+
if (value !== undefined) return resolveTranslationValue(value, localeToTry, params);
|
|
203
211
|
}
|
|
204
212
|
}
|
|
205
213
|
}
|
|
@@ -217,14 +225,6 @@ function bundleLocaleTiers(
|
|
|
217
225
|
return [[formalLocaleTag(locale), formalLocaleTag(languageRoot)], plainTier];
|
|
218
226
|
}
|
|
219
227
|
|
|
220
|
-
function interpolate(template: string, params?: Readonly<Record<string, unknown>>): string {
|
|
221
|
-
if (params === undefined) return template;
|
|
222
|
-
return template.replace(/\{(\w+)\}/g, (_, name: string) => {
|
|
223
|
-
const value = params[name];
|
|
224
|
-
return value !== undefined ? String(value) : `{${name}}`;
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
228
|
/** Non-throwing LocaleContext read — undefined outside LocaleProvider.
|
|
229
229
|
* Used by DataTable FormatCell so plain tables without a provider do not crash. */
|
|
230
230
|
export function useOptionalLocale(): string | undefined {
|