meno-core 1.0.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 (231) hide show
  1. package/bin/cli.ts +281 -0
  2. package/build-static.ts +298 -0
  3. package/bunfig.toml +39 -0
  4. package/entries/client-router.tsx +111 -0
  5. package/entries/server-router.tsx +71 -0
  6. package/lib/client/ClientInitializer.test.ts +9 -0
  7. package/lib/client/ClientInitializer.test.ts.skip +92 -0
  8. package/lib/client/ClientInitializer.ts +60 -0
  9. package/lib/client/ErrorBoundary.test.tsx +595 -0
  10. package/lib/client/ErrorBoundary.tsx +230 -0
  11. package/lib/client/componentRegistry.test.ts +165 -0
  12. package/lib/client/componentRegistry.ts +18 -0
  13. package/lib/client/contexts/ThemeContext.tsx +73 -0
  14. package/lib/client/core/ComponentBuilder.test.ts +677 -0
  15. package/lib/client/core/ComponentBuilder.ts +660 -0
  16. package/lib/client/core/ComponentRenderer.test.tsx +176 -0
  17. package/lib/client/core/ComponentRenderer.tsx +83 -0
  18. package/lib/client/core/cmsTemplateProcessor.ts +129 -0
  19. package/lib/client/elementRegistry.ts +81 -0
  20. package/lib/client/hmr/HMRManager.tsx +179 -0
  21. package/lib/client/hmr/index.ts +5 -0
  22. package/lib/client/hmrWebSocket.test.ts +9 -0
  23. package/lib/client/hmrWebSocket.ts +250 -0
  24. package/lib/client/hooks/useColorVariables.test.ts +166 -0
  25. package/lib/client/hooks/useColorVariables.ts +249 -0
  26. package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
  27. package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
  28. package/lib/client/hydration/HydrationUtils.test.ts +154 -0
  29. package/lib/client/hydration/HydrationUtils.ts +35 -0
  30. package/lib/client/i18nConfigService.test.ts +74 -0
  31. package/lib/client/i18nConfigService.ts +78 -0
  32. package/lib/client/index.ts +56 -0
  33. package/lib/client/navigation.test.ts +441 -0
  34. package/lib/client/navigation.ts +23 -0
  35. package/lib/client/responsiveStyleResolver.test.ts +491 -0
  36. package/lib/client/responsiveStyleResolver.ts +184 -0
  37. package/lib/client/routing/RouteLoader.test.ts +635 -0
  38. package/lib/client/routing/RouteLoader.ts +347 -0
  39. package/lib/client/routing/Router.tsx +382 -0
  40. package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
  41. package/lib/client/scripts/ScriptExecutor.ts +171 -0
  42. package/lib/client/scripts/formHandler.ts +103 -0
  43. package/lib/client/styleProcessor.test.ts +126 -0
  44. package/lib/client/styleProcessor.ts +92 -0
  45. package/lib/client/styles/StyleInjector.test.ts +354 -0
  46. package/lib/client/styles/StyleInjector.ts +154 -0
  47. package/lib/client/templateEngine.test.ts +660 -0
  48. package/lib/client/templateEngine.ts +667 -0
  49. package/lib/client/theme.test.ts +173 -0
  50. package/lib/client/theme.ts +159 -0
  51. package/lib/client/utils/toast.ts +46 -0
  52. package/lib/server/createServer.ts +170 -0
  53. package/lib/server/cssGenerator.test.ts +172 -0
  54. package/lib/server/cssGenerator.ts +58 -0
  55. package/lib/server/fileWatcher.ts +134 -0
  56. package/lib/server/index.ts +55 -0
  57. package/lib/server/jsonLoader.test.ts +103 -0
  58. package/lib/server/jsonLoader.ts +350 -0
  59. package/lib/server/middleware/cors.test.ts +177 -0
  60. package/lib/server/middleware/cors.ts +69 -0
  61. package/lib/server/middleware/errorHandler.test.ts +208 -0
  62. package/lib/server/middleware/errorHandler.ts +63 -0
  63. package/lib/server/middleware/index.ts +9 -0
  64. package/lib/server/middleware/logger.test.ts +233 -0
  65. package/lib/server/middleware/logger.ts +99 -0
  66. package/lib/server/pageCache.test.ts +167 -0
  67. package/lib/server/pageCache.ts +97 -0
  68. package/lib/server/projectContext.ts +51 -0
  69. package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
  70. package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
  71. package/lib/server/providers/fileSystemPageProvider.ts +83 -0
  72. package/lib/server/routes/api/cms.test.ts +177 -0
  73. package/lib/server/routes/api/cms.ts +82 -0
  74. package/lib/server/routes/api/colors.ts +59 -0
  75. package/lib/server/routes/api/components.ts +70 -0
  76. package/lib/server/routes/api/config.test.ts +9 -0
  77. package/lib/server/routes/api/config.ts +28 -0
  78. package/lib/server/routes/api/core-routes.ts +182 -0
  79. package/lib/server/routes/api/functions.ts +170 -0
  80. package/lib/server/routes/api/index.ts +69 -0
  81. package/lib/server/routes/api/pages.ts +95 -0
  82. package/lib/server/routes/api/shared.test.ts +81 -0
  83. package/lib/server/routes/api/shared.ts +31 -0
  84. package/lib/server/routes/editor.test.ts +9 -0
  85. package/lib/server/routes/index.ts +104 -0
  86. package/lib/server/routes/pages.ts +161 -0
  87. package/lib/server/routes/static.ts +107 -0
  88. package/lib/server/services/ColorService.ts +193 -0
  89. package/lib/server/services/cmsService.test.ts +388 -0
  90. package/lib/server/services/cmsService.ts +296 -0
  91. package/lib/server/services/componentService.test.ts +276 -0
  92. package/lib/server/services/componentService.ts +346 -0
  93. package/lib/server/services/configService.ts +156 -0
  94. package/lib/server/services/fileWatcherService.ts +67 -0
  95. package/lib/server/services/index.ts +10 -0
  96. package/lib/server/services/pageService.test.ts +258 -0
  97. package/lib/server/services/pageService.ts +240 -0
  98. package/lib/server/ssrRenderer.test.ts +1005 -0
  99. package/lib/server/ssrRenderer.ts +878 -0
  100. package/lib/server/utilityClassGenerator.ts +11 -0
  101. package/lib/server/utils/index.ts +5 -0
  102. package/lib/server/utils/jsonLineMapper.test.ts +100 -0
  103. package/lib/server/utils/jsonLineMapper.ts +166 -0
  104. package/lib/server/validateStyleCoverage.test.ts +9 -0
  105. package/lib/server/validateStyleCoverage.ts +167 -0
  106. package/lib/server/websocketManager.test.ts +9 -0
  107. package/lib/server/websocketManager.ts +95 -0
  108. package/lib/shared/attributeNodeUtils.test.ts +152 -0
  109. package/lib/shared/attributeNodeUtils.ts +50 -0
  110. package/lib/shared/breakpoints.test.ts +166 -0
  111. package/lib/shared/breakpoints.ts +65 -0
  112. package/lib/shared/colorProperties.test.ts +111 -0
  113. package/lib/shared/colorProperties.ts +40 -0
  114. package/lib/shared/colorVariableUtils.test.ts +319 -0
  115. package/lib/shared/colorVariableUtils.ts +97 -0
  116. package/lib/shared/constants.test.ts +175 -0
  117. package/lib/shared/constants.ts +116 -0
  118. package/lib/shared/cssGeneration.ts +481 -0
  119. package/lib/shared/cssProperties.test.ts +252 -0
  120. package/lib/shared/cssProperties.ts +338 -0
  121. package/lib/shared/elementUtils.test.ts +245 -0
  122. package/lib/shared/elementUtils.ts +90 -0
  123. package/lib/shared/fontLoader.ts +97 -0
  124. package/lib/shared/i18n.test.ts +313 -0
  125. package/lib/shared/i18n.ts +286 -0
  126. package/lib/shared/index.ts +50 -0
  127. package/lib/shared/interfaces/contentProvider.test.ts +9 -0
  128. package/lib/shared/interfaces/contentProvider.ts +121 -0
  129. package/lib/shared/nodeUtils.test.ts +320 -0
  130. package/lib/shared/nodeUtils.ts +220 -0
  131. package/lib/shared/pathArrayUtils.test.ts +315 -0
  132. package/lib/shared/pathArrayUtils.ts +17 -0
  133. package/lib/shared/pathUtils.test.ts +260 -0
  134. package/lib/shared/pathUtils.ts +244 -0
  135. package/lib/shared/paths/Path.test.ts +74 -0
  136. package/lib/shared/paths/Path.ts +23 -0
  137. package/lib/shared/paths/PathConverter.test.ts +232 -0
  138. package/lib/shared/paths/PathConverter.ts +141 -0
  139. package/lib/shared/paths/PathUtils.ts +290 -0
  140. package/lib/shared/paths/PathValidator.test.ts +193 -0
  141. package/lib/shared/paths/PathValidator.ts +53 -0
  142. package/lib/shared/paths/index.ts +48 -0
  143. package/lib/shared/propResolver.test.ts +639 -0
  144. package/lib/shared/propResolver.ts +124 -0
  145. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
  146. package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
  147. package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
  148. package/lib/shared/registry/ClientRegistry.test.ts +26 -0
  149. package/lib/shared/registry/ClientRegistry.ts +15 -0
  150. package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
  151. package/lib/shared/registry/ComponentRegistry.ts +100 -0
  152. package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
  153. package/lib/shared/registry/NodeTypeManager.ts +94 -0
  154. package/lib/shared/registry/RegistryManager.test.ts +58 -0
  155. package/lib/shared/registry/RegistryManager.ts +60 -0
  156. package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
  157. package/lib/shared/registry/SSRRegistry.test.ts +26 -0
  158. package/lib/shared/registry/SSRRegistry.ts +15 -0
  159. package/lib/shared/registry/createNodeType.ts +175 -0
  160. package/lib/shared/registry/defineNodeType.ts +73 -0
  161. package/lib/shared/registry/fieldPresets.ts +109 -0
  162. package/lib/shared/registry/index.ts +50 -0
  163. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
  164. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
  165. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
  166. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
  167. package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
  168. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
  169. package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
  170. package/lib/shared/registry/nodeTypes/index.ts +75 -0
  171. package/lib/shared/responsiveScaling.test.ts +268 -0
  172. package/lib/shared/responsiveScaling.ts +194 -0
  173. package/lib/shared/responsiveStyleUtils.test.ts +300 -0
  174. package/lib/shared/responsiveStyleUtils.ts +139 -0
  175. package/lib/shared/slugTranslator.test.ts +325 -0
  176. package/lib/shared/slugTranslator.ts +177 -0
  177. package/lib/shared/styleNodeUtils.test.ts +132 -0
  178. package/lib/shared/styleNodeUtils.ts +102 -0
  179. package/lib/shared/styleUtils.test.ts +238 -0
  180. package/lib/shared/styleUtils.ts +63 -0
  181. package/lib/shared/themeDefaults.test.ts +113 -0
  182. package/lib/shared/themeDefaults.ts +103 -0
  183. package/lib/shared/tree/PathBuilder.ts +383 -0
  184. package/lib/shared/treePathUtils.test.ts +539 -0
  185. package/lib/shared/treePathUtils.ts +339 -0
  186. package/lib/shared/types/api.ts +58 -0
  187. package/lib/shared/types/cms.ts +95 -0
  188. package/lib/shared/types/colors.ts +45 -0
  189. package/lib/shared/types/components.ts +121 -0
  190. package/lib/shared/types/errors.test.ts +103 -0
  191. package/lib/shared/types/errors.ts +69 -0
  192. package/lib/shared/types/index.ts +96 -0
  193. package/lib/shared/types/nodes.ts +20 -0
  194. package/lib/shared/types/rendering.ts +61 -0
  195. package/lib/shared/types/styles.ts +38 -0
  196. package/lib/shared/types.ts +11 -0
  197. package/lib/shared/utilityClassConfig.ts +287 -0
  198. package/lib/shared/utilityClassMapper.test.ts +140 -0
  199. package/lib/shared/utilityClassMapper.ts +229 -0
  200. package/lib/shared/utils/fileUtils.test.ts +99 -0
  201. package/lib/shared/utils/fileUtils.ts +56 -0
  202. package/lib/shared/utils.test.ts +261 -0
  203. package/lib/shared/utils.ts +84 -0
  204. package/lib/shared/validation/index.ts +7 -0
  205. package/lib/shared/validation/propValidator.test.ts +178 -0
  206. package/lib/shared/validation/propValidator.ts +238 -0
  207. package/lib/shared/validation/schemas.test.ts +177 -0
  208. package/lib/shared/validation/schemas.ts +401 -0
  209. package/lib/shared/validation/validators.test.ts +109 -0
  210. package/lib/shared/validation/validators.ts +304 -0
  211. package/lib/test-utils/dom-setup.ts +55 -0
  212. package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
  213. package/lib/test-utils/factories/DomMockFactory.ts +487 -0
  214. package/lib/test-utils/factories/EventMockFactory.ts +244 -0
  215. package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
  216. package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
  217. package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
  218. package/lib/test-utils/factories/index.ts +11 -0
  219. package/lib/test-utils/fixtures.ts +134 -0
  220. package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
  221. package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
  222. package/lib/test-utils/helpers/index.ts +6 -0
  223. package/lib/test-utils/helpers.test.ts +73 -0
  224. package/lib/test-utils/helpers.ts +90 -0
  225. package/lib/test-utils/index.ts +17 -0
  226. package/lib/test-utils/mockFactories.ts +92 -0
  227. package/lib/test-utils/mocks.ts +341 -0
  228. package/package.json +38 -0
  229. package/templates/index-router.html +34 -0
  230. package/tsconfig.json +14 -0
  231. package/vite.config.ts +43 -0
