@jasperoosthoek/react-toolbox 0.8.0 → 0.9.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 (63) hide show
  1. package/change-log.md +330 -309
  2. package/dist/components/buttons/ConfirmButton.d.ts +2 -2
  3. package/dist/components/buttons/DeleteConfirmButton.d.ts +2 -2
  4. package/dist/components/buttons/IconButtons.d.ts +40 -41
  5. package/dist/components/errors/Errors.d.ts +1 -2
  6. package/dist/components/forms/FormField.d.ts +22 -0
  7. package/dist/components/forms/FormFields.d.ts +1 -56
  8. package/dist/components/forms/FormModal.d.ts +7 -34
  9. package/dist/components/forms/FormModalProvider.d.ts +19 -26
  10. package/dist/components/forms/FormProvider.d.ts +66 -0
  11. package/dist/components/forms/fields/FormBadgesSelection.d.ts +26 -0
  12. package/dist/components/forms/fields/FormCheckbox.d.ts +7 -0
  13. package/dist/components/forms/fields/FormDropdown.d.ts +19 -0
  14. package/dist/components/forms/fields/FormInput.d.ts +17 -0
  15. package/dist/components/forms/fields/FormSelect.d.ts +12 -0
  16. package/dist/components/forms/fields/index.d.ts +5 -0
  17. package/dist/components/indicators/CheckIndicator.d.ts +1 -2
  18. package/dist/components/indicators/LoadingIndicator.d.ts +4 -4
  19. package/dist/components/login/LoginPage.d.ts +1 -1
  20. package/dist/components/tables/DataTable.d.ts +2 -2
  21. package/dist/components/tables/DragAndDropList.d.ts +2 -2
  22. package/dist/components/tables/SearchBox.d.ts +2 -2
  23. package/dist/index.d.ts +4 -1
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.LICENSE.txt +0 -4
  26. package/dist/localization/LocalizationContext.d.ts +1 -1
  27. package/dist/utils/hooks.d.ts +1 -1
  28. package/dist/utils/timeAndDate.d.ts +5 -2
  29. package/dist/utils/utils.d.ts +3 -3
  30. package/package.json +10 -11
  31. package/src/__tests__/buttons.test.tsx +545 -0
  32. package/src/__tests__/errors.test.tsx +339 -0
  33. package/src/__tests__/forms.test.tsx +3021 -0
  34. package/src/__tests__/hooks.test.tsx +413 -0
  35. package/src/__tests__/indicators.test.tsx +284 -0
  36. package/src/__tests__/localization.test.tsx +462 -0
  37. package/src/__tests__/login.test.tsx +417 -0
  38. package/src/__tests__/setupTests.ts +328 -0
  39. package/src/__tests__/tables.test.tsx +609 -0
  40. package/src/__tests__/timeAndDate.test.tsx +308 -0
  41. package/src/__tests__/utils.test.tsx +422 -0
  42. package/src/components/forms/FormField.tsx +92 -0
  43. package/src/components/forms/FormFields.tsx +3 -423
  44. package/src/components/forms/FormModal.tsx +168 -243
  45. package/src/components/forms/FormModalProvider.tsx +141 -95
  46. package/src/components/forms/FormProvider.tsx +218 -0
  47. package/src/components/forms/fields/FormBadgesSelection.tsx +108 -0
  48. package/src/components/forms/fields/FormCheckbox.tsx +76 -0
  49. package/src/components/forms/fields/FormDropdown.tsx +123 -0
  50. package/src/components/forms/fields/FormInput.tsx +114 -0
  51. package/src/components/forms/fields/FormSelect.tsx +47 -0
  52. package/src/components/forms/fields/index.ts +6 -0
  53. package/src/index.ts +32 -28
  54. package/src/localization/LocalizationContext.tsx +156 -131
  55. package/src/localization/localization.ts +131 -131
  56. package/src/utils/hooks.ts +108 -94
  57. package/src/utils/timeAndDate.ts +33 -4
  58. package/src/utils/utils.ts +74 -66
  59. package/dist/components/forms/CreateEditModal.d.ts +0 -41
  60. package/dist/components/forms/CreateEditModalProvider.d.ts +0 -41
  61. package/dist/components/forms/FormFields.test.d.ts +0 -4
  62. package/dist/login/Login.d.ts +0 -70
  63. package/src/components/forms/FormFields.test.tsx +0 -107
