@js-empire/emperor-ui 1.2.0 → 1.2.2

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 (70) hide show
  1. package/dist/emperor-ui.js +52 -48
  2. package/dist/emperor-ui.umd.cjs +8 -8
  3. package/dist/{features-animation-D_Ss-HYx.js → features-animation-uyo1KMg-.js} +1 -1
  4. package/dist/{index-C3mfrNCk.js → index-B3d8-vnJ.js} +4 -4
  5. package/dist/{index-SRvFgjzo.js → index-DOwkJus4.js} +3871 -3600
  6. package/dist/index-DrkA25TM.js +5 -0
  7. package/dist/index.d.ts +89 -7
  8. package/dist/src-UW24ZMRV-D6kiVea5.js +5 -0
  9. package/package.json +1 -1
  10. package/src/components/atoms/uploader/avatar-label.tsx +1 -1
  11. package/src/components/atoms/uploader/index.ts +1 -0
  12. package/src/components/atoms/uploader/stories/uploader.stories.tsx +52 -18
  13. package/src/components/atoms/uploader/upload-file-error-box.tsx +2 -2
  14. package/src/components/atoms/uploader/upload-file-label.tsx +7 -3
  15. package/src/components/atoms/uploader/uploader-title.tsx +21 -0
  16. package/src/components/atoms/uploader/uploader.tsx +3 -0
  17. package/src/components/organisms/footer/footer.tsx +1 -1
  18. package/src/components/organisms/listings/listings.tsx +12 -3
  19. package/src/components/organisms/listings/stories/listings.stories.tsx +30 -0
  20. package/src/components/organisms/listings/styles/classes.ts +11 -0
  21. package/src/components/organisms/listings/styles/index.ts +2 -0
  22. package/src/components/organisms/listings/styles/styles.ts +6 -0
  23. package/src/constants/defaults.ts +3 -26
  24. package/src/hooks/use-uploader.tsx +5 -4
  25. package/src/i18n/configs/i18n.ts +7 -0
  26. package/src/i18n/configs/index.ts +1 -0
  27. package/src/i18n/constants/index.ts +1 -0
  28. package/src/i18n/constants/locales.ts +4 -0
  29. package/src/i18n/index.ts +5 -0
  30. package/src/i18n/locales/ar.ts +15 -0
  31. package/src/i18n/locales/atoms/ar.ts +15 -0
  32. package/src/i18n/locales/atoms/en.ts +15 -0
  33. package/src/i18n/locales/atoms/index.ts +2 -0
  34. package/src/i18n/locales/common/ar.ts +1 -0
  35. package/src/i18n/locales/common/en.ts +1 -0
  36. package/src/i18n/locales/common/index.ts +2 -0
  37. package/src/i18n/locales/en.ts +15 -0
  38. package/src/i18n/locales/index.ts +4 -0
  39. package/src/i18n/locales/molecules/ar.ts +1 -0
  40. package/src/i18n/locales/molecules/en.ts +1 -0
  41. package/src/i18n/locales/molecules/index.ts +2 -0
  42. package/src/i18n/locales/organisms/ar.ts +1 -0
  43. package/src/i18n/locales/organisms/en.ts +1 -0
  44. package/src/i18n/locales/organisms/index.ts +2 -0
  45. package/src/i18n/locales/templates/ar.ts +1 -0
  46. package/src/i18n/locales/templates/en.ts +1 -0
  47. package/src/i18n/locales/templates/index.ts +2 -0
  48. package/src/i18n/locales/toasts/ar.ts +1 -0
  49. package/src/i18n/locales/toasts/en.ts +1 -0
  50. package/src/i18n/locales/toasts/index.ts +2 -0
  51. package/src/i18n/types/index.ts +2 -0
  52. package/src/i18n/types/locale.ts +5 -0
  53. package/src/i18n/types/toasts.ts +3 -0
  54. package/src/i18n/utils/get-locales.ts +4 -0
  55. package/src/i18n/utils/index.ts +2 -0
  56. package/src/i18n/utils/localize.ts +15 -0
  57. package/src/mocks/index.ts +1 -0
  58. package/src/mocks/listings.ts +200 -0
  59. package/src/providers/config-provider.tsx +18 -0
  60. package/src/types/components/atoms/uploader.ts +5 -5
  61. package/src/types/components/molecules/listings/listings.ts +5 -1
  62. package/src/types/context/config.ts +1 -32
  63. package/src/types/context/index.ts +2 -0
  64. package/src/types/context/localization.ts +23 -0
  65. package/src/types/context/theme.ts +17 -0
  66. package/src/utils/index.ts +1 -0
  67. package/src/utils/locales.ts +54 -0
  68. package/src/utils/uploader.ts +6 -5
  69. package/dist/index-CZpTSGZs.js +0 -5
  70. package/dist/src-UW24ZMRV-Ducut0ty.js +0 -5
