@valfuse-node/react 0.2.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/dist/index.mjs ADDED
@@ -0,0 +1,957 @@
1
+ // src/hooks/use-valfuse-form.ts
2
+ import { useMemo as useMemo3 } from "react";
3
+
4
+ // src/hooks/sub-hooks/use-form-core.ts
5
+ import { useState, useRef } from "react";
6
+ function useFormCore(props) {
7
+ const { schema, defaultValues, mode = "onSubmit" } = props;
8
+ const [values, setValues] = useState(
9
+ () => ({ ...defaultValues })
10
+ );
11
+ const [errors, setErrorsState] = useState({});
12
+ const [isSubmitting, setIsSubmitting] = useState(false);
13
+ const [isSubmitted, setIsSubmitted] = useState(false);
14
+ const [isSubmitSuccessful, setIsSubmitSuccessful] = useState(false);
15
+ const [submitCount, setSubmitCount] = useState(0);
16
+ const [touchedFields, setTouchedFields] = useState(
17
+ () => /* @__PURE__ */ new Set()
18
+ );
19
+ const valuesRef = useRef(values);
20
+ valuesRef.current = values;
21
+ const errorsRef = useRef(errors);
22
+ errorsRef.current = errors;
23
+ const touchedFieldsRef = useRef(touchedFields);
24
+ touchedFieldsRef.current = touchedFields;
25
+ const modeRef = useRef(mode);
26
+ modeRef.current = mode;
27
+ const schemaRef = useRef(schema);
28
+ schemaRef.current = schema;
29
+ const watchSubscribersRef = useRef(/* @__PURE__ */ new Set());
30
+ const lastChangedFieldRef = useRef(void 0);
31
+ return {
32
+ schema,
33
+ defaultValues,
34
+ mode,
35
+ values,
36
+ errors,
37
+ isSubmitting,
38
+ isSubmitted,
39
+ isSubmitSuccessful,
40
+ submitCount,
41
+ touchedFields,
42
+ setValues,
43
+ setErrorsState,
44
+ setTouchedFields,
45
+ setIsSubmitting,
46
+ setIsSubmitted,
47
+ setIsSubmitSuccessful,
48
+ setSubmitCount,
49
+ refs: {
50
+ valuesRef,
51
+ errorsRef,
52
+ touchedFieldsRef,
53
+ modeRef,
54
+ schemaRef,
55
+ lastChangedFieldRef,
56
+ watchSubscribersRef
57
+ }
58
+ };
59
+ }
60
+
61
+ // src/hooks/sub-hooks/use-field-validation.ts
62
+ import { useCallback } from "react";
63
+ import { validateSchema } from "@valfuse-node/form";
64
+
65
+ // src/helpers/validation-mode.ts
66
+ function shouldValidateOnChange(mode, isTouched) {
67
+ return mode === "onChange" || mode === "all" || mode === "onTouched" && isTouched;
68
+ }
69
+ function shouldValidateOnBlur(mode) {
70
+ return mode === "onBlur" || mode === "all" || mode === "onTouched";
71
+ }
72
+
73
+ // src/helpers/field-error.ts
74
+ function buildFieldError(fieldErrors, field) {
75
+ const err = fieldErrors[field];
76
+ if (!err) return null;
77
+ return {
78
+ message: err.message,
79
+ type: err.type ?? "validation",
80
+ ...err.code !== void 0 && { code: err.code }
81
+ };
82
+ }
83
+ function mapToFieldErrors(schemaErrors) {
84
+ return Object.fromEntries(
85
+ Object.entries(schemaErrors).map(([key, err]) => {
86
+ const fieldError = {
87
+ message: err.message,
88
+ type: err.type ?? "validation",
89
+ ...err.code !== void 0 && { code: err.code },
90
+ ...err.metadata !== void 0 && { metadata: err.metadata }
91
+ };
92
+ return [key, fieldError];
93
+ })
94
+ );
95
+ }
96
+
97
+ // src/hooks/sub-hooks/use-field-validation.ts
98
+ function useFieldValidation({
99
+ schema,
100
+ setErrorsState
101
+ }) {
102
+ const validateField = useCallback(
103
+ (name, currentValues) => {
104
+ if (!schema[name]) return;
105
+ const raw = validateSchema({ [name]: schema[name] }, currentValues);
106
+ const error = buildFieldError(raw, name);
107
+ setErrorsState((prev) => {
108
+ if (!error && !(name in prev)) return prev;
109
+ const prevError = prev[name];
110
+ if (error && prevError && error.message === prevError.message && error.type === prevError.type && error.code === prevError.code) return prev;
111
+ const next = { ...prev };
112
+ if (error) {
113
+ next[name] = error;
114
+ } else {
115
+ delete next[name];
116
+ }
117
+ return next;
118
+ });
119
+ },
120
+ [schema, setErrorsState]
121
+ );
122
+ const clearStaleFieldError = useCallback(
123
+ (name) => {
124
+ setErrorsState((prev) => {
125
+ if (!(name in prev)) return prev;
126
+ const next = { ...prev };
127
+ delete next[name];
128
+ return next;
129
+ });
130
+ },
131
+ [setErrorsState]
132
+ );
133
+ return { validateField, clearStaleFieldError };
134
+ }
135
+
136
+ // src/hooks/sub-hooks/use-form-registration.ts
137
+ import { useCallback as useCallback2, useMemo } from "react";
138
+ function useFormRegistration({
139
+ core,
140
+ validateField,
141
+ clearStaleFieldError
142
+ }) {
143
+ const { values, refs, touchedFields, setValues, setTouchedFields } = core;
144
+ const valuesRef = refs.valuesRef;
145
+ const modeRef = refs.modeRef;
146
+ const schemaRef = refs.schemaRef;
147
+ const touchedFieldsRef = refs.touchedFieldsRef;
148
+ const lastChangedFieldRef = refs.lastChangedFieldRef;
149
+ const register = useCallback2(
150
+ (name) => ({
151
+ name,
152
+ value: valuesRef.current[name],
153
+ onChange: (e) => {
154
+ const rawValue = e.target.value;
155
+ const fieldTransform = schemaRef.current[name]?.transform;
156
+ const newValue = fieldTransform ? fieldTransform(rawValue) : rawValue;
157
+ const updated = { ...valuesRef.current, [name]: newValue };
158
+ lastChangedFieldRef.current = name;
159
+ setValues(updated);
160
+ if (shouldValidateOnChange(modeRef.current, touchedFieldsRef.current.has(name))) {
161
+ validateField(name, updated);
162
+ } else {
163
+ clearStaleFieldError(name);
164
+ }
165
+ },
166
+ onBlur: () => {
167
+ setTouchedFields((prev) => {
168
+ if (prev.has(name)) return prev;
169
+ const next = new Set(prev);
170
+ next.add(name);
171
+ return next;
172
+ });
173
+ if (shouldValidateOnBlur(modeRef.current)) {
174
+ const current = valuesRef.current;
175
+ const fieldTransform = schemaRef.current[name]?.transform;
176
+ const forValidation = fieldTransform ? { ...current, [name]: fieldTransform(current[name]) } : current;
177
+ validateField(name, forValidation);
178
+ }
179
+ }
180
+ }),
181
+ // mode/touchedFields/schema read via refs — only stable deps here.
182
+ [validateField, clearStaleFieldError]
183
+ );
184
+ const _updateField = useCallback2(
185
+ (name, value) => {
186
+ const fieldTransform = schemaRef.current[name]?.transform;
187
+ const transformedValue = fieldTransform ? fieldTransform(value) : value;
188
+ const updated = { ...valuesRef.current, [name]: transformedValue };
189
+ lastChangedFieldRef.current = name;
190
+ setValues(updated);
191
+ if (shouldValidateOnChange(modeRef.current, touchedFieldsRef.current.has(name))) {
192
+ validateField(name, updated);
193
+ } else {
194
+ clearStaleFieldError(name);
195
+ }
196
+ },
197
+ // schema/mode/touchedFields/values read via refs.
198
+ [validateField, clearStaleFieldError]
199
+ );
200
+ const _touchField = useCallback2(
201
+ (name) => {
202
+ setTouchedFields((prev) => {
203
+ if (prev.has(name)) return prev;
204
+ const next = new Set(prev);
205
+ next.add(name);
206
+ return next;
207
+ });
208
+ if (shouldValidateOnBlur(modeRef.current)) {
209
+ const current = valuesRef.current;
210
+ const fieldTransform = schemaRef.current[name]?.transform;
211
+ const forValidation = fieldTransform ? { ...current, [name]: fieldTransform(current[name]) } : current;
212
+ validateField(name, forValidation);
213
+ }
214
+ },
215
+ // schema/mode/values read via refs — only validateField is a dep.
216
+ [validateField]
217
+ );
218
+ const control = useMemo(
219
+ () => ({
220
+ _values: values,
221
+ _errors: core.errors,
222
+ _updateField,
223
+ _touchField,
224
+ _touchedFields: touchedFields
225
+ }),
226
+ [values, core.errors, touchedFields, _updateField, _touchField]
227
+ );
228
+ return { register, control, _updateField, _touchField };
229
+ }
230
+
231
+ // src/hooks/sub-hooks/use-form-watch.ts
232
+ import { useCallback as useCallback3, useEffect } from "react";
233
+ function useFormWatch(core) {
234
+ const { watchSubscribersRef, lastChangedFieldRef } = core.refs;
235
+ useEffect(() => {
236
+ if (watchSubscribersRef.current.size > 0) {
237
+ const info = {
238
+ name: lastChangedFieldRef.current,
239
+ type: lastChangedFieldRef.current ? "change" : void 0
240
+ };
241
+ watchSubscribersRef.current.forEach((cb) => cb(core.values, info));
242
+ }
243
+ lastChangedFieldRef.current = void 0;
244
+ }, [core.values, watchSubscribersRef, lastChangedFieldRef]);
245
+ const watch = useCallback3(
246
+ (nameOrNamesOrCallback) => {
247
+ const { valuesRef } = core.refs;
248
+ if (typeof nameOrNamesOrCallback === "function") {
249
+ const cb = nameOrNamesOrCallback;
250
+ watchSubscribersRef.current.add(cb);
251
+ return () => watchSubscribersRef.current.delete(cb);
252
+ }
253
+ if (Array.isArray(nameOrNamesOrCallback)) {
254
+ return nameOrNamesOrCallback.map(
255
+ (n) => valuesRef.current[n]
256
+ );
257
+ }
258
+ if (typeof nameOrNamesOrCallback === "string") {
259
+ return valuesRef.current[nameOrNamesOrCallback];
260
+ }
261
+ return valuesRef.current;
262
+ },
263
+ [core.refs, watchSubscribersRef]
264
+ );
265
+ return { watch };
266
+ }
267
+
268
+ // src/hooks/sub-hooks/use-form-actions.ts
269
+ import { useCallback as useCallback4 } from "react";
270
+ import {
271
+ validateSchema as validateSchema2,
272
+ normalizeError,
273
+ transformValues
274
+ } from "@valfuse-node/form";
275
+ function useFormActions(core, clearStaleFieldError) {
276
+ const handleSubmit = useCallback4(
277
+ (onValid) => async (e) => {
278
+ e?.preventDefault?.();
279
+ const { schema, refs } = core;
280
+ const currentValues = refs.valuesRef.current;
281
+ const transformedValues = transformValues(
282
+ schema,
283
+ currentValues
284
+ );
285
+ const schemaErrors = validateSchema2(
286
+ schema,
287
+ transformedValues
288
+ );
289
+ core.setSubmitCount((c) => c + 1);
290
+ core.setIsSubmitted(true);
291
+ if (Object.keys(schemaErrors).length > 0) {
292
+ core.setErrorsState(mapToFieldErrors(schemaErrors));
293
+ core.setIsSubmitSuccessful(false);
294
+ return;
295
+ }
296
+ if (Object.keys(refs.errorsRef.current).length > 0) core.setErrorsState({});
297
+ core.setIsSubmitting(true);
298
+ try {
299
+ await onValid(transformedValues);
300
+ core.setIsSubmitSuccessful(true);
301
+ } catch (err) {
302
+ core.setIsSubmitSuccessful(false);
303
+ throw err;
304
+ } finally {
305
+ core.setIsSubmitting(false);
306
+ }
307
+ },
308
+ // All read via core.* — list each stable field explicitly.
309
+ [
310
+ core.schema,
311
+ core.refs.valuesRef,
312
+ core.refs.errorsRef,
313
+ core.setSubmitCount,
314
+ core.setIsSubmitted,
315
+ core.setIsSubmitting,
316
+ core.setIsSubmitSuccessful,
317
+ core.setErrorsState
318
+ ]
319
+ );
320
+ const setErrors = useCallback4(
321
+ (fieldErrors) => {
322
+ core.setErrorsState((prev) => {
323
+ const next = { ...prev };
324
+ let changed = false;
325
+ for (const [fieldName, rawError] of Object.entries(fieldErrors)) {
326
+ if (rawError === void 0) continue;
327
+ const normalized = normalizeError(rawError);
328
+ const prevFieldError = prev[fieldName];
329
+ if (prevFieldError && prevFieldError.message === normalized.message && prevFieldError.type === (normalized.type ?? "manual") && prevFieldError.code === normalized.code) continue;
330
+ next[fieldName] = {
331
+ message: normalized.message,
332
+ type: normalized.type ?? "manual",
333
+ ...normalized.code !== void 0 && { code: normalized.code },
334
+ ...normalized.metadata !== void 0 && {
335
+ metadata: normalized.metadata
336
+ }
337
+ };
338
+ changed = true;
339
+ }
340
+ return changed ? next : prev;
341
+ });
342
+ },
343
+ [core.setErrorsState]
344
+ );
345
+ const clearErrors = useCallback4(
346
+ (name) => {
347
+ if (name === void 0) {
348
+ if (Object.keys(core.refs.errorsRef.current).length === 0) return;
349
+ core.setErrorsState({});
350
+ return;
351
+ }
352
+ const fields = Array.isArray(name) ? name : [name];
353
+ core.setErrorsState((prev) => {
354
+ const toDelete = fields.filter((k) => k in prev);
355
+ if (toDelete.length === 0) return prev;
356
+ const next = { ...prev };
357
+ for (const n of toDelete) delete next[n];
358
+ return next;
359
+ });
360
+ },
361
+ [core.refs.errorsRef, core.setErrorsState]
362
+ );
363
+ const trigger = useCallback4(
364
+ (name) => {
365
+ const { schema, refs } = core;
366
+ const current = refs.valuesRef.current;
367
+ const transformed = transformValues(schema, current);
368
+ const fieldsToValidate = name === void 0 ? Object.keys(schema) : Array.isArray(name) ? name : [name];
369
+ let allValid = true;
370
+ let changed = false;
371
+ const prevErrors = refs.errorsRef.current;
372
+ const nextErrors = { ...prevErrors };
373
+ for (const field of fieldsToValidate) {
374
+ if (!schema[field]) continue;
375
+ const raw = validateSchema2({ [field]: schema[field] }, transformed);
376
+ const error = buildFieldError(raw, field);
377
+ if (error) {
378
+ allValid = false;
379
+ const prev = prevErrors[field];
380
+ if (!prev || prev.message !== error.message || prev.type !== error.type || prev.code !== error.code) {
381
+ changed = true;
382
+ }
383
+ nextErrors[field] = error;
384
+ } else {
385
+ if (field in nextErrors) {
386
+ changed = true;
387
+ delete nextErrors[field];
388
+ }
389
+ }
390
+ }
391
+ if (changed) core.setErrorsState(nextErrors);
392
+ return allValid;
393
+ },
394
+ [core.schema, core.refs.valuesRef, core.refs.errorsRef, core.setErrorsState]
395
+ );
396
+ const setValue = useCallback4(
397
+ (name, value, options) => {
398
+ const { schema, refs } = core;
399
+ const fieldTransform = refs.schemaRef.current[name]?.transform;
400
+ const transformedValue = fieldTransform ? fieldTransform(value) : value;
401
+ const updated = { ...refs.valuesRef.current, [name]: transformedValue };
402
+ refs.valuesRef.current = updated;
403
+ refs.lastChangedFieldRef.current = name;
404
+ core.setValues(updated);
405
+ if (options?.shouldValidate) {
406
+ const field = name;
407
+ if (!schema[field]) return;
408
+ const raw = validateSchema2(
409
+ { [field]: schema[field] },
410
+ updated
411
+ );
412
+ const error = buildFieldError(raw, field);
413
+ core.setErrorsState((prev) => {
414
+ if (!error && !(field in prev)) return prev;
415
+ const prevError = prev[name];
416
+ if (error && prevError && error.message === prevError.message && error.type === prevError.type && error.code === prevError.code) return prev;
417
+ const next = { ...prev };
418
+ if (error) {
419
+ next[name] = error;
420
+ } else {
421
+ delete next[name];
422
+ }
423
+ return next;
424
+ });
425
+ } else {
426
+ clearStaleFieldError(name);
427
+ }
428
+ },
429
+ [
430
+ core.schema,
431
+ core.refs.schemaRef,
432
+ core.refs.valuesRef,
433
+ core.refs.lastChangedFieldRef,
434
+ core.setValues,
435
+ core.setErrorsState,
436
+ clearStaleFieldError
437
+ ]
438
+ );
439
+ const reset = useCallback4(
440
+ (newValues) => {
441
+ core.setValues({ ...core.defaultValues, ...newValues });
442
+ core.setErrorsState({});
443
+ core.setTouchedFields(/* @__PURE__ */ new Set());
444
+ core.setIsSubmitted(false);
445
+ core.setIsSubmitSuccessful(false);
446
+ core.setSubmitCount(0);
447
+ },
448
+ [
449
+ core.defaultValues,
450
+ core.setValues,
451
+ core.setErrorsState,
452
+ core.setTouchedFields,
453
+ core.setIsSubmitted,
454
+ core.setIsSubmitSuccessful,
455
+ core.setSubmitCount
456
+ ]
457
+ );
458
+ return { handleSubmit, setErrors, clearErrors, setValue, trigger, reset };
459
+ }
460
+
461
+ // src/hooks/sub-hooks/use-form-derived-state.ts
462
+ import { useMemo as useMemo2 } from "react";
463
+ import { validateSchema as validateSchema3, transformValues as transformValues2 } from "@valfuse-node/form";
464
+ function useFormDerivedState(core) {
465
+ const { schema, defaultValues, values, errors, touchedFields } = core;
466
+ const isDirty = useMemo2(
467
+ () => Object.keys(defaultValues).some(
468
+ (key) => values[key] !== defaultValues[key]
469
+ ),
470
+ [values, defaultValues]
471
+ );
472
+ const dirtyFields = useMemo2(
473
+ () => Object.keys(defaultValues).reduce(
474
+ (acc, key) => {
475
+ const k = key;
476
+ if (values[k] !== defaultValues[k]) acc[k] = true;
477
+ return acc;
478
+ },
479
+ {}
480
+ ),
481
+ [values, defaultValues]
482
+ );
483
+ const touchedFieldsRecord = useMemo2(
484
+ () => Array.from(touchedFields).reduce(
485
+ (acc, key) => {
486
+ acc[key] = true;
487
+ return acc;
488
+ },
489
+ {}
490
+ ),
491
+ [touchedFields]
492
+ );
493
+ const isValid = useMemo2(() => {
494
+ if (Object.keys(errors).length > 0) return false;
495
+ const transformed = transformValues2(
496
+ schema,
497
+ values
498
+ );
499
+ return Object.keys(validateSchema3(schema, transformed)).length === 0;
500
+ }, [schema, values, errors]);
501
+ return { isDirty, dirtyFields, touchedFieldsRecord, isValid };
502
+ }
503
+
504
+ // src/hooks/use-valfuse-form.ts
505
+ function useValfuseForm(props) {
506
+ const { defaultValues } = props;
507
+ const core = useFormCore(props);
508
+ const { validateField, clearStaleFieldError } = useFieldValidation({
509
+ schema: core.schema,
510
+ setErrorsState: core.setErrorsState
511
+ });
512
+ const { register, control } = useFormRegistration({
513
+ core,
514
+ validateField,
515
+ clearStaleFieldError
516
+ });
517
+ const { watch } = useFormWatch(core);
518
+ const { handleSubmit, setErrors, clearErrors, setValue, trigger, reset } = useFormActions(core, clearStaleFieldError);
519
+ const { isDirty, dirtyFields, touchedFieldsRecord, isValid } = useFormDerivedState(core);
520
+ const formState = useMemo3(
521
+ () => ({
522
+ errors: core.errors,
523
+ isSubmitting: core.isSubmitting,
524
+ isSubmitted: core.isSubmitted,
525
+ isSubmitSuccessful: core.isSubmitSuccessful,
526
+ submitCount: core.submitCount,
527
+ isDirty,
528
+ isValid,
529
+ dirtyFields,
530
+ touchedFields: touchedFieldsRecord,
531
+ defaultValues
532
+ }),
533
+ [
534
+ core.errors,
535
+ core.isSubmitting,
536
+ core.isSubmitted,
537
+ core.isSubmitSuccessful,
538
+ core.submitCount,
539
+ isDirty,
540
+ isValid,
541
+ dirtyFields,
542
+ touchedFieldsRecord,
543
+ defaultValues
544
+ ]
545
+ );
546
+ return {
547
+ register,
548
+ control,
549
+ handleSubmit,
550
+ formState,
551
+ setErrors,
552
+ clearErrors,
553
+ setValue,
554
+ trigger,
555
+ watch,
556
+ reset
557
+ };
558
+ }
559
+
560
+ // src/components/valfuse-controller.tsx
561
+ import { useCallback as useCallback5, useMemo as useMemo4 } from "react";
562
+ function ValfuseController({
563
+ control,
564
+ name,
565
+ render
566
+ }) {
567
+ const onChange = useCallback5(
568
+ (value) => control._updateField(name, value),
569
+ [control._updateField, name]
570
+ );
571
+ const onBlur = useCallback5(
572
+ () => control._touchField(name),
573
+ [control._touchField, name]
574
+ );
575
+ const fieldValue = control._values[name];
576
+ const fieldError = control._errors[name];
577
+ const isTouched = control._touchedFields.has(name);
578
+ const field = useMemo4(
579
+ () => ({
580
+ name,
581
+ value: fieldValue,
582
+ onChange,
583
+ onBlur
584
+ }),
585
+ [name, fieldValue, onChange, onBlur]
586
+ );
587
+ const fieldState = useMemo4(
588
+ () => ({
589
+ error: fieldError,
590
+ isTouched
591
+ }),
592
+ [fieldError, isTouched]
593
+ );
594
+ return render({ field, fieldState });
595
+ }
596
+
597
+ // src/localization/provider/localization-provider.tsx
598
+ import { createContext, useCallback as useCallback6, useMemo as useMemo5, useState as useState2 } from "react";
599
+
600
+ // src/localization/bridge/create-localization-store.ts
601
+ import { interpolate, lookupMessage } from "@valfuse-node/localization/runtime";
602
+ function createLocalizationStore(manifest, initialLocale) {
603
+ let locale = initialLocale ?? manifest.base_locale;
604
+ return {
605
+ getLocale: () => locale,
606
+ setLocale(nextLocale) {
607
+ locale = nextLocale;
608
+ },
609
+ t(key, params) {
610
+ const value = lookupMessage(
611
+ {
612
+ locale,
613
+ fallbackLocale: manifest.fallback_locale,
614
+ messages: manifest.messages
615
+ },
616
+ key
617
+ );
618
+ return interpolate(value, params);
619
+ }
620
+ };
621
+ }
622
+
623
+ // src/localization/provider/localization-provider.tsx
624
+ import { jsx } from "react/jsx-runtime";
625
+ var LocalizationContext = createContext(null);
626
+ function determineInitialLocale(manifest, initialLocale, storage) {
627
+ const stored = storage?.get();
628
+ if (stored && manifest.locales.includes(stored)) return stored;
629
+ if (initialLocale && manifest.locales.includes(initialLocale)) return initialLocale;
630
+ return manifest.base_locale;
631
+ }
632
+ function LocalizationProvider({
633
+ manifest,
634
+ initialLocale,
635
+ storage,
636
+ children
637
+ }) {
638
+ const [locale, _setLocale] = useState2(
639
+ () => determineInitialLocale(manifest, initialLocale, storage)
640
+ );
641
+ const setLocale = useCallback6(
642
+ (nextLocale) => {
643
+ storage?.set(nextLocale);
644
+ _setLocale(nextLocale);
645
+ },
646
+ [storage]
647
+ );
648
+ const value = useMemo5(() => {
649
+ const store = createLocalizationStore(manifest, locale);
650
+ return { locale, setLocale, store, manifest };
651
+ }, [locale, manifest, setLocale]);
652
+ return /* @__PURE__ */ jsx(LocalizationContext.Provider, { value, children });
653
+ }
654
+
655
+ // src/localization/hooks/use-localization.ts
656
+ import { useCallback as useCallback7, useContext, useMemo as useMemo6 } from "react";
657
+ import {
658
+ interpolate as interpolate2,
659
+ lookupMessage as lookupMessage2,
660
+ pickStructuredContextVariant,
661
+ pickStructuredGenderVariant,
662
+ pickStructuredPluralVariant
663
+ } from "@valfuse-node/localization/runtime";
664
+ function pickFallbackText(fallback, key) {
665
+ if (!fallback) return void 0;
666
+ if (typeof fallback === "string") return fallback;
667
+ if (typeof fallback === "function") return fallback(key);
668
+ return fallback[key];
669
+ }
670
+ function useLocalization(options = {}) {
671
+ const context = useContext(LocalizationContext);
672
+ if (!context) {
673
+ throw new Error("useLocalization must be used within LocalizationProvider.");
674
+ }
675
+ const { locale, manifest } = context;
676
+ const fallback = options.fallback;
677
+ const runtimeContext = useMemo6(
678
+ () => ({
679
+ locale,
680
+ fallbackLocale: manifest.fallback_locale,
681
+ messages: manifest.messages
682
+ }),
683
+ [locale, manifest.fallback_locale, manifest.messages]
684
+ );
685
+ const readTranslation = useCallback7(
686
+ (key) => lookupMessage2(runtimeContext, key),
687
+ [runtimeContext]
688
+ );
689
+ const hasKey = useCallback7(
690
+ (key) => key in (manifest.messages[locale] ?? {}) || key in (manifest.messages[manifest.fallback_locale] ?? {}),
691
+ [locale, manifest.fallback_locale, manifest.messages]
692
+ );
693
+ const translate = useCallback7(
694
+ (key, fallbackValue) => {
695
+ const translated = readTranslation(key);
696
+ if (!translated || translated === key) {
697
+ if (fallbackValue === null) return translated;
698
+ return fallbackValue ?? pickFallbackText(fallback, key) ?? translated;
699
+ }
700
+ return translated;
701
+ },
702
+ [fallback, readTranslation]
703
+ );
704
+ const format = useCallback7(
705
+ (key, params) => interpolate2(translate(key), params),
706
+ [translate]
707
+ );
708
+ const translateOrNull = useCallback7(
709
+ (key) => {
710
+ if (key == null) return null;
711
+ return hasKey(key) ? translate(key) : null;
712
+ },
713
+ [hasKey, translate]
714
+ );
715
+ const formatOrNull = useCallback7(
716
+ (key, params) => {
717
+ if (key == null) return null;
718
+ return hasKey(key) ? format(key, params) : null;
719
+ },
720
+ [hasKey, format]
721
+ );
722
+ const raw = useCallback7(
723
+ (key) => manifest.messages[locale]?.[key] ?? manifest.messages[manifest.fallback_locale]?.[key] ?? "",
724
+ [locale, manifest.fallback_locale, manifest.messages]
725
+ );
726
+ const plural = useCallback7(
727
+ (key, count) => pickStructuredPluralVariant(raw(key), count),
728
+ [raw]
729
+ );
730
+ const pluralOrNull = useCallback7(
731
+ (key, count) => {
732
+ if (key == null) return null;
733
+ return hasKey(key) ? plural(key, count) : null;
734
+ },
735
+ [hasKey, plural]
736
+ );
737
+ const gender = useCallback7(
738
+ (key, value, params) => interpolate2(pickStructuredGenderVariant(raw(key), value), params),
739
+ [raw]
740
+ );
741
+ const contextFn = useCallback7(
742
+ (key, value, params) => {
743
+ const translated = pickStructuredContextVariant(raw(key), value);
744
+ return params ? interpolate2(translated, params) : translated;
745
+ },
746
+ [raw]
747
+ );
748
+ const namespace = useCallback7(
749
+ (scope) => ({
750
+ translate: (key, fallbackValue) => translate(`${scope}.${key}`, fallbackValue),
751
+ translateOrNull: (key) => key == null ? null : translateOrNull(`${scope}.${key}`),
752
+ format: (key, params) => format(`${scope}.${key}`, params),
753
+ formatOrNull: (key, params) => key == null ? null : formatOrNull(`${scope}.${key}`, params),
754
+ plural: (key, count) => plural(`${scope}.${key}`, count),
755
+ pluralOrNull: (key, count) => key == null ? null : pluralOrNull(`${scope}.${key}`, count),
756
+ gender: (key, value, params) => gender(`${scope}.${key}`, value, params),
757
+ context: (key, value, params) => contextFn(`${scope}.${key}`, value, params)
758
+ }),
759
+ [translate, translateOrNull, format, formatOrNull, plural, pluralOrNull, gender, contextFn]
760
+ );
761
+ const entriesForLocale = useMemo6(
762
+ () => Object.entries(manifest.messages[locale] ?? {}).sort(
763
+ ([a], [b]) => a.localeCompare(b)
764
+ ),
765
+ [locale, manifest.messages]
766
+ );
767
+ return {
768
+ ...context,
769
+ translate,
770
+ translateOrNull,
771
+ format,
772
+ formatOrNull,
773
+ plural,
774
+ pluralOrNull,
775
+ gender,
776
+ context: contextFn,
777
+ namespace,
778
+ entriesForLocale
779
+ };
780
+ }
781
+
782
+ // src/localization/hooks/use-localization-tree.ts
783
+ import { useMemo as useMemo7 } from "react";
784
+ function toSegments(key) {
785
+ return key.split(".").slice(1);
786
+ }
787
+ function useLocalizationTree() {
788
+ const { store, manifest } = useLocalization();
789
+ return useMemo7(() => {
790
+ const strings = {};
791
+ const placeholders = {};
792
+ for (const entry of manifest.entries) {
793
+ const segments = toSegments(entry.key);
794
+ if (segments.length === 0) continue;
795
+ const target = entry.placeholders.length > 0 ? placeholders : strings;
796
+ let cursor = target;
797
+ for (let i = 0; i < segments.length - 1; i += 1) {
798
+ const segment = segments[i];
799
+ if (!segment) continue;
800
+ cursor[segment] ?? (cursor[segment] = {});
801
+ cursor = cursor[segment];
802
+ }
803
+ const leaf = segments[segments.length - 1];
804
+ if (!leaf) continue;
805
+ if (entry.placeholders.length > 0) {
806
+ cursor[leaf] = (params) => store.t(entry.key, params);
807
+ } else {
808
+ cursor[leaf] = store.t(entry.key);
809
+ }
810
+ }
811
+ return { strings, placeholders };
812
+ }, [store, manifest]);
813
+ }
814
+
815
+ // src/localization/lazy/create-lazy-locale-loader.ts
816
+ function createLazyLocaleLoader(manifest) {
817
+ return async (locale) => manifest.messages[locale] ?? {};
818
+ }
819
+
820
+ // src/localization/ssr/create-ssr-localization-state.ts
821
+ function createSsrLocalizationState(manifest, locale) {
822
+ const activeLocale = locale ?? manifest.base_locale;
823
+ return {
824
+ locale: activeLocale,
825
+ messages: manifest.messages[activeLocale] ?? {}
826
+ };
827
+ }
828
+
829
+ // src/localization/storage/locale-storage.ts
830
+ function localStorageStrategy(options = {}) {
831
+ const key = options.key ?? "locale";
832
+ return {
833
+ get() {
834
+ try {
835
+ return window.localStorage.getItem(key) ?? void 0;
836
+ } catch {
837
+ return void 0;
838
+ }
839
+ },
840
+ set(locale) {
841
+ try {
842
+ window.localStorage.setItem(key, locale);
843
+ } catch {
844
+ }
845
+ },
846
+ remove() {
847
+ try {
848
+ window.localStorage.removeItem(key);
849
+ } catch {
850
+ }
851
+ }
852
+ };
853
+ }
854
+ function sessionStorageStrategy(options = {}) {
855
+ const key = options.key ?? "locale";
856
+ return {
857
+ get() {
858
+ try {
859
+ return window.sessionStorage.getItem(key) ?? void 0;
860
+ } catch {
861
+ return void 0;
862
+ }
863
+ },
864
+ set(locale) {
865
+ try {
866
+ window.sessionStorage.setItem(key, locale);
867
+ } catch {
868
+ }
869
+ },
870
+ remove() {
871
+ try {
872
+ window.sessionStorage.removeItem(key);
873
+ } catch {
874
+ }
875
+ }
876
+ };
877
+ }
878
+ function cookieStrategy(options = {}) {
879
+ const {
880
+ key = "locale",
881
+ domain,
882
+ path = "/",
883
+ maxAge = 31536e3,
884
+ sameSite = "Lax"
885
+ } = options;
886
+ function buildCookie(value, age) {
887
+ const isSecure = options.secure !== void 0 ? options.secure : typeof location !== "undefined" && location.protocol === "https:";
888
+ let cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
889
+ cookie += `; path=${path}`;
890
+ cookie += `; max-age=${age}`;
891
+ if (domain) cookie += `; domain=${domain}`;
892
+ if (isSecure) cookie += "; Secure";
893
+ cookie += `; SameSite=${sameSite}`;
894
+ return cookie;
895
+ }
896
+ return {
897
+ get() {
898
+ if (typeof document === "undefined") return void 0;
899
+ const encodedKey = encodeURIComponent(key);
900
+ const match = document.cookie.split("; ").find((c) => c.startsWith(`${encodedKey}=`));
901
+ const rawValue = match?.split("=").slice(1).join("=");
902
+ return rawValue !== void 0 ? decodeURIComponent(rawValue) : void 0;
903
+ },
904
+ set(locale) {
905
+ if (typeof document === "undefined") return;
906
+ document.cookie = buildCookie(locale, maxAge);
907
+ },
908
+ remove() {
909
+ if (typeof document === "undefined") return;
910
+ document.cookie = buildCookie("", 0);
911
+ }
912
+ };
913
+ }
914
+ function memoryStrategy(options = {}) {
915
+ let stored = options.initialLocale;
916
+ return {
917
+ get: () => stored,
918
+ set: (locale) => {
919
+ stored = locale;
920
+ },
921
+ remove: () => {
922
+ stored = void 0;
923
+ }
924
+ };
925
+ }
926
+ function composeStorage(...storages) {
927
+ return {
928
+ get() {
929
+ for (const s of storages) {
930
+ const v = s.get();
931
+ if (v !== void 0) return v;
932
+ }
933
+ return void 0;
934
+ },
935
+ set(locale) {
936
+ storages.forEach((s) => s.set(locale));
937
+ },
938
+ remove() {
939
+ storages.forEach((s) => s.remove?.());
940
+ }
941
+ };
942
+ }
943
+ export {
944
+ LocalizationProvider,
945
+ ValfuseController,
946
+ composeStorage,
947
+ cookieStrategy,
948
+ createLazyLocaleLoader,
949
+ createLocalizationStore,
950
+ createSsrLocalizationState,
951
+ localStorageStrategy,
952
+ memoryStrategy,
953
+ sessionStorageStrategy,
954
+ useLocalization,
955
+ useLocalizationTree,
956
+ useValfuseForm
957
+ };