@@ -0,0 +1,123 @@
1
+ import React, { useMemo, KeyboardEvent } from 'react';
2
+ import { Form } from 'react-bootstrap';
3
+ import { useFormField } from '../FormField';
4
+ import { useLocalization } from '../../../localization/LocalizationContext';
5
+
6
+ type DisabledProps = {
7
+ list: any[];
8
+ value: string | number;
9
+ state: any;
10
+ initialState: any;
11
+ initialValue: any;
12
+ }
13
+
14
+ export interface FormDropdownProps<T> extends Omit<
15
+ React.InputHTMLAttributes<HTMLInputElement>,
16
+ 'name' | 'value' | 'onChange' | 'disabled' | 'list'
17
+ > {
18
+ name: string;
19
+ label?: React.ReactElement | string;
20
+ list?: T[];
21
+ options?: T[]; // Alias for list
22
+ idKey?: keyof T;
23
+ nameKey?: keyof T;
24
+ disabled?: boolean | ((props: DisabledProps) => boolean);
25
+ }
26
+
27
+ export const FormDropdown = <T,>(props: FormDropdownProps<T>) => {
28
+ const {
29
+ list: listProp,
30
+ options,
31
+ idKey = 'value' as keyof T, // Change default to match test
32
+ nameKey = 'label' as keyof T, // Change default to match test
33
+ disabled,
34
+ ...componentProps
35
+ } = props;
36
+
37
+ const { value, onChange, isInvalid, error, label, required, mergedProps, submit } = useFormField(componentProps);
38
+ const { strings } = useLocalization();
39
+
40
+ // Use options or list, with options taking precedence
41
+ const listBase = options || listProp;
42
+
43
+ const [list, mismatch] = useMemo(() => {
44
+ if (!listBase || !Array.isArray(listBase)) {
45
+ return [null, null] as [T[] | null, any];
46
+ }
47
+
48
+ // Handle string arrays by converting them to { value, label } objects
49
+ if (listBase.length > 0 && typeof listBase[0] === 'string') {
50
+ const convertedList = listBase.map((item: any) => ({
51
+ value: item,
52
+ label: item
53
+ })) as unknown as T[];
54
+ return [convertedList, null];
55
+ }
56
+
57
+ // Only validate object arrays
58
+ if (listBase.length > 0 && typeof listBase[0] === 'object') {
59
+ const mismatch = listBase?.find((item: any) => (
60
+ !['number', 'string'].includes(typeof item[idKey])
61
+ || !['number', 'string'].includes(typeof item[nameKey])
62
+ ))
63
+ if (mismatch) return [null, mismatch];
64
+ }
65
+
66
+ return [listBase, null];
67
+ }, [listBase, idKey, nameKey]);
68
+
69
+ if (!list) {
70
+ console.error(
71
+ `FormDropdown Error:
72
+ - Each item in 'list' must include a valid 'idKey' (${String(idKey)}) and 'nameKey' (${String(nameKey)}).
73
+ - Both keys must exist on every item and be of type 'string' or 'number'.
74
+ - One or more items failed this check.
75
+
76
+ Received list:`,
77
+ listBase,
78
+ ...mismatch ? ['First mismatch:', mismatch] : []
79
+ );
80
+ return null;
81
+ }
82
+
83
+ const selectedItem = list.find(item => item[idKey] === value);
84
+
85
+ return (
86
+ <Form.Group controlId={props.name}>
87
+ {label && <Form.Label htmlFor={props.name}>{label}{required && ' *'}</Form.Label>}
88
+ {isInvalid && error && (
89
+ <Form.Text className="text-danger">
90
+ {error}
91
+ </Form.Text>
92
+ )}
93
+
94
+ <Form.Select
95
+ id={props.name}
96
+ value={value || ''}
97
+ isInvalid={isInvalid}
98
+ onChange={(e) => onChange(e.target.value)}
99
+ onKeyDown={(e: KeyboardEvent<HTMLSelectElement>) => {
100
+ if (e.key === 'Enter') {
101
+ e.preventDefault();
102
+ submit();
103
+ }
104
+ }}
105
+ {...mergedProps}
106
+ >
107
+ {list.map((item, key) =>
108
+ <option
109
+ key={key}
110
+ value={item[idKey] as string | number}
111
+ disabled={
112
+ typeof disabled === 'function'
113
+ ? disabled({ list, value: item[idKey] as number | string, state: {}, initialState: {}, initialValue: '' })
114
+ : disabled
115
+ }
116
+ >
117
+ {item[nameKey] as string}
118
+ </option>
119
+ )}
120
+ </Form.Select>
121
+ </Form.Group>
122
+ );
123
+ };
@@ -0,0 +1,114 @@
1
+ import React, { ChangeEvent, KeyboardEvent } from 'react';
2
+ import { Form } from 'react-bootstrap';
3
+ import { useFormField } from '../FormField';
4
+
5
+ export interface FormInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'onChange'> {
6
+ name: string;
7
+ label?: React.ReactElement | string;
8
+ as?: string; // For textarea, select, etc.
9
+ rows?: number; // For textarea
10
+ onChange: (value: string) => void;
11
+ }
12
+
13
+ export const FormInput = (props: FormInputProps) => {
14
+ const { value, onChange, isInvalid, error, label, required, mergedProps, submit } = useFormField(props);
15
+
16
+ const errorId = isInvalid && error ? `${props.name}-error` : undefined;
17
+
18
+ return (
19
+ <Form.Group controlId={props.name}>
20
+ {label && <Form.Label htmlFor={props.name}>{label}{required && ' *'}</Form.Label>}
21
+ {isInvalid && error && (
22
+ <Form.Text id={errorId} className="text-danger">
23
+ {error}
24
+ </Form.Text>
25
+ )}
26
+ <Form.Control
27
+ id={props.name}
28
+ autoComplete="off"
29
+ {...mergedProps}
30
+ value={value || ''}
31
+ isInvalid={isInvalid}
32
+ aria-describedby={errorId}
33
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
34
+ onChange(e.target.value);
35
+ }}
36
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
37
+ if (e.key === 'Enter' && mergedProps.as !== 'textarea') {
38
+ // Pressing the enter key will save data unless it is a multi line text area
39
+ e.preventDefault();
40
+ submit();
41
+ }
42
+ }}
43
+ />
44
+ </Form.Group>
45
+ );
46
+ };
47
+
48
+ export const FormTextarea = ({ rows = 3, ...props }: FormInputProps) => (
49
+ <FormInput as="textarea" rows={rows} {...props} />
50
+ );
51
+
52
+ export const FormDate = (props: FormInputProps) => (
53
+ <FormInput type="date" {...props} />
54
+ );
55
+
56
+ export interface FormDateTimeProps extends Omit<FormInputProps, 'value' | 'onChange'> {
57
+ value?: string | Date;
58
+ onChange?: (value: string) => void;
59
+ timezone?: string;
60
+ }
61
+
62
+ export const FormDateTime = ({ value, onChange, timezone, ...props }: FormDateTimeProps) => {
63
+ // Convert value to datetime-local format (YYYY-MM-DDTHH:mm)
64
+ const formatForInput = (val: string | Date | undefined) => {
65
+ if (!val) return '';
66
+
67
+ try {
68
+ const date = typeof val === 'string' ? new Date(val) : val;
69
+ if (isNaN(date.getTime())) return '';
70
+
71
+ // Format as YYYY-MM-DDTHH:mm for datetime-local input
72
+ const year = date.getFullYear();
73
+ const month = String(date.getMonth() + 1).padStart(2, '0');
74
+ const day = String(date.getDate()).padStart(2, '0');
75
+ const hours = String(date.getHours()).padStart(2, '0');
76
+ const minutes = String(date.getMinutes()).padStart(2, '0');
77
+
78
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
79
+ } catch {
80
+ return '';
81
+ }
82
+ };
83
+
84
+ const handleChange = (inputValue: string) => {
85
+ if (!onChange) return;
86
+
87
+ if (!inputValue) {
88
+ onChange('');
89
+ return;
90
+ }
91
+
92
+ try {
93
+ // Convert datetime-local value to ISO string
94
+ const date = new Date(inputValue);
95
+ if (isNaN(date.getTime())) {
96
+ onChange('');
97
+ return;
98
+ }
99
+
100
+ onChange(date.toISOString());
101
+ } catch {
102
+ onChange('');
103
+ }
104
+ };
105
+
106
+ return (
107
+ <FormInput
108
+ type="datetime-local"
109
+ value={formatForInput(value)}
110
+ onChange={handleChange}
111
+ {...props}
112
+ />
113
+ );
114
+ };
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { Form } from 'react-bootstrap';
3
+ import { useFormField } from '../FormField';
4
+
5
+ export interface FormSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'name' | 'value' | 'onChange'> {
6
+ name: string;
7
+ label?: React.ReactElement | string;
8
+ options: Array<{ value: string | number; label: string; disabled?: boolean }>;
9
+ placeholder?: string;
10
+ }
11
+
12
+ export const FormSelect = (props: FormSelectProps) => {
13
+ const { value, onChange, isInvalid, error, label, required, mergedProps } = useFormField(props);
14
+ const { options, placeholder = "Choose..." } = props;
15
+
16
+ return (
17
+ <Form.Group controlId={props.name}>
18
+ {label && <Form.Label>{label}{required && ' *'}</Form.Label>}
19
+ {isInvalid && error && (
20
+ <Form.Text className="text-danger">
21
+ {error}
22
+ </Form.Text>
23
+ )}
24
+ <Form.Select
25
+ {...mergedProps}
26
+ value={value || ''}
27
+ isInvalid={isInvalid}
28
+ onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
29
+ onChange(e.target.value);
30
+ }}
31
+ >
32
+ <option value="" disabled>
33
+ {placeholder}
34
+ </option>
35
+ {options.map((option, index) => (
36
+ <option
37
+ key={index}
38
+ value={option.value}
39
+ disabled={option.disabled}
40
+ >
41
+ {option.label}
42
+ </option>
43
+ ))}
44
+ </Form.Select>
45
+ </Form.Group>
46
+ );
47
+ };
@@ -0,0 +1,6 @@
1
+ // Export all form field components
2
+ export * from './FormInput';
3
+ export * from './FormSelect';
4
+ export * from './FormCheckbox';
5
+ export * from './FormDropdown';
6
+ export * from './FormBadgesSelection';
package/src/index.ts CHANGED
@@ -1,28 +1,32 @@
1
- export * from './components/buttons/IconButtons';
2
- export * from './components/buttons/ConfirmButton';
3
- export { default as ConfirmButton } from './components/buttons/ConfirmButton';
4
- export { default as DeleteConfirmButton } from './components/buttons/DeleteConfirmButton';
5
- export * from './components/buttons/DeleteConfirmButton';
6
-
7
- export * from './components/forms/FormModal';
8
- export * from './components/forms/FormModalProvider';
9
- export * from './components/forms/FormFields';
10
-
11
- export * from './components/indicators/LoadingIndicator';
12
- export * from './components/indicators/CheckIndicator';
13
-
14
- export * from './components/tables/DataTable';
15
- export * from './components/tables/DragAndDropList';
16
- export * from './components/tables/SearchBox';
17
-
18
- export * from './components/errors/Errors';
19
- export * from './components/errors/ErrorBoundary';
20
-
21
- export * from './utils/hooks';
22
- export * from './utils/timeAndDate';
23
- export * from './utils/utils';
24
-
25
- export * from './components/login/LoginPage';
26
-
27
- export * from './localization/localization';
28
- export * from './localization/LocalizationContext';
1
+ export * from './components/buttons/IconButtons';
2
+ export * from './components/buttons/ConfirmButton';
3
+ export { default as ConfirmButton } from './components/buttons/ConfirmButton';
4
+ export { default as DeleteConfirmButton } from './components/buttons/DeleteConfirmButton';
5
+ export * from './components/buttons/DeleteConfirmButton';
6
+
7
+ export * from './components/forms/FormProvider';
8
+ export * from './components/forms/FormModal';
9
+ export * from './components/forms/FormModalProvider';
10
+ export * from './components/forms/FormFields';
11
+ export * from './components/forms/FormField';
12
+ export * from './components/forms/fields';
13
+
14
+ export * from './components/indicators/LoadingIndicator';
15
+ export * from './components/indicators/CheckIndicator';
16
+
17
+ export * from './components/tables/DataTable';
18
+ export * from './components/tables/DragAndDropList';
19
+ export * from './components/tables/SearchBox';
20
+
21
+ export * from './components/errors/Errors';
22
+ export * from './components/errors/ErrorBoundary';
23
+
24
+ export * from './components/login/LoginPage';
25
+
26
+ export * from './utils/hooks';
27
+ export * from './utils/timeAndDate';
28
+ export * from './utils/utils';
29
+
30
+
31
+ export * from './localization/localization';
32
+ export * from './localization/LocalizationContext';
@@ -1,131 +1,156 @@
1
- import React, { useState, useContext } from 'react';
2
- import LocalizedStrings from 'react-localization';
3
- import {
4
- defaultLocalization,
5
- AdditionalLocalization,
6
- LocalizationElement,
7
- LocalizationFunction,
8
- defaultLanguages,
9
- } from './localization';
10
-
11
- const out_of_context_error = 'This function should only be used in a child of LocalizationProvider.';
12
-
13
- export const combineLocalization = (...locals: AdditionalLocalization[]) => {
14
- // Extract all unique language keys
15
- const languages = [...new Set(locals.flatMap(Object.keys))];
16
-
17
- return languages.reduce(
18
- (o, lang) => ({
19
- ...o,
20
- [lang]: locals.reduce<LocalizationElement>(
21
- (acc, l) => ({ ...acc, ...(l[lang] || {}) }),
22
- {} as LocalizationElement
23
- ),
24
- }),
25
- {} as AdditionalLocalization
26
- );
27
- };
28
-
29
- export const LocalizationContext = React.createContext({
30
- lang: 'en',
31
- languages: Object.keys(defaultLanguages),
32
- setLanguage: (lang: string) => console.error(out_of_context_error),
33
- text: (str: TemplateStringsArray, ...values: (string | number)[]) => {
34
- console.error(out_of_context_error);
35
- return str[0];
36
- },
37
- textByLang: (lang: string) => (str: TemplateStringsArray, ...values: (string | number)[]) => {
38
- console.error(out_of_context_error);
39
- return str[0];
40
- },
41
- strings: new LocalizedStrings({ en: {} }),
42
- localizationStrings: {} as AdditionalLocalization,
43
- setLocalization: (localization: AdditionalLocalization) => console.error(out_of_context_error),
44
- });
45
-
46
- export type RestProps = {
47
- [prop: string]: any;
48
- }
49
- export interface LocalizationProviderProps extends RestProps {
50
- lang?: string;
51
- localization?: AdditionalLocalization;
52
- languages?: string[];
53
- children?: any;
54
- [prop: string]: any;
55
- }
56
-
57
- export const LocalizationProvider = ({
58
- lang: initialLanguage = 'en',
59
- localization: additionalLocalizationInitial = {},
60
- languages: languagesOverride,
61
- children,
62
- ...restProps
63
- }: LocalizationProviderProps) => {
64
- const [additionalLocalization, setLocalization] = useState(additionalLocalizationInitial);
65
- const [lang, setLanguage] = useState(initialLanguage);
66
- const languages = Array.from(new Set([
67
- ...languagesOverride || Object.keys(defaultLocalization),
68
- ...Object.keys(additionalLocalization),
69
- ]));
70
- const localizationStrings = languages.reduce(
71
- (o, lang) => ({
72
- ...o,
73
- [lang]: {
74
- ...defaultLocalization[lang] || {},
75
- ...additionalLocalization[lang] || {},
76
- },
77
- }),
78
- {}
79
- );
80
-
81
- const strings = new LocalizedStrings(localizationStrings);
82
- strings.setLanguage(lang);
83
-
84
- const textByLang = (lang: string) => (str: TemplateStringsArray, ...values: (string | number)[]) => {
85
- const text_or_func = strings.getString(str[0], lang) as string | LocalizationFunction;
86
- if (!text_or_func) {
87
- console.error(`Language string not found: "${str[0]}"`);
88
- return str[0];
89
- } else if (typeof text_or_func === 'function') {
90
- if (text_or_func.length !== values.length) {
91
- console.error(
92
- `Language function "${str[0]}" expects exactly ${text_or_func.length} argument${text_or_func.length === 1 ? '' : 's'}. `
93
- + (
94
- text_or_func.length > 0
95
- ? ` Use text${'`'}${str[0]}${'${arg1}${arg2}`'}`
96
- : ` Use text${'`'}${str[0]}${'`'}`
97
- )
98
- );
99
- return text_or_func.length > 0 ? `${str[0]}${'${arg1}${arg2}'}` : str[0];
100
- }
101
- return text_or_func(...values);
102
- }
103
-
104
- return text_or_func;
105
- }
106
- return (
107
- <LocalizationContext.Provider
108
- value={{
109
- lang,
110
- languages,
111
- setLanguage: (lang: string) => {
112
- if (!languages.includes(lang)) {
113
- console.error(`Language ${lang} not available`);
114
- return;
115
- }
116
- setLanguage(lang);
117
- },
118
- strings,
119
- text: textByLang(lang),
120
- textByLang,
121
- localizationStrings,
122
- setLocalization,
123
- ...restProps,
124
- }}
125
- >
126
- {children}
127
- </LocalizationContext.Provider>
128
- );
129
- };
130
-
131
- export const useLocalization = () => useContext(LocalizationContext);
1
+ import React, { useState, useContext, useEffect, useMemo } from 'react';
2
+ import LocalizedStrings from 'react-localization';
3
+ import {
4
+ defaultLocalization,
5
+ AdditionalLocalization,
6
+ LocalizationElement,
7
+ LocalizationFunction,
8
+ defaultLanguages,
9
+ } from './localization';
10
+
11
+ const out_of_context_error = 'This function should only be used in a child of LocalizationProvider.';
12
+
13
+ export const combineLocalization = (...locals: AdditionalLocalization[]) => {
14
+ if (locals.length === 0) {
15
+ return {} as AdditionalLocalization;
16
+ }
17
+
18
+ // Filter out empty objects
19
+ const validLocals = locals.filter(local => local && Object.keys(local).length > 0);
20
+
21
+ if (validLocals.length === 0) {
22
+ return {} as AdditionalLocalization;
23
+ }
24
+
25
+ // Extract all unique language keys
26
+ const languages = [...new Set(validLocals.flatMap(Object.keys))];
27
+
28
+ return languages.reduce(
29
+ (o, lang) => ({
30
+ ...o,
31
+ [lang]: validLocals.reduce<LocalizationElement>(
32
+ (acc, l) => ({ ...acc, ...(l[lang] || {}) }),
33
+ {} as LocalizationElement
34
+ ),
35
+ }),
36
+ {} as AdditionalLocalization
37
+ );
38
+ };
39
+
40
+ export const LocalizationContext = React.createContext({
41
+ lang: 'en',
42
+ languages: Object.keys(defaultLanguages),
43
+ setLanguage: (lang: string) => console.error(out_of_context_error),
44
+ text: (str: TemplateStringsArray, ...values: (string | number)[]) => {
45
+ console.error(out_of_context_error);
46
+ return str[0];
47
+ },
48
+ textByLang: (lang: string) => (str: TemplateStringsArray, ...values: (string | number)[]) => {
49
+ console.error(out_of_context_error);
50
+ return str[0];
51
+ },
52
+ strings: new LocalizedStrings({ en: {} }),
53
+ localizationStrings: {} as AdditionalLocalization,
54
+ setLocalization: (localization: AdditionalLocalization) => console.error(out_of_context_error),
55
+ });
56
+
57
+ export type RestProps = {
58
+ [prop: string]: any;
59
+ }
60
+ export interface LocalizationProviderProps extends RestProps {
61
+ lang?: string;
62
+ localization?: AdditionalLocalization;
63
+ languages?: string[];
64
+ children?: any;
65
+ [prop: string]: any;
66
+ }
67
+
68
+ export const LocalizationProvider = ({
69
+ lang: initialLanguage = 'en',
70
+ localization: additionalLocalizationInitial = {},
71
+ languages: languagesOverride,
72
+ children,
73
+ ...restProps
74
+ }: LocalizationProviderProps) => {
75
+ const [additionalLocalization, setAdditionalLocalization] = useState(additionalLocalizationInitial);
76
+ const [lang, setLanguage] = useState(initialLanguage);
77
+
78
+ const languages = Array.from(new Set([
79
+ ...languagesOverride || Object.keys(defaultLocalization),
80
+ ...Object.keys(additionalLocalization),
81
+ ]));
82
+
83
+ const localizationStrings = useMemo(() => {
84
+ return languages.reduce(
85
+ (o, lang) => ({
86
+ ...o,
87
+ [lang]: {
88
+ ...defaultLocalization[lang] || {},
89
+ ...additionalLocalization[lang] || {},
90
+ },
91
+ }),
92
+ {}
93
+ );
94
+ }, [languages, additionalLocalization]);
95
+
96
+ const strings = useMemo(() => {
97
+ const localizedStrings = new LocalizedStrings(localizationStrings);
98
+ localizedStrings.setLanguage(lang);
99
+ return localizedStrings;
100
+ }, [localizationStrings, lang]);
101
+
102
+ const setLocalization = (newLocalization: AdditionalLocalization) => {
103
+ setAdditionalLocalization(prev => {
104
+ const merged = combineLocalization(prev, newLocalization);
105
+ return merged;
106
+ });
107
+ };
108
+
109
+ const textByLang = (lang: string) => (str: TemplateStringsArray, ...values: (string | number)[]) => {
110
+ const text_or_func = strings.getString(str[0], lang) as string | LocalizationFunction;
111
+ if (!text_or_func) {
112
+ console.error(`Language string not found: "${str[0]}"`);
113
+ return str[0];
114
+ } else if (typeof text_or_func === 'function') {
115
+ if (text_or_func.length !== values.length) {
116
+ console.error(
117
+ `Language function "${str[0]}" expects exactly ${text_or_func.length} argument${text_or_func.length === 1 ? '' : 's'}. `
118
+ + (
119
+ text_or_func.length > 0
120
+ ? ` Use text${'`'}${str[0]}${'${arg1}${arg2}`'}`
121
+ : ` Use text${'`'}${str[0]}${'`'}`
122
+ )
123
+ );
124
+ return text_or_func.length > 0 ? `${str[0]}${'${arg1}${arg2}'}` : str[0];
125
+ }
126
+ return text_or_func(...values);
127
+ }
128
+
129
+ return text_or_func;
130
+ }
131
+ return (
132
+ <LocalizationContext.Provider
133
+ value={{
134
+ lang,
135
+ languages,
136
+ setLanguage: (newLang: string) => {
137
+ if (!languages.includes(newLang)) {
138
+ console.error(`Language ${newLang} not available`);
139
+ return;
140
+ }
141
+ setLanguage(newLang);
142
+ },
143
+ strings,
144
+ text: textByLang(lang),
145
+ textByLang,
146
+ localizationStrings,
147
+ setLocalization,
148
+ ...restProps,
149
+ }}
150
+ >
151
+ {children}
152
+ </LocalizationContext.Provider>
153
+ );
154
+ };
155
+
156
+ export const useLocalization = () => useContext(LocalizationContext);