@@ -0,0 +1,7 @@
1
+ import { LangKey } from "@/i18n/constants/locales"; //! never change this until you solve the circular dependency issue
2
+
3
+ export const i18n = {
4
+ defaultLocale: "en",
5
+ locales: [LangKey.ARABIC, LangKey.ENGLISH],
6
+ localDetection: true,
7
+ };
@@ -0,0 +1 @@
1
+ export * from "./i18n";
@@ -0,0 +1 @@
1
+ export * from "./locales";
@@ -0,0 +1,4 @@
1
+ export enum LangKey {
2
+ ARABIC = "ar",
3
+ ENGLISH = "en",
4
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./configs";
2
+ export * from "./types";
3
+ export * from "./utils";
4
+ export * from "./constants";
5
+ export * from "./locales";
@@ -0,0 +1,15 @@
1
+ import { commonAr } from "./common";
2
+ import { toastsAr } from "./toasts";
3
+ import { atomsAr } from "./atoms";
4
+ import { moleculesAr } from "./molecules";
5
+ import { organismsAr } from "./organisms";
6
+ import { templatesAr } from "./templates";
7
+
8
+ export const ar = {
9
+ common: commonAr,
10
+ toasts: toastsAr,
11
+ atoms: atomsAr,
12
+ molecules: moleculesAr,
13
+ organisms: organismsAr,
14
+ templates: templatesAr,
15
+ };
@@ -0,0 +1,15 @@
1
+ export const atomsAr = {
2
+ uploader: {
3
+ dropHere: "أسقط الملف هنا",
4
+ selectFile: "حدد ملفًا أو اسحبه وأفلته هنا",
5
+ selectionTypes: "JPG, PNG أو PDF، حجم الملف لا يزيد عن 10MB",
6
+ selectBtn: "حدد ملف",
7
+ errorUploadingFile: "لم يتم تحميل أي ملف",
8
+ maxNumImages: "تم تجاوز الحد الأقصى للصور المرفوعة: ",
9
+ errorUploadedTypes: "يمكنك فقط تحميل الملفات من الأنواع التالية: ",
10
+ maxSizeExceededError:
11
+ "يمكنك فقط رفع ملفات/صور بحجم أقل من MAX_FILE_SIZE ميغابايت، بينما حجم الملف الذي رفعته هو UPLOADED_FILE_SIZE ميغابايت",
12
+ duplicatesDenied:
13
+ "لا يمكنك رفع صور تحمل نفس الاسم، يمكنك إعادة تسميتها قبل عملية الرفع",
14
+ },
15
+ };
@@ -0,0 +1,15 @@
1
+ export const atomsEn = {
2
+ uploader: {
3
+ dropHere: "Drop file here",
4
+ selectFile: "Select a file or drag and drop here",
5
+ selectionTypes: "JPG, PNG or PDF, file size no more than 10MB",
6
+ selectBtn: "Select file",
7
+ errorUploadingFile: "No file was uploaded",
8
+ maxNumImages: "Maximum number of uploaded images exceeded: ",
9
+ errorUploadedTypes: "You can only upload files within these types: ",
10
+ maxSizeExceededError:
11
+ "You can only upload files/images with less than MAX_FILE_SIZE Mega Bytes, you're file's size is UPLOADED_FILE_SIZE Mega Bytes",
12
+ duplicatesDenied:
13
+ "You can't upload images with duplicate names, you may rename them before the upload process",
14
+ },
15
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./ar";
2
+ export * from "./en";
@@ -0,0 +1 @@
1
+ export const commonAr = {};
@@ -0,0 +1 @@
1
+ export const commonEn = {};
@@ -0,0 +1,2 @@
1
+ export * from "./ar";
2
+ export * from "./en";
@@ -0,0 +1,15 @@
1
+ import { commonEn } from "./common";
2
+ import { toastsEn } from "./toasts";
3
+ import { atomsEn } from "./atoms";
4
+ import { moleculesEn } from "./molecules";
5
+ import { organismsEn } from "./organisms";
6
+ import { templatesEn } from "./templates";
7
+
8
+ export const en = {
9
+ common: commonEn,
10
+ toasts: toastsEn,
11
+ atoms: atomsEn,
12
+ molecules: moleculesEn,
13
+ organisms: organismsEn,
14
+ templates: templatesEn,
15
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./ar";
2
+ export * from "./en";
3
+ export * from "./common";
4
+ export * from "./toasts";
@@ -0,0 +1 @@
1
+ export const moleculesAr = {};
@@ -0,0 +1 @@
1
+ export const moleculesEn = {};
@@ -0,0 +1,2 @@
1
+ export * from "./ar";
2
+ export * from "./en";
@@ -0,0 +1 @@
1
+ export const organismsAr = {};
@@ -0,0 +1 @@
1
+ export const organismsEn = {};
@@ -0,0 +1,2 @@
1
+ export * from "./ar";
2
+ export * from "./en";
@@ -0,0 +1 @@
1
+ export const templatesAr = {};
@@ -0,0 +1 @@
1
+ export const templatesEn = {};
@@ -0,0 +1,2 @@
1
+ export * from "./ar";
2
+ export * from "./en";
@@ -0,0 +1 @@
1
+ export const toastsAr = {};
@@ -0,0 +1 @@
1
+ export const toastsEn = {};
@@ -0,0 +1,2 @@
1
+ export * from "./ar";
2
+ export * from "./en";
@@ -0,0 +1,2 @@
1
+ export type * from "./locale";
2
+ export type * from "./toasts";
@@ -0,0 +1,5 @@
1
+ import { ar, en, i18n } from "@/i18n";
2
+
3
+ export type Lang = (typeof i18n)["locales"][number];
4
+
5
+ export type Locale = typeof ar | typeof en;
@@ -0,0 +1,3 @@
1
+ import { toastsAr, toastsEn } from "@/i18n";
2
+
3
+ export type ToastKey = keyof typeof toastsAr & keyof typeof toastsEn;
@@ -0,0 +1,4 @@
1
+ import { Lang, Locale, ar, en } from "@/i18n";
2
+
3
+ export const getLocales = (lang: string | Lang): Locale =>
4
+ (lang === "en" ? en : ar) as Locale;
@@ -0,0 +1,2 @@
1
+ export * from "./get-locales";
2
+ export * from "./localize";
@@ -0,0 +1,15 @@
1
+ import { Lang } from "@/i18n";
2
+
3
+ export const localize = ({
4
+ object,
5
+ lang,
6
+ key,
7
+ }: {
8
+ lang: Lang;
9
+ key: string;
10
+ object?: Record<string, string> | undefined;
11
+ }): string | null | undefined =>
12
+ ({
13
+ ar: object?.[key],
14
+ en: object?.[key],
15
+ })[lang] || null;
@@ -1 +1,2 @@
1
1
  export * from "./header";
2
+ export * from "./listings";
@@ -0,0 +1,200 @@
1
+ export type MockItemType = {
2
+ id: number;
3
+ title: string;
4
+ description: string;
5
+ image: string;
6
+ };
7
+
8
+ export const MOCK_LISTINGS: MockItemType[] = [
9
+ {
10
+ id: 1,
11
+ title: "Listing 1",
12
+ description: "Description 1",
13
+ image: "https://via.placeholder.com/150",
14
+ },
15
+ {
16
+ id: 2,
17
+ title: "Listing 2",
18
+ description: "Description 2",
19
+ image: "https://via.placeholder.com/150",
20
+ },
21
+ {
22
+ id: 3,
23
+ title: "Listing 3",
24
+ description: "Description 3",
25
+ image: "https://via.placeholder.com/150",
26
+ },
27
+
28
+ {
29
+ id: 4,
30
+ title: "Listing 4",
31
+ description: "Description 4",
32
+ image: "https://via.placeholder.com/150",
33
+ },
34
+ {
35
+ id: 5,
36
+ title: "Listing 5",
37
+ description: "Description 5",
38
+ image: "https://via.placeholder.com/150",
39
+ },
40
+ {
41
+ id: 6,
42
+ title: "Listing 6",
43
+ description: "Description 6",
44
+ image: "https://via.placeholder.com/150",
45
+ },
46
+ {
47
+ id: 7,
48
+ title: "Listing 7",
49
+ description: "Description 7",
50
+ image: "https://via.placeholder.com/150",
51
+ },
52
+ {
53
+ id: 8,
54
+ title: "Listing 8",
55
+ description: "Description 8",
56
+ image: "https://via.placeholder.com/150",
57
+ },
58
+ {
59
+ id: 9,
60
+ title: "Listing 9",
61
+ description: "Description 9",
62
+ image: "https://via.placeholder.com/150",
63
+ },
64
+ {
65
+ id: 10,
66
+ title: "Listing 10",
67
+ description: "Description 10",
68
+ image: "https://via.placeholder.com/150",
69
+ },
70
+ {
71
+ id: 11,
72
+ title: "Listing 11",
73
+ description: "Description 11",
74
+ image: "https://via.placeholder.com/150",
75
+ },
76
+ {
77
+ id: 12,
78
+ title: "Listing 12",
79
+ description: "Description 12",
80
+ image: "https://via.placeholder.com/150",
81
+ },
82
+ {
83
+ id: 13,
84
+ title: "Listing 13",
85
+ description: "Description 13",
86
+ image: "https://via.placeholder.com/150",
87
+ },
88
+ {
89
+ id: 14,
90
+ title: "Listing 14",
91
+ description: "Description 14",
92
+ image: "https://via.placeholder.com/150",
93
+ },
94
+ {
95
+ id: 15,
96
+ title: "Listing 15",
97
+ description: "Description 15",
98
+ image: "https://via.placeholder.com/150",
99
+ },
100
+ {
101
+ id: 16,
102
+ title: "Listing 16",
103
+ description: "Description 16",
104
+ image: "https://via.placeholder.com/150",
105
+ },
106
+ {
107
+ id: 17,
108
+ title: "Listing 17",
109
+ description: "Description 17",
110
+ image: "https://via.placeholder.com/150",
111
+ },
112
+ {
113
+ id: 18,
114
+ title: "Listing 18",
115
+ description: "Description 18",
116
+ image: "https://via.placeholder.com/150",
117
+ },
118
+ {
119
+ id: 19,
120
+ title: "Listing 19",
121
+ description: "Description 19",
122
+ image: "https://via.placeholder.com/150",
123
+ },
124
+ {
125
+ id: 20,
126
+ title: "Listing 20",
127
+ description: "Description 20",
128
+ image: "https://via.placeholder.com/150",
129
+ },
130
+ {
131
+ id: 21,
132
+ title: "Listing 21",
133
+ description: "Description 21",
134
+ image: "https://via.placeholder.com/150",
135
+ },
136
+ {
137
+ id: 22,
138
+ title: "Listing 22",
139
+ description: "Description 22",
140
+ image: "https://via.placeholder.com/150",
141
+ },
142
+ {
143
+ id: 23,
144
+ title: "Listing 23",
145
+ description: "Description 23",
146
+ image: "https://via.placeholder.com/150",
147
+ },
148
+ {
149
+ id: 24,
150
+ title: "Listing 24",
151
+ description: "Description 24",
152
+ image: "https://via.placeholder.com/150",
153
+ },
154
+ {
155
+ id: 25,
156
+ title: "Listing 25",
157
+ description: "Description 25",
158
+ image: "https://via.placeholder.com/150",
159
+ },
160
+ {
161
+ id: 26,
162
+ title: "Listing 26",
163
+ description: "Description 26",
164
+ image: "https://via.placeholder.com/150",
165
+ },
166
+ {
167
+ id: 27,
168
+ title: "Listing 27",
169
+ description: "Description 27",
170
+ image: "https://via.placeholder.com/150",
171
+ },
172
+ {
173
+ id: 28,
174
+ title: "Listing 28",
175
+ description: "Description 28",
176
+ image: "https://via.placeholder.com/150",
177
+ },
178
+ {
179
+ id: 29,
180
+ title: "Listing 29",
181
+ description: "Description 29",
182
+ image: "https://via.placeholder.com/150",
183
+ },
184
+ {
185
+ id: 30,
186
+ title: "Listing 30",
187
+ description: "Description 30",
188
+ image: "https://via.placeholder.com/150",
189
+ },
190
+ ];
191
+
192
+ export const getListings = ({
193
+ page = 1,
194
+ pageSize = 10,
195
+ }: {
196
+ page?: number;
197
+ pageSize?: number;
198
+ }) => {
199
+ return MOCK_LISTINGS.slice((page - 1) * pageSize, page * pageSize);
200
+ };
@@ -1,11 +1,13 @@
1
1
  import { Scaffold } from "@/components";
2
2
  import { defaultEmperorUIConfig } from "@/constants";
3
3
  import { EmperorUIContext } from "@/context";
4
+ import { LangKey, Locale } from "@/i18n";
4
5
  import type {
5
6
  ConfigContextState,
6
7
  ConfigProviderProps,
7
8
  EmperorUIConfig,
8
9
  } from "@/types";
10
+ import { mergeLocales } from "@/utils";
9
11
  import { useMemo } from "react";
10
12
 
11
13
  export function ConfigProvider({
@@ -29,6 +31,22 @@ export function ConfigProvider({
29
31
  interLocalization: {
30
32
  ...defaultEmperorUIConfig?.interLocalization,
31
33
  ...config?.interLocalization,
34
+ locales: {
35
+ [LangKey.ENGLISH]: mergeLocales({
36
+ defaultLocales: defaultEmperorUIConfig?.interLocalization
37
+ ?.locales?.[LangKey.ENGLISH] as Locale,
38
+ configLocales: config?.interLocalization?.locales?.[
39
+ LangKey.ENGLISH
40
+ ] as Locale,
41
+ }),
42
+ [LangKey.ARABIC]: mergeLocales({
43
+ defaultLocales: defaultEmperorUIConfig?.interLocalization
44
+ ?.locales?.[LangKey.ARABIC] as Locale,
45
+ configLocales: config?.interLocalization?.locales?.[
46
+ LangKey.ARABIC
47
+ ] as Locale,
48
+ }),
49
+ },
32
50
  },
33
51
  };
34
52
 
@@ -25,7 +25,7 @@ export type SharedLabelIdType = string;
25
25
  export type SharedOnInputChangeType = (
26
26
  event: React.ChangeEvent<HTMLInputElement> &
27
27
  React.DragEvent<HTMLLabelElement>,
28
- ) => Promise<void>;
28
+ ) => Promise<void | string | null>;
29
29
 
30
30
  export type UseUploadFileProps = {
31
31
  labelId: string;
@@ -52,10 +52,7 @@ export type UseUploadFileReturn = {
52
52
  isLoading: boolean;
53
53
  setFiles: Dispatch<SetStateAction<FileObject[]>>;
54
54
  handleClearFile: (fileName?: string) => void;
55
- onInputChange: (
56
- event: React.ChangeEvent<HTMLInputElement> &
57
- React.DragEvent<HTMLLabelElement>,
58
- ) => Promise<void | string | null>;
55
+ onInputChange: SharedOnInputChangeType;
59
56
  };
60
57
 
61
58
  export type UploaderContextState = {
@@ -65,6 +62,8 @@ export type UploaderContextState = {
65
62
  labelId: SharedLabelIdType;
66
63
  labelContent?: ReactNode;
67
64
  avatarLabelContent?: ReactNode;
65
+ title?: ReactNode;
66
+ errorMessage?: ReactNode;
68
67
 
69
68
  isFileViewable?: boolean;
70
69
  isRequired?: boolean;
@@ -89,6 +88,7 @@ export type UploaderContextState = {
89
88
  listing?: string;
90
89
  error?: string;
91
90
  input?: string;
91
+ title?: string;
92
92
  };
93
93
  };
94
94
 
@@ -4,6 +4,10 @@ export type ListingsClassnames = {
4
4
  base?: string;
5
5
  };
6
6
 
7
- export type ListingsProps = SharedComponentProps & {
7
+ export type ListingsVariant = "default";
8
+
9
+ export type ListingsProps<ListingType> = SharedComponentProps & {
8
10
  classNames?: ListingsClassnames;
11
+ variant?: ListingsVariant;
12
+ items: ListingType[];
9
13
  };
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { HeroUIProviderProps } from "@heroui/react";
3
+ import { EmperorUITheme, EmperorUIInterLocalization } from "@/types";
3
4
 
4
5
  export type ConfigContextState = {
5
6
  config: EmperorUIConfig;
@@ -10,42 +11,10 @@ export type ConfigProviderProps = {
10
11
  config?: EmperorUIConfig;
11
12
  };
12
13
 
13
- export type ColorMode = "light" | "dark";
14
- export type AppDirection = "ltr" | "rtl";
15
-
16
- export type ColorsPalette = {
17
- primary: string;
18
- secondary: string;
19
- success: string;
20
- danger: string;
21
- warning: string;
22
- info: string;
23
- background: string;
24
- foreground: string;
25
- };
26
-
27
- export type EmperorUITheme = {
28
- mode: ColorMode;
29
- colors: Partial<ColorsPalette>;
30
- };
31
-
32
14
  export type EmperorUILayout = {
33
15
  withScaffold: boolean;
34
16
  };
35
17
 
36
- export type EmperorUILang = "en" | "ar";
37
-
38
- export type EmperorUILocales = Record<EmperorUILang, Record<string, string>>;
39
-
40
- export type EmperorUIInterLocalization = {
41
- lang?: EmperorUILang;
42
- languages?: EmperorUILang[];
43
- defaultLanguage?: EmperorUILang;
44
- isMultiLingual?: boolean;
45
- dir?: AppDirection;
46
- locales?: EmperorUILocales;
47
- };
48
-
49
18
  export type EmperorUIConfig = {
50
19
  theme?: Partial<EmperorUITheme>;
51
20
  layout?: Partial<EmperorUILayout>;
@@ -1,2 +1,4 @@
1
1
  export type * from "./config";
2
2
  export type * from "./navigation";
3
+ export type * from "./localization";
4
+ export type * from "./theme";
@@ -0,0 +1,23 @@
1
+ import { Locale } from "@/i18n";
2
+
3
+ export type AppDirection = "ltr" | "rtl";
4
+
5
+ export type EmperorUILang = "en" | "ar";
6
+
7
+ export type EmperorUILocales = Record<
8
+ EmperorUILang,
9
+ Partial<{
10
+ atoms?: {
11
+ uploader?: Partial<Locale["atoms"]["uploader"]>;
12
+ };
13
+ }>
14
+ >;
15
+
16
+ export type EmperorUIInterLocalization = {
17
+ lang?: EmperorUILang;
18
+ languages?: EmperorUILang[];
19
+ defaultLanguage?: EmperorUILang;
20
+ isMultiLingual?: boolean;
21
+ dir?: AppDirection;
22
+ locales?: EmperorUILocales;
23
+ };
@@ -0,0 +1,17 @@
1
+ export type ColorMode = "light" | "dark";
2
+
3
+ export type ColorsPalette = {
4
+ primary: string;
5
+ secondary: string;
6
+ success: string;
7
+ danger: string;
8
+ warning: string;
9
+ info: string;
10
+ background: string;
11
+ foreground: string;
12
+ };
13
+
14
+ export type EmperorUITheme = {
15
+ mode: ColorMode;
16
+ colors: Partial<ColorsPalette>;
17
+ };
@@ -2,3 +2,4 @@ export * from "./cn";
2
2
  export * from "./storybook";
3
3
  export * from "./compress-images";
4
4
  export * from "./uploader";
5
+ export * from "./locales";
@@ -0,0 +1,54 @@
1
+ import { Locale } from "@/i18n";
2
+
3
+ /**
4
+ * Checks if a value is a plain object (not array, null, or other object types).
5
+ */
6
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
7
+ typeof value === "object" && value !== null && !Array.isArray(value);
8
+
9
+ /**
10
+ * Deep-merges config locales into default locales. Default values are used as base,
11
+ * and config values override only the keys they provide. Nested objects are merged
12
+ * recursively so unrelated keys at any level remain unchanged.
13
+ */
14
+ export const mergeLocales = ({
15
+ defaultLocales,
16
+ configLocales,
17
+ }: {
18
+ defaultLocales: Locale;
19
+ configLocales?: Partial<Locale>;
20
+ }): Locale => {
21
+ const deepMerge = <T extends Record<string, unknown>>(
22
+ defaultObj: T,
23
+ configObj: Partial<T> | undefined,
24
+ ): T => {
25
+ if (configObj == null || Object.keys(configObj).length === 0) {
26
+ return defaultObj;
27
+ }
28
+
29
+ const result = { ...defaultObj } as T;
30
+
31
+ for (const key of Object.keys(configObj) as (keyof T)[]) {
32
+ const configVal = configObj[key];
33
+ const defaultVal = defaultObj[key];
34
+
35
+ if (configVal === undefined) continue;
36
+
37
+ if (isPlainObject(configVal) && isPlainObject(defaultVal as unknown)) {
38
+ (result as Record<string, unknown>)[key as string] = deepMerge(
39
+ defaultVal as Record<string, unknown>,
40
+ configVal,
41
+ );
42
+ } else {
43
+ (result as Record<string, unknown>)[key as string] = configVal;
44
+ }
45
+ }
46
+
47
+ return result;
48
+ };
49
+
50
+ return deepMerge(
51
+ defaultLocales as unknown as Record<string, unknown>,
52
+ configLocales as Partial<Record<string, unknown>>,
53
+ ) as Locale;
54
+ };