@@ -0,0 +1,313 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import {
3
+ DEFAULT_I18N_CONFIG,
4
+ getLocaleCodes,
5
+ findLocaleByCode,
6
+ isValidLocaleCode,
7
+ migrateI18nConfig,
8
+ isI18nValue,
9
+ resolveTranslation,
10
+ resolveI18nValue,
11
+ resolveI18nInProps,
12
+ extractLocaleFromPath,
13
+ buildLocalizedPath,
14
+ parseLocaleFromPath,
15
+ } from './i18n';
16
+ import type { I18nConfig, I18nValue } from './types/components';
17
+
18
+ describe('i18n', () => {
19
+ const testConfig: I18nConfig = {
20
+ defaultLocale: 'en',
21
+ locales: [
22
+ { code: 'en', name: 'English', nativeName: 'English', langTag: 'en-US' },
23
+ { code: 'pl', name: 'Polish', nativeName: 'Polski', langTag: 'pl-PL' },
24
+ { code: 'de', name: 'German', nativeName: 'Deutsch', langTag: 'de-DE' },
25
+ ],
26
+ };
27
+
28
+ describe('DEFAULT_I18N_CONFIG', () => {
29
+ test('should have default locale en', () => {
30
+ expect(DEFAULT_I18N_CONFIG.defaultLocale).toBe('en');
31
+ });
32
+
33
+ test('should have English locale configured', () => {
34
+ expect(DEFAULT_I18N_CONFIG.locales).toHaveLength(1);
35
+ expect(DEFAULT_I18N_CONFIG.locales[0].code).toBe('en');
36
+ });
37
+ });
38
+
39
+ describe('getLocaleCodes', () => {
40
+ test('should extract locale codes from config', () => {
41
+ const codes = getLocaleCodes(testConfig);
42
+ expect(codes).toEqual(['en', 'pl', 'de']);
43
+ });
44
+
45
+ test('should handle empty locales array', () => {
46
+ const config: I18nConfig = { defaultLocale: 'en', locales: [] };
47
+ expect(getLocaleCodes(config)).toEqual([]);
48
+ });
49
+ });
50
+
51
+ describe('findLocaleByCode', () => {
52
+ test('should find locale by code', () => {
53
+ const locale = findLocaleByCode(testConfig, 'pl');
54
+ expect(locale?.code).toBe('pl');
55
+ expect(locale?.nativeName).toBe('Polski');
56
+ });
57
+
58
+ test('should return undefined for non-existent code', () => {
59
+ expect(findLocaleByCode(testConfig, 'fr')).toBeUndefined();
60
+ });
61
+ });
62
+
63
+ describe('isValidLocaleCode', () => {
64
+ test('should return true for valid codes', () => {
65
+ expect(isValidLocaleCode(testConfig, 'en')).toBe(true);
66
+ expect(isValidLocaleCode(testConfig, 'pl')).toBe(true);
67
+ expect(isValidLocaleCode(testConfig, 'de')).toBe(true);
68
+ });
69
+
70
+ test('should return false for invalid codes', () => {
71
+ expect(isValidLocaleCode(testConfig, 'fr')).toBe(false);
72
+ expect(isValidLocaleCode(testConfig, 'es')).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe('migrateI18nConfig', () => {
77
+ test('should migrate old string array format', () => {
78
+ const oldConfig = {
79
+ defaultLocale: 'en',
80
+ locales: ['en', 'pl', 'de'],
81
+ };
82
+
83
+ const migrated = migrateI18nConfig(oldConfig);
84
+ expect(migrated.defaultLocale).toBe('en');
85
+ expect(migrated.locales).toHaveLength(3);
86
+ expect(migrated.locales[0].code).toBe('en');
87
+ expect(migrated.locales[1].code).toBe('pl');
88
+ });
89
+
90
+ test('should preserve new format config', () => {
91
+ const migrated = migrateI18nConfig(testConfig);
92
+ expect(migrated).toEqual(testConfig);
93
+ });
94
+
95
+ test('should return default config for invalid input', () => {
96
+ expect(migrateI18nConfig(null)).toEqual(DEFAULT_I18N_CONFIG);
97
+ expect(migrateI18nConfig(undefined)).toEqual(DEFAULT_I18N_CONFIG);
98
+ expect(migrateI18nConfig('invalid')).toEqual(DEFAULT_I18N_CONFIG);
99
+ });
100
+
101
+ test('should return default config for missing locales', () => {
102
+ const invalidConfig = { defaultLocale: 'en' };
103
+ expect(migrateI18nConfig(invalidConfig)).toEqual(DEFAULT_I18N_CONFIG);
104
+ });
105
+ });
106
+
107
+ describe('isI18nValue', () => {
108
+ test('should return true for valid I18nValue', () => {
109
+ const value: I18nValue = {
110
+ _i18n: true,
111
+ en: 'Hello',
112
+ pl: 'Cze[',
113
+ };
114
+ expect(isI18nValue(value)).toBe(true);
115
+ });
116
+
117
+ test('should return false for regular objects', () => {
118
+ expect(isI18nValue({ text: 'Hello' })).toBe(false);
119
+ expect(isI18nValue({ en: 'Hello' })).toBe(false);
120
+ });
121
+
122
+ test('should return false for non-objects', () => {
123
+ expect(isI18nValue(null)).toBe(false);
124
+ expect(isI18nValue(undefined)).toBe(false);
125
+ expect(isI18nValue('string')).toBe(false);
126
+ expect(isI18nValue(123)).toBe(false);
127
+ expect(isI18nValue([])).toBe(false);
128
+ });
129
+ });
130
+
131
+ describe('resolveTranslation', () => {
132
+ const i18nValue: I18nValue = {
133
+ _i18n: true,
134
+ en: 'Hello',
135
+ pl: 'Cze[',
136
+ de: 'Hallo',
137
+ };
138
+
139
+ test('should resolve exact locale match', () => {
140
+ expect(resolveTranslation(i18nValue, 'pl', testConfig)).toBe('Cze[');
141
+ expect(resolveTranslation(i18nValue, 'de', testConfig)).toBe('Hallo');
142
+ });
143
+
144
+ test('should fallback to default locale', () => {
145
+ const value: I18nValue = {
146
+ _i18n: true,
147
+ en: 'Hello',
148
+ pl: 'Cze[',
149
+ };
150
+ expect(resolveTranslation(value, 'fr', testConfig)).toBe('Hello');
151
+ });
152
+
153
+ test('should fallback to first available translation', () => {
154
+ const value: I18nValue = {
155
+ _i18n: true,
156
+ pl: 'Cze[',
157
+ };
158
+ expect(resolveTranslation(value, 'fr', testConfig)).toBe('Cze[');
159
+ });
160
+
161
+ test('should return empty string if no translation available', () => {
162
+ const value: I18nValue = { _i18n: true };
163
+ expect(resolveTranslation(value, 'en', testConfig)).toBe('');
164
+ });
165
+ });
166
+
167
+ describe('resolveI18nValue', () => {
168
+ test('should resolve I18nValue', () => {
169
+ const value: I18nValue = {
170
+ _i18n: true,
171
+ en: 'Hello',
172
+ pl: 'Cze[',
173
+ };
174
+ expect(resolveI18nValue(value, 'pl', testConfig)).toBe('Cze[');
175
+ });
176
+
177
+ test('should return non-I18nValue unchanged', () => {
178
+ expect(resolveI18nValue('Hello', 'pl', testConfig)).toBe('Hello');
179
+ expect(resolveI18nValue(123, 'pl', testConfig)).toBe(123);
180
+ expect(resolveI18nValue(null, 'pl', testConfig)).toBeNull();
181
+ });
182
+ });
183
+
184
+ describe('resolveI18nInProps', () => {
185
+ test('should resolve I18nValue props', () => {
186
+ const props = {
187
+ title: { _i18n: true, en: 'Hello', pl: 'Cze[' },
188
+ count: 5,
189
+ };
190
+
191
+ const resolved = resolveI18nInProps(props, 'pl', testConfig);
192
+ expect(resolved.title).toBe('Cze[');
193
+ expect(resolved.count).toBe(5);
194
+ });
195
+
196
+ test('should resolve I18nValue in arrays', () => {
197
+ const props = {
198
+ items: [
199
+ { _i18n: true, en: 'Item 1', pl: 'Pozycja 1' },
200
+ { _i18n: true, en: 'Item 2', pl: 'Pozycja 2' },
201
+ ],
202
+ };
203
+
204
+ const resolved = resolveI18nInProps(props, 'pl', testConfig);
205
+ expect(resolved.items).toEqual(['Pozycja 1', 'Pozycja 2']);
206
+ });
207
+
208
+ test('should recursively resolve nested objects', () => {
209
+ const props = {
210
+ header: {
211
+ title: { _i18n: true, en: 'Welcome', pl: 'Witaj' },
212
+ subtitle: { _i18n: true, en: 'Hello', pl: 'Cze[' },
213
+ },
214
+ };
215
+
216
+ const resolved = resolveI18nInProps(props, 'pl', testConfig);
217
+ expect((resolved.header as any).title).toBe('Witaj');
218
+ expect((resolved.header as any).subtitle).toBe('Cze[');
219
+ });
220
+
221
+ test('should handle mixed content', () => {
222
+ const props = {
223
+ title: { _i18n: true, en: 'Hello', pl: 'Cze[' },
224
+ count: 5,
225
+ enabled: true,
226
+ data: { value: 100 },
227
+ };
228
+
229
+ const resolved = resolveI18nInProps(props, 'en', testConfig);
230
+ expect(resolved.title).toBe('Hello');
231
+ expect(resolved.count).toBe(5);
232
+ expect(resolved.enabled).toBe(true);
233
+ expect((resolved.data as any).value).toBe(100);
234
+ });
235
+ });
236
+
237
+ describe('extractLocaleFromPath', () => {
238
+ test('should extract locale from path with locale prefix', () => {
239
+ const result = extractLocaleFromPath('/pl/about', testConfig);
240
+ expect(result.locale).toBe('pl');
241
+ expect(result.pathWithoutLocale).toBe('/about');
242
+ });
243
+
244
+ test('should handle root path with locale', () => {
245
+ const result = extractLocaleFromPath('/de', testConfig);
246
+ expect(result.locale).toBe('de');
247
+ expect(result.pathWithoutLocale).toBe('/');
248
+ });
249
+
250
+ test('should return null locale for path without prefix', () => {
251
+ const result = extractLocaleFromPath('/about', testConfig);
252
+ expect(result.locale).toBeNull();
253
+ expect(result.pathWithoutLocale).toBe('/about');
254
+ });
255
+
256
+ test('should handle path without leading slash', () => {
257
+ const result = extractLocaleFromPath('pl/about', testConfig);
258
+ expect(result.locale).toBe('pl');
259
+ expect(result.pathWithoutLocale).toBe('/about');
260
+ });
261
+
262
+ test('should not extract invalid locale codes', () => {
263
+ const result = extractLocaleFromPath('/fr/about', testConfig);
264
+ expect(result.locale).toBeNull();
265
+ expect(result.pathWithoutLocale).toBe('/fr/about');
266
+ });
267
+ });
268
+
269
+ describe('buildLocalizedPath', () => {
270
+ test('should build localized path', () => {
271
+ expect(buildLocalizedPath('/about', 'pl')).toBe('/pl/about');
272
+ expect(buildLocalizedPath('/contact', 'de')).toBe('/de/contact');
273
+ });
274
+
275
+ test('should handle root path', () => {
276
+ expect(buildLocalizedPath('/', 'pl')).toBe('/pl');
277
+ });
278
+
279
+ test('should handle path without leading slash', () => {
280
+ expect(buildLocalizedPath('about', 'pl')).toBe('/pl/about');
281
+ });
282
+ });
283
+
284
+ describe('parseLocaleFromPath', () => {
285
+ test('should parse locale and return context', () => {
286
+ const context = parseLocaleFromPath('/pl/about', testConfig);
287
+ expect(context.locale).toBe('pl');
288
+ expect(context.pathWithoutLocale).toBe('/about');
289
+ expect(context.isDefaultLocale).toBe(false);
290
+ });
291
+
292
+ test('should use default locale when no prefix', () => {
293
+ const context = parseLocaleFromPath('/about', testConfig);
294
+ expect(context.locale).toBe('en');
295
+ expect(context.pathWithoutLocale).toBe('/about');
296
+ expect(context.isDefaultLocale).toBe(true);
297
+ });
298
+
299
+ test('should handle default locale path', () => {
300
+ const context = parseLocaleFromPath('/en/about', testConfig);
301
+ expect(context.locale).toBe('en');
302
+ expect(context.pathWithoutLocale).toBe('/about');
303
+ expect(context.isDefaultLocale).toBe(true);
304
+ });
305
+
306
+ test('should handle root path', () => {
307
+ const context = parseLocaleFromPath('/', testConfig);
308
+ expect(context.locale).toBe('en');
309
+ expect(context.pathWithoutLocale).toBe('/');
310
+ expect(context.isDefaultLocale).toBe(true);
311
+ });
312
+ });
313
+ });
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Internationalization (i18n) utilities
3
+ * Handles inline translation resolution for component props
4
+ */
5
+
6
+ import type { I18nValue, I18nConfig, LocaleConfig } from './types/components';
7
+
8
+ /**
9
+ * Default i18n configuration
10
+ */
11
+ export const DEFAULT_I18N_CONFIG: I18nConfig = {
12
+ defaultLocale: 'en',
13
+ locales: [
14
+ { code: 'en', name: 'English', nativeName: 'English', langTag: 'en-US' }
15
+ ],
16
+ };
17
+
18
+ // ============================================
19
+ // Locale helper functions
20
+ // ============================================
21
+
22
+ /**
23
+ * Get array of locale codes from config
24
+ */
25
+ export function getLocaleCodes(config: I18nConfig): string[] {
26
+ return config.locales.map(loc => loc.code);
27
+ }
28
+
29
+ /**
30
+ * Find a locale config by its code
31
+ */
32
+ export function findLocaleByCode(config: I18nConfig, code: string): LocaleConfig | undefined {
33
+ return config.locales.find(loc => loc.code === code);
34
+ }
35
+
36
+ /**
37
+ * Check if a locale code is valid/exists in config
38
+ */
39
+ export function isValidLocaleCode(config: I18nConfig, code: string): boolean {
40
+ return config.locales.some(loc => loc.code === code);
41
+ }
42
+
43
+ // ============================================
44
+ // Config Migration (old string[] -> new LocaleConfig[])
45
+ // ============================================
46
+
47
+ /**
48
+ * Convert old locale format (string) to new format (LocaleConfig)
49
+ */
50
+ function migrateLocaleString(code: string): LocaleConfig {
51
+ const upperCode = code.toUpperCase();
52
+ return {
53
+ code: code.toLowerCase(),
54
+ name: upperCode,
55
+ nativeName: upperCode,
56
+ langTag: `${code.toLowerCase()}-${upperCode}`,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Check if locales array is in old string format
62
+ */
63
+ function isOldLocaleFormat(locales: unknown): locales is string[] {
64
+ return Array.isArray(locales) && locales.length > 0 && typeof locales[0] === 'string';
65
+ }
66
+
67
+ /**
68
+ * Migrate i18n config from old format to new format
69
+ * Old: { defaultLocale: "en", locales: ["en", "pl"] }
70
+ * New: { defaultLocale: "en", locales: [{ code: "en", name: "EN", ... }] }
71
+ */
72
+ export function migrateI18nConfig(i18n: unknown): I18nConfig {
73
+ if (!i18n || typeof i18n !== 'object') {
74
+ return DEFAULT_I18N_CONFIG;
75
+ }
76
+
77
+ const config = i18n as Record<string, unknown>;
78
+ const defaultLocale = (config.defaultLocale as string) || 'en';
79
+ const locales = config.locales;
80
+
81
+ if (!locales || !Array.isArray(locales)) {
82
+ return DEFAULT_I18N_CONFIG;
83
+ }
84
+
85
+ // Migrate old string[] format
86
+ if (isOldLocaleFormat(locales)) {
87
+ return {
88
+ defaultLocale,
89
+ locales: locales.map(migrateLocaleString),
90
+ };
91
+ }
92
+
93
+ // Already in new format
94
+ return {
95
+ defaultLocale,
96
+ locales: locales as LocaleConfig[],
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Type guard to check if a value is an I18nValue object
102
+ */
103
+ export function isI18nValue(value: unknown): value is I18nValue {
104
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
105
+ return false;
106
+ }
107
+ return '_i18n' in value && (value as Record<string, unknown>)._i18n === true;
108
+ }
109
+
110
+ /**
111
+ * Resolve a single translation value for the given locale
112
+ * Fallback order: exact locale -> default locale -> first available -> empty string
113
+ */
114
+ export function resolveTranslation(
115
+ value: I18nValue,
116
+ locale: string,
117
+ config: I18nConfig
118
+ ): string {
119
+ // Try exact locale match
120
+ if (typeof value[locale] === 'string') {
121
+ return value[locale] as string;
122
+ }
123
+
124
+ // Try default locale
125
+ if (typeof value[config.defaultLocale] === 'string') {
126
+ return value[config.defaultLocale] as string;
127
+ }
128
+
129
+ // Get first available translation (skip _i18n marker)
130
+ for (const key of Object.keys(value)) {
131
+ if (key !== '_i18n' && typeof value[key] === 'string') {
132
+ return value[key] as string;
133
+ }
134
+ }
135
+
136
+ return '';
137
+ }
138
+
139
+ /**
140
+ * Resolve a value that might be an I18nValue or a regular value
141
+ * Returns the original value if not an I18nValue
142
+ */
143
+ export function resolveI18nValue(
144
+ value: unknown,
145
+ locale: string,
146
+ config: I18nConfig
147
+ ): unknown {
148
+ if (isI18nValue(value)) {
149
+ return resolveTranslation(value, locale, config);
150
+ }
151
+ return value;
152
+ }
153
+
154
+ /**
155
+ * Recursively resolve all I18nValue objects in a props object
156
+ */
157
+ export function resolveI18nInProps(
158
+ props: Record<string, unknown>,
159
+ locale: string,
160
+ config: I18nConfig
161
+ ): Record<string, unknown> {
162
+ const resolved: Record<string, unknown> = {};
163
+
164
+ for (const [key, value] of Object.entries(props)) {
165
+ if (isI18nValue(value)) {
166
+ resolved[key] = resolveTranslation(value, locale, config);
167
+ } else if (Array.isArray(value)) {
168
+ resolved[key] = value.map((item) =>
169
+ isI18nValue(item) ? resolveTranslation(item, locale, config) : item
170
+ );
171
+ } else if (typeof value === 'object' && value !== null) {
172
+ // Recursively resolve nested objects (but not I18nValue which is already handled)
173
+ resolved[key] = resolveI18nInProps(
174
+ value as Record<string, unknown>,
175
+ locale,
176
+ config
177
+ );
178
+ } else {
179
+ resolved[key] = value;
180
+ }
181
+ }
182
+
183
+ return resolved;
184
+ }
185
+
186
+ /**
187
+ * Extract locale from URL path prefix
188
+ * e.g., '/en/about' -> 'en', '/pl/' -> 'pl', '/about' -> null
189
+ */
190
+ export function extractLocaleFromPath(
191
+ path: string,
192
+ config: I18nConfig
193
+ ): { locale: string | null; pathWithoutLocale: string } {
194
+ // Remove leading slash and split
195
+ const cleanPath = path.startsWith('/') ? path.slice(1) : path;
196
+ const segments = cleanPath.split('/');
197
+
198
+ if (segments.length > 0 && isValidLocaleCode(config, segments[0])) {
199
+ const locale = segments[0];
200
+ const pathWithoutLocale = '/' + segments.slice(1).join('/');
201
+ return { locale, pathWithoutLocale: pathWithoutLocale || '/' };
202
+ }
203
+
204
+ return { locale: null, pathWithoutLocale: path };
205
+ }
206
+
207
+ /**
208
+ * Build a localized path
209
+ * e.g., ('/about', 'pl') -> '/pl/about'
210
+ */
211
+ export function buildLocalizedPath(path: string, locale: string): string {
212
+ const cleanPath = path.startsWith('/') ? path : '/' + path;
213
+ return `/${locale}${cleanPath === '/' ? '' : cleanPath}`;
214
+ }
215
+
216
+ /**
217
+ * Locale context containing resolved locale info
218
+ */
219
+ export interface LocaleContext {
220
+ /** The effective locale (never null) */
221
+ locale: string;
222
+ /** Path with locale prefix removed */
223
+ pathWithoutLocale: string;
224
+ /** Whether the current locale is the default */
225
+ isDefaultLocale: boolean;
226
+ }
227
+
228
+ /**
229
+ * Parse locale from path and return full context with effective locale
230
+ * Combines extractLocaleFromPath + defaultLocale resolution in one call
231
+ */
232
+ export function parseLocaleFromPath(
233
+ path: string,
234
+ config: I18nConfig
235
+ ): LocaleContext {
236
+ const { locale, pathWithoutLocale } = extractLocaleFromPath(path, config);
237
+ const effectiveLocale = locale || config.defaultLocale;
238
+
239
+ return {
240
+ locale: effectiveLocale,
241
+ pathWithoutLocale,
242
+ isDefaultLocale: effectiveLocale === config.defaultLocale
243
+ };
244
+ }
245
+
246
+ // ============================================
247
+ // Client-side locale persistence utilities
248
+ // ============================================
249
+
250
+ const LOCALE_STORAGE_KEY = 'uplo_locale_preference';
251
+
252
+ /**
253
+ * Get stored locale preference from localStorage (client-side only)
254
+ */
255
+ export function getStoredLocale(): string | null {
256
+ if (typeof window === 'undefined') return null;
257
+ try {
258
+ return localStorage.getItem(LOCALE_STORAGE_KEY);
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Store locale preference to localStorage (client-side only)
266
+ */
267
+ export function setStoredLocale(locale: string): void {
268
+ if (typeof window === 'undefined') return;
269
+ try {
270
+ localStorage.setItem(LOCALE_STORAGE_KEY, locale);
271
+ } catch {
272
+ // Storage not available
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Clear stored locale preference (client-side only)
278
+ */
279
+ export function clearStoredLocale(): void {
280
+ if (typeof window === 'undefined') return;
281
+ try {
282
+ localStorage.removeItem(LOCALE_STORAGE_KEY);
283
+ } catch {
284
+ // Storage not available
285
+ }
286
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @meno/core shared exports
3
+ * Re-exports all types, constants, and utilities from shared modules
4
+ */
5
+
6
+ // Types
7
+ export * from './types';
8
+
9
+ // Constants
10
+ export * from './constants';
11
+
12
+ // Path utilities
13
+ export * from './pathArrayUtils';
14
+ export * from './treePathUtils';
15
+ export * from './paths';
16
+
17
+ // Node utilities
18
+ export * from './nodeUtils';
19
+
20
+ // Style utilities
21
+ export * from './breakpoints';
22
+ export * from './styleUtils';
23
+ export * from './cssProperties';
24
+ export * from './cssGeneration';
25
+ export * from './colorProperties';
26
+ export * from './utilityClassMapper';
27
+
28
+ // i18n utilities
29
+ export * from './i18n';
30
+
31
+ // Tree utilities
32
+ export * from './tree/PathBuilder';
33
+
34
+ // Validation
35
+ export * from './validation';
36
+
37
+ // Utils
38
+ export * from './utils';
39
+
40
+ // Registry
41
+ export * from './registry';
42
+
43
+ // Theme defaults
44
+ export * from './themeDefaults';
45
+
46
+ // Color variable utilities
47
+ export * from './colorVariableUtils';
48
+
49
+ // Responsive style utilities
50
+ export * from './responsiveStyleUtils';
@@ -0,0 +1,9 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+
3
+ describe('contentProvider', () => {
4
+ test('placeholder test for coverage', () => {
5
+ // Content provider interface - no logic to test
6
+ // This placeholder ensures the file appears in coverage reports
7
+ expect(true).toBe(true);
8
+ });
9
+ });