@cosmicdrift/kumiko-renderer 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/src/i18n.tsx ADDED
@@ -0,0 +1,158 @@
1
+ // Locale-Handling für React-Consumer des Kumiko-Renderers. Eine dünne
2
+ // Schicht um den platform-agnostischen `LocaleResolver`-Contract aus
3
+ // @cosmicdrift/kumiko-headless: Provider, Hooks, Default-Noop-Resolver und ein
4
+ // Fallback-Bundle-Merge für Feature-gelieferte Translations.
5
+ //
6
+ // Architektur-Idee:
7
+ // 1. App liefert genau einen `LocaleResolver` über `<LocaleProvider>`
8
+ // (oder überhaupt keinen → Default-Resolver returnt keys as-is).
9
+ // 2. Feature-Plugins dürfen Fallback-Bundles mitbringen: wenn der
10
+ // App-Resolver einen Key nicht auflöst, probiert `useTranslation`
11
+ // die Plugin-Bundles. Das hält Feature-UI unabhängig von der
12
+ // App-seitigen i18next-Instanz und funktioniert out-of-the-box,
13
+ // bleibt aber vollständig overridbar.
14
+ // 3. Re-Render bei Locale-Wechsel via `useSyncExternalStore` auf
15
+ // dem Resolver's `subscribe()` — App-Code kann mitten in der
16
+ // Session die Sprache umschalten ohne Reload.
17
+
18
+ import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
19
+ import { createContext, type ReactNode, useContext, useSyncExternalStore } from "react";
20
+
21
+ /** Map von i18n-Key → Template-String. Templates dürfen `{name}`-
22
+ * Platzhalter enthalten — identische Semantik zu i18next-t. */
23
+ export type TranslationBundle = Readonly<Record<string, string>>;
24
+
25
+ /** Map von Locale-Code (BCP-47, z.B. `"de"`, `"en-US"`) → Bundle. */
26
+ export type TranslationsByLocale = Readonly<Record<string, TranslationBundle>>;
27
+
28
+ type LocaleContextValue = {
29
+ readonly resolver: LocaleResolver;
30
+ readonly fallbackBundles: readonly TranslationsByLocale[];
31
+ readonly fallbackLocale: string;
32
+ };
33
+
34
+ const LocaleContext = createContext<LocaleContextValue | undefined>(undefined);
35
+
36
+ export type LocaleProviderProps = {
37
+ readonly resolver: LocaleResolver;
38
+ /** Von Feature-Plugins gelieferte Default-Bundles. Lookup-Reihenfolge
39
+ * pro Key: (1) App-Resolver, (2) diese Bundles in Array-Order,
40
+ * (3) Key as-is. Apps können somit einzelne Keys overriden ohne
41
+ * ganze Feature-Bundles austauschen zu müssen. */
42
+ readonly fallbackBundles?: readonly TranslationsByLocale[];
43
+ /** Auf den fallbackLocale wird zurückgegriffen, wenn weder der
44
+ * current-locale- noch der key-Lookup im Plugin-Bundle greift.
45
+ * Default: `"en"`. */
46
+ readonly fallbackLocale?: string;
47
+ readonly children: ReactNode;
48
+ };
49
+
50
+ export function LocaleProvider({
51
+ resolver,
52
+ fallbackBundles = [],
53
+ fallbackLocale = "en",
54
+ children,
55
+ }: LocaleProviderProps): ReactNode {
56
+ return (
57
+ <LocaleContext.Provider value={{ resolver, fallbackBundles, fallbackLocale }}>
58
+ {children}
59
+ </LocaleContext.Provider>
60
+ );
61
+ }
62
+
63
+ /** Liefert den aktuellen LocaleResolver und abonniert automatisch
64
+ * Locale-Änderungen — der aufrufende Component re-rendert sobald die
65
+ * Sprache gewechselt wird. Wirft wenn kein Provider im Baum ist. */
66
+ export function useLocale(): LocaleResolver {
67
+ const ctx = useContext(LocaleContext);
68
+ if (ctx === undefined) {
69
+ throw new Error("useLocale must be used inside <LocaleProvider>");
70
+ }
71
+ // Subscribe + current locale-snapshot. Wir brauchen den Rückgabewert
72
+ // selbst nicht — wichtig ist nur der re-render-trigger.
73
+ useSyncExternalStore(
74
+ ctx.resolver.subscribe,
75
+ () => ctx.resolver.locale(),
76
+ () => "en",
77
+ );
78
+ return ctx.resolver;
79
+ }
80
+
81
+ /** Primäre API für Feature-UI. `t("key", params)` versucht in dieser
82
+ * Reihenfolge:
83
+ * 1. App-Resolver (z.B. i18next)
84
+ * 2. Plugin-Fallback-Bundles für current-locale
85
+ * 3. Plugin-Fallback-Bundles für fallbackLocale
86
+ * 4. Key as-is
87
+ * Interpolation für Platzhalter `{name}` passiert unabhängig von der
88
+ * Source — auch Fallback-Strings können parameters nutzen. */
89
+ export function useTranslation(): (
90
+ key: string,
91
+ params?: Readonly<Record<string, unknown>>,
92
+ ) => string {
93
+ const ctx = useContext(LocaleContext);
94
+ if (ctx === undefined) {
95
+ throw new Error("useTranslation must be used inside <LocaleProvider>");
96
+ }
97
+ // Re-Render bei Sprach-Wechsel. `ctx.resolver.subscribe` ist bereits
98
+ // eine stable-reference aus dem Resolver, daher hier keine eigene
99
+ // Memoization der Subscribe-Callback nötig.
100
+ useSyncExternalStore(
101
+ ctx.resolver.subscribe,
102
+ () => ctx.resolver.locale(),
103
+ () => "en",
104
+ );
105
+
106
+ return (key, params) => {
107
+ // 1. App-provided resolver zuerst. Convention: wenn der App-Resolver
108
+ // den Key nicht kennt, gibt er den Key zurück — das ist die
109
+ // Fallback-Einladung an Plugin-Bundles. i18next verhält sich
110
+ // exakt so per default.
111
+ const resolved = ctx.resolver.translate(key, params);
112
+ if (resolved !== key) return resolved;
113
+
114
+ // 2. + 3. Plugin-Bundles durchlaufen für current + fallback-locale.
115
+ const currentLocale = ctx.resolver.locale();
116
+ const primaryLookup = currentLocale;
117
+ // `primaryLookup` könnte z.B. "de-AT" sein — in den Bundles stehen
118
+ // oft nur die Language-Roots ("de"). Wir versuchen beide.
119
+ const languageRoot = primaryLookup.split("-")[0] ?? primaryLookup;
120
+ const localesToTry = [primaryLookup, languageRoot, ctx.fallbackLocale];
121
+
122
+ for (const bundle of ctx.fallbackBundles) {
123
+ for (const locale of localesToTry) {
124
+ const value = bundle[locale]?.[key];
125
+ if (value !== undefined) return interpolate(value, params);
126
+ }
127
+ }
128
+
129
+ // 4. Nichts gefunden — key zurück, wie der Default-Resolver auch.
130
+ return key;
131
+ };
132
+ }
133
+
134
+ function interpolate(template: string, params?: Readonly<Record<string, unknown>>): string {
135
+ if (params === undefined) return template;
136
+ return template.replace(/\{(\w+)\}/g, (_, name: string) => {
137
+ const value = params[name];
138
+ return value !== undefined ? String(value) : `{${name}}`;
139
+ });
140
+ }
141
+
142
+ /** Default-Resolver für Apps ohne eigene i18n-Schicht. Gibt jeden Key
143
+ * unverändert zurück — die Plugin-Fallback-Bundles erledigen dann die
144
+ * echte Übersetzung. Nützlich auch als Basis für Tests. */
145
+ export function createStaticLocaleResolver(
146
+ options: { readonly locale?: string; readonly timeZone?: string } = {},
147
+ ): LocaleResolver {
148
+ const locale = options.locale ?? "en";
149
+ const timeZone = options.timeZone ?? "UTC";
150
+ return {
151
+ translate: (key) => key,
152
+ locale: () => locale,
153
+ timeZone: () => timeZone,
154
+ // No-op subscribe: unsere Locale ist statisch, es gibt nie ein
155
+ // Change-Event. Unsubscribe ist ebenfalls no-op.
156
+ subscribe: () => () => {},
157
+ };
158
+ }
package/src/index.ts ADDED
@@ -0,0 +1,104 @@
1
+ // Platform-agnostic React renderer for Kumiko screens. Das ist der
2
+ // Shared-Layer: Components, Hooks, Contexts, Types. Plattform-Impls
3
+ // (Web-DOM, React-Native) leben in den jeweiligen Plattform-Packages
4
+ // und reichen ihre konkreten Primitives/Nav/SSE-Impls via Provider
5
+ // in diesen Baum.
6
+ //
7
+ // Wer diesen Layer direkt konsumiert: andere Renderer-Packages
8
+ // (@cosmicdrift/kumiko-renderer-web, später -native) oder eine App die ihren
9
+ // eigenen Bootstrap schreiben will. Normale Samples gehen über
10
+ // @cosmicdrift/kumiko-renderer-web/createKumikoApp, das alle Provider verdrahtet.
11
+
12
+ export type {
13
+ ColumnRendererComponent,
14
+ ColumnRendererProps,
15
+ ColumnRenderersMap,
16
+ ColumnRenderersProviderProps,
17
+ } from "./app/column-renderers";
18
+ export { ColumnRenderersProvider, useColumnRenderer } from "./app/column-renderers";
19
+ export type { CustomScreensMap, CustomScreensProviderProps } from "./app/custom-screens";
20
+ export { CustomScreensProvider, useCustomScreenComponent } from "./app/custom-screens";
21
+ export type { AppSchema, FeatureSchema, WorkspaceSchema } from "./app/feature-schema";
22
+ export { isAppSchema, toAppSchema } from "./app/feature-schema";
23
+ export type { KumikoScreenProps } from "./app/kumiko-screen";
24
+ export { KumikoScreen, qualifyNavId, qualifyScreenId } from "./app/kumiko-screen";
25
+ export type { NavApi, NavProviderProps, NavRoute, NavTarget } from "./app/nav";
26
+ export { formatPath, NavProvider, parsePath, useNav } from "./app/nav";
27
+ export { lastSegment } from "./app/qn";
28
+ export type { RenderEditProps } from "./components/render-edit";
29
+ export { RenderEdit } from "./components/render-edit";
30
+ export type { RenderFieldProps } from "./components/render-field";
31
+ export { RenderField } from "./components/render-field";
32
+ export type { RenderListProps } from "./components/render-list";
33
+ export { RenderList } from "./components/render-list";
34
+ export type { DispatcherProviderProps } from "./context/dispatcher-context";
35
+ export {
36
+ DispatcherProvider,
37
+ useDispatcher,
38
+ useDispatcherStatus,
39
+ useOptionalDispatcher,
40
+ } from "./context/dispatcher-context";
41
+ export {
42
+ REFERENCE_COMBOBOX_LIMIT,
43
+ REFERENCE_LIST_LOOKUP_LIMIT,
44
+ REFERENCE_SEARCH_DEBOUNCE_MS,
45
+ } from "./hooks/reference-limits";
46
+ export type { UseFormOptions, UseFormResult } from "./hooks/use-form";
47
+ export { useForm } from "./hooks/use-form";
48
+ export type {
49
+ ListSort,
50
+ ListSortDir,
51
+ ListUrlState,
52
+ ListUrlStateApi,
53
+ } from "./hooks/use-list-url-state";
54
+ export { useListUrlState } from "./hooks/use-list-url-state";
55
+ export type { UseQueryOptions, UseQueryResult } from "./hooks/use-query";
56
+ export { useQuery } from "./hooks/use-query";
57
+ export { useStore, useStoreSelector } from "./hooks/use-store";
58
+ export type {
59
+ LocaleProviderProps,
60
+ TranslationBundle,
61
+ TranslationsByLocale,
62
+ } from "./i18n";
63
+ export {
64
+ createStaticLocaleResolver,
65
+ LocaleProvider,
66
+ useLocale,
67
+ useTranslation,
68
+ } from "./i18n";
69
+ export { kumikoDefaultTranslations } from "./i18n-defaults";
70
+ export type {
71
+ AppPrimitives,
72
+ BannerProps,
73
+ ButtonProps,
74
+ CorePrimitives,
75
+ DataTableProps,
76
+ DataTableRowAction,
77
+ DataTableSort,
78
+ DataTableSortDir,
79
+ DialogProps,
80
+ FieldProps,
81
+ FormProps,
82
+ GridCellProps,
83
+ GridProps,
84
+ HeadingProps,
85
+ InputProps,
86
+ PrimitivesProviderProps,
87
+ PrimitivesRegistry,
88
+ SectionProps,
89
+ TextProps,
90
+ } from "./primitives";
91
+ export { PrimitivesProvider, usePrimitives } from "./primitives";
92
+ export type { LiveEvent, LiveEventSubscriber, LiveEventsProviderProps } from "./sse/live-events";
93
+ export { LiveEventsProvider, useLiveEvents } from "./sse/live-events";
94
+ export type {
95
+ AppTokens,
96
+ ColorTokens,
97
+ CoreTokens,
98
+ RadiusTokens,
99
+ ThemeMode,
100
+ Tokens,
101
+ TokensApi,
102
+ TokensProviderProps,
103
+ } from "./tokens";
104
+ export { cssVarTokens, TokensProvider, useTokenController, useTokens } from "./tokens";