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.
- package/bin/cli.ts +281 -0
- package/build-static.ts +298 -0
- package/bunfig.toml +39 -0
- package/entries/client-router.tsx +111 -0
- package/entries/server-router.tsx +71 -0
- package/lib/client/ClientInitializer.test.ts +9 -0
- package/lib/client/ClientInitializer.test.ts.skip +92 -0
- package/lib/client/ClientInitializer.ts +60 -0
- package/lib/client/ErrorBoundary.test.tsx +595 -0
- package/lib/client/ErrorBoundary.tsx +230 -0
- package/lib/client/componentRegistry.test.ts +165 -0
- package/lib/client/componentRegistry.ts +18 -0
- package/lib/client/contexts/ThemeContext.tsx +73 -0
- package/lib/client/core/ComponentBuilder.test.ts +677 -0
- package/lib/client/core/ComponentBuilder.ts +660 -0
- package/lib/client/core/ComponentRenderer.test.tsx +176 -0
- package/lib/client/core/ComponentRenderer.tsx +83 -0
- package/lib/client/core/cmsTemplateProcessor.ts +129 -0
- package/lib/client/elementRegistry.ts +81 -0
- package/lib/client/hmr/HMRManager.tsx +179 -0
- package/lib/client/hmr/index.ts +5 -0
- package/lib/client/hmrWebSocket.test.ts +9 -0
- package/lib/client/hmrWebSocket.ts +250 -0
- package/lib/client/hooks/useColorVariables.test.ts +166 -0
- package/lib/client/hooks/useColorVariables.ts +249 -0
- package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
- package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
- package/lib/client/hydration/HydrationUtils.test.ts +154 -0
- package/lib/client/hydration/HydrationUtils.ts +35 -0
- package/lib/client/i18nConfigService.test.ts +74 -0
- package/lib/client/i18nConfigService.ts +78 -0
- package/lib/client/index.ts +56 -0
- package/lib/client/navigation.test.ts +441 -0
- package/lib/client/navigation.ts +23 -0
- package/lib/client/responsiveStyleResolver.test.ts +491 -0
- package/lib/client/responsiveStyleResolver.ts +184 -0
- package/lib/client/routing/RouteLoader.test.ts +635 -0
- package/lib/client/routing/RouteLoader.ts +347 -0
- package/lib/client/routing/Router.tsx +382 -0
- package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
- package/lib/client/scripts/ScriptExecutor.ts +171 -0
- package/lib/client/scripts/formHandler.ts +103 -0
- package/lib/client/styleProcessor.test.ts +126 -0
- package/lib/client/styleProcessor.ts +92 -0
- package/lib/client/styles/StyleInjector.test.ts +354 -0
- package/lib/client/styles/StyleInjector.ts +154 -0
- package/lib/client/templateEngine.test.ts +660 -0
- package/lib/client/templateEngine.ts +667 -0
- package/lib/client/theme.test.ts +173 -0
- package/lib/client/theme.ts +159 -0
- package/lib/client/utils/toast.ts +46 -0
- package/lib/server/createServer.ts +170 -0
- package/lib/server/cssGenerator.test.ts +172 -0
- package/lib/server/cssGenerator.ts +58 -0
- package/lib/server/fileWatcher.ts +134 -0
- package/lib/server/index.ts +55 -0
- package/lib/server/jsonLoader.test.ts +103 -0
- package/lib/server/jsonLoader.ts +350 -0
- package/lib/server/middleware/cors.test.ts +177 -0
- package/lib/server/middleware/cors.ts +69 -0
- package/lib/server/middleware/errorHandler.test.ts +208 -0
- package/lib/server/middleware/errorHandler.ts +63 -0
- package/lib/server/middleware/index.ts +9 -0
- package/lib/server/middleware/logger.test.ts +233 -0
- package/lib/server/middleware/logger.ts +99 -0
- package/lib/server/pageCache.test.ts +167 -0
- package/lib/server/pageCache.ts +97 -0
- package/lib/server/projectContext.ts +51 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
- package/lib/server/providers/fileSystemPageProvider.ts +83 -0
- package/lib/server/routes/api/cms.test.ts +177 -0
- package/lib/server/routes/api/cms.ts +82 -0
- package/lib/server/routes/api/colors.ts +59 -0
- package/lib/server/routes/api/components.ts +70 -0
- package/lib/server/routes/api/config.test.ts +9 -0
- package/lib/server/routes/api/config.ts +28 -0
- package/lib/server/routes/api/core-routes.ts +182 -0
- package/lib/server/routes/api/functions.ts +170 -0
- package/lib/server/routes/api/index.ts +69 -0
- package/lib/server/routes/api/pages.ts +95 -0
- package/lib/server/routes/api/shared.test.ts +81 -0
- package/lib/server/routes/api/shared.ts +31 -0
- package/lib/server/routes/editor.test.ts +9 -0
- package/lib/server/routes/index.ts +104 -0
- package/lib/server/routes/pages.ts +161 -0
- package/lib/server/routes/static.ts +107 -0
- package/lib/server/services/ColorService.ts +193 -0
- package/lib/server/services/cmsService.test.ts +388 -0
- package/lib/server/services/cmsService.ts +296 -0
- package/lib/server/services/componentService.test.ts +276 -0
- package/lib/server/services/componentService.ts +346 -0
- package/lib/server/services/configService.ts +156 -0
- package/lib/server/services/fileWatcherService.ts +67 -0
- package/lib/server/services/index.ts +10 -0
- package/lib/server/services/pageService.test.ts +258 -0
- package/lib/server/services/pageService.ts +240 -0
- package/lib/server/ssrRenderer.test.ts +1005 -0
- package/lib/server/ssrRenderer.ts +878 -0
- package/lib/server/utilityClassGenerator.ts +11 -0
- package/lib/server/utils/index.ts +5 -0
- package/lib/server/utils/jsonLineMapper.test.ts +100 -0
- package/lib/server/utils/jsonLineMapper.ts +166 -0
- package/lib/server/validateStyleCoverage.test.ts +9 -0
- package/lib/server/validateStyleCoverage.ts +167 -0
- package/lib/server/websocketManager.test.ts +9 -0
- package/lib/server/websocketManager.ts +95 -0
- package/lib/shared/attributeNodeUtils.test.ts +152 -0
- package/lib/shared/attributeNodeUtils.ts +50 -0
- package/lib/shared/breakpoints.test.ts +166 -0
- package/lib/shared/breakpoints.ts +65 -0
- package/lib/shared/colorProperties.test.ts +111 -0
- package/lib/shared/colorProperties.ts +40 -0
- package/lib/shared/colorVariableUtils.test.ts +319 -0
- package/lib/shared/colorVariableUtils.ts +97 -0
- package/lib/shared/constants.test.ts +175 -0
- package/lib/shared/constants.ts +116 -0
- package/lib/shared/cssGeneration.ts +481 -0
- package/lib/shared/cssProperties.test.ts +252 -0
- package/lib/shared/cssProperties.ts +338 -0
- package/lib/shared/elementUtils.test.ts +245 -0
- package/lib/shared/elementUtils.ts +90 -0
- package/lib/shared/fontLoader.ts +97 -0
- package/lib/shared/i18n.test.ts +313 -0
- package/lib/shared/i18n.ts +286 -0
- package/lib/shared/index.ts +50 -0
- package/lib/shared/interfaces/contentProvider.test.ts +9 -0
- package/lib/shared/interfaces/contentProvider.ts +121 -0
- package/lib/shared/nodeUtils.test.ts +320 -0
- package/lib/shared/nodeUtils.ts +220 -0
- package/lib/shared/pathArrayUtils.test.ts +315 -0
- package/lib/shared/pathArrayUtils.ts +17 -0
- package/lib/shared/pathUtils.test.ts +260 -0
- package/lib/shared/pathUtils.ts +244 -0
- package/lib/shared/paths/Path.test.ts +74 -0
- package/lib/shared/paths/Path.ts +23 -0
- package/lib/shared/paths/PathConverter.test.ts +232 -0
- package/lib/shared/paths/PathConverter.ts +141 -0
- package/lib/shared/paths/PathUtils.ts +290 -0
- package/lib/shared/paths/PathValidator.test.ts +193 -0
- package/lib/shared/paths/PathValidator.ts +53 -0
- package/lib/shared/paths/index.ts +48 -0
- package/lib/shared/propResolver.test.ts +639 -0
- package/lib/shared/propResolver.ts +124 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
- package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
- package/lib/shared/registry/ClientRegistry.test.ts +26 -0
- package/lib/shared/registry/ClientRegistry.ts +15 -0
- package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
- package/lib/shared/registry/ComponentRegistry.ts +100 -0
- package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
- package/lib/shared/registry/NodeTypeManager.ts +94 -0
- package/lib/shared/registry/RegistryManager.test.ts +58 -0
- package/lib/shared/registry/RegistryManager.ts +60 -0
- package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
- package/lib/shared/registry/SSRRegistry.test.ts +26 -0
- package/lib/shared/registry/SSRRegistry.ts +15 -0
- package/lib/shared/registry/createNodeType.ts +175 -0
- package/lib/shared/registry/defineNodeType.ts +73 -0
- package/lib/shared/registry/fieldPresets.ts +109 -0
- package/lib/shared/registry/index.ts +50 -0
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
- package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
- package/lib/shared/registry/nodeTypes/index.ts +75 -0
- package/lib/shared/responsiveScaling.test.ts +268 -0
- package/lib/shared/responsiveScaling.ts +194 -0
- package/lib/shared/responsiveStyleUtils.test.ts +300 -0
- package/lib/shared/responsiveStyleUtils.ts +139 -0
- package/lib/shared/slugTranslator.test.ts +325 -0
- package/lib/shared/slugTranslator.ts +177 -0
- package/lib/shared/styleNodeUtils.test.ts +132 -0
- package/lib/shared/styleNodeUtils.ts +102 -0
- package/lib/shared/styleUtils.test.ts +238 -0
- package/lib/shared/styleUtils.ts +63 -0
- package/lib/shared/themeDefaults.test.ts +113 -0
- package/lib/shared/themeDefaults.ts +103 -0
- package/lib/shared/tree/PathBuilder.ts +383 -0
- package/lib/shared/treePathUtils.test.ts +539 -0
- package/lib/shared/treePathUtils.ts +339 -0
- package/lib/shared/types/api.ts +58 -0
- package/lib/shared/types/cms.ts +95 -0
- package/lib/shared/types/colors.ts +45 -0
- package/lib/shared/types/components.ts +121 -0
- package/lib/shared/types/errors.test.ts +103 -0
- package/lib/shared/types/errors.ts +69 -0
- package/lib/shared/types/index.ts +96 -0
- package/lib/shared/types/nodes.ts +20 -0
- package/lib/shared/types/rendering.ts +61 -0
- package/lib/shared/types/styles.ts +38 -0
- package/lib/shared/types.ts +11 -0
- package/lib/shared/utilityClassConfig.ts +287 -0
- package/lib/shared/utilityClassMapper.test.ts +140 -0
- package/lib/shared/utilityClassMapper.ts +229 -0
- package/lib/shared/utils/fileUtils.test.ts +99 -0
- package/lib/shared/utils/fileUtils.ts +56 -0
- package/lib/shared/utils.test.ts +261 -0
- package/lib/shared/utils.ts +84 -0
- package/lib/shared/validation/index.ts +7 -0
- package/lib/shared/validation/propValidator.test.ts +178 -0
- package/lib/shared/validation/propValidator.ts +238 -0
- package/lib/shared/validation/schemas.test.ts +177 -0
- package/lib/shared/validation/schemas.ts +401 -0
- package/lib/shared/validation/validators.test.ts +109 -0
- package/lib/shared/validation/validators.ts +304 -0
- package/lib/test-utils/dom-setup.ts +55 -0
- package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
- package/lib/test-utils/factories/DomMockFactory.ts +487 -0
- package/lib/test-utils/factories/EventMockFactory.ts +244 -0
- package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
- package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
- package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
- package/lib/test-utils/factories/index.ts +11 -0
- package/lib/test-utils/fixtures.ts +134 -0
- package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
- package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
- package/lib/test-utils/helpers/index.ts +6 -0
- package/lib/test-utils/helpers.test.ts +73 -0
- package/lib/test-utils/helpers.ts +90 -0
- package/lib/test-utils/index.ts +17 -0
- package/lib/test-utils/mockFactories.ts +92 -0
- package/lib/test-utils/mocks.ts +341 -0
- package/package.json +38 -0
- package/templates/index-router.html +34 -0
- package/tsconfig.json +14 -0
- 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
|
+
});
|