meno-core 1.1.7 → 1.1.8

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 (52) hide show
  1. package/dist/bin/cli.js +1 -1
  2. package/dist/chunks/{chunk-XTKNX4FW.js → chunk-AMNAKQAI.js} +25 -11
  3. package/dist/chunks/{chunk-XTKNX4FW.js.map → chunk-AMNAKQAI.js.map} +2 -2
  4. package/dist/chunks/{chunk-YMBUSHII.js → chunk-CG3O7H5V.js} +9 -4
  5. package/dist/chunks/chunk-CG3O7H5V.js.map +7 -0
  6. package/dist/chunks/{chunk-4ZRU52J2.js → chunk-EJ6QOX7C.js} +14 -10
  7. package/dist/chunks/chunk-EJ6QOX7C.js.map +7 -0
  8. package/dist/chunks/{chunk-KX2LPIZZ.js → chunk-EKB7GGQK.js} +223 -38
  9. package/dist/chunks/chunk-EKB7GGQK.js.map +7 -0
  10. package/dist/chunks/{chunk-WOJS25K3.js → chunk-G6OXV2IV.js} +2 -2
  11. package/dist/lib/client/index.js +9 -5
  12. package/dist/lib/client/index.js.map +2 -2
  13. package/dist/lib/server/index.js +14 -5
  14. package/dist/lib/server/index.js.map +2 -2
  15. package/dist/lib/shared/index.js +18 -6
  16. package/dist/lib/shared/index.js.map +2 -2
  17. package/lib/client/core/ComponentBuilder.test.ts +40 -0
  18. package/lib/client/core/ComponentBuilder.ts +8 -1
  19. package/lib/server/routes/static.test.ts +67 -0
  20. package/lib/server/routes/static.ts +9 -1
  21. package/lib/server/ssr/htmlGenerator.ts +7 -0
  22. package/lib/server/ssr/ssrRenderer.test.ts +31 -0
  23. package/lib/server/ssr/ssrRenderer.ts +8 -1
  24. package/lib/shared/cssGeneration.test.ts +150 -9
  25. package/lib/shared/cssGeneration.ts +116 -31
  26. package/lib/shared/cssProperties.ts +40 -14
  27. package/lib/shared/i18n.test.ts +21 -0
  28. package/lib/shared/i18n.ts +24 -10
  29. package/lib/shared/nodeUtils.test.ts +22 -0
  30. package/lib/shared/nodeUtils.ts +39 -4
  31. package/lib/shared/permissions.test.ts +46 -6
  32. package/lib/shared/permissions.ts +14 -2
  33. package/lib/shared/pxToRem.ts +2 -0
  34. package/lib/shared/responsiveScaling.test.ts +19 -0
  35. package/lib/shared/responsiveScaling.ts +8 -3
  36. package/lib/shared/styleUtils.ts +53 -0
  37. package/lib/shared/tailwindThemeScale.ts +91 -0
  38. package/lib/shared/types/components.ts +45 -7
  39. package/lib/shared/types/index.ts +1 -0
  40. package/lib/shared/types/permissions.ts +27 -11
  41. package/lib/shared/utilityClassMapper.test.ts +105 -0
  42. package/lib/shared/utilityClassMapper.ts +43 -5
  43. package/lib/shared/utilityClassNames.ts +14 -1
  44. package/lib/shared/validation/propValidator.test.ts +47 -0
  45. package/lib/shared/validation/propValidator.ts +8 -0
  46. package/lib/shared/validation/schemas.test.ts +149 -16
  47. package/lib/shared/validation/schemas.ts +101 -5
  48. package/package.json +1 -1
  49. package/dist/chunks/chunk-4ZRU52J2.js.map +0 -7
  50. package/dist/chunks/chunk-KX2LPIZZ.js.map +0 -7
  51. package/dist/chunks/chunk-YMBUSHII.js.map +0 -7
  52. /package/dist/chunks/{chunk-WOJS25K3.js.map → chunk-G6OXV2IV.js.map} +0 -0
@@ -6,6 +6,22 @@
6
6
  import { describe, test, expect } from 'bun:test';
7
7
  import { PropDefinitionSchema, ComponentNodeSchema, ComponentDefinitionSchema, PageDataSchema } from './schemas';
8
8
 
9
+ // Collect every message in a zod error tree (union branches included), so
10
+ // assertions don't depend on which branch zod happens to surface.
11
+ const allMessages = (error: import('zod').ZodError): string[] => {
12
+ const out: string[] = [];
13
+ const walk = (issues: import('zod').ZodIssue[]) => {
14
+ for (const issue of issues) {
15
+ out.push(issue.message);
16
+ if (issue.code === 'invalid_union') {
17
+ for (const ue of (issue as import('zod').ZodInvalidUnionIssue).unionErrors) walk(ue.errors);
18
+ }
19
+ }
20
+ };
21
+ walk(error.issues);
22
+ return out;
23
+ };
24
+
9
25
  describe('Schema Validation', () => {
10
26
  describe('Valid component definitions pass', () => {
11
27
  test('valid prop definition', () => {
@@ -202,22 +218,6 @@ describe('Schema Validation', () => {
202
218
  });
203
219
 
204
220
  describe('list prop authoring mistakes surface a clear, prop-named error', () => {
205
- // Collect every message in a zod error tree (union branches included), so
206
- // assertions don't depend on which branch zod happens to surface.
207
- const allMessages = (error: import('zod').ZodError): string[] => {
208
- const out: string[] = [];
209
- const walk = (issues: import('zod').ZodIssue[]) => {
210
- for (const issue of issues) {
211
- out.push(issue.message);
212
- if (issue.code === 'invalid_union') {
213
- for (const ue of (issue as import('zod').ZodInvalidUnionIssue).unionErrors) walk(ue.errors);
214
- }
215
- }
216
- };
217
- walk(error.issues);
218
- return out;
219
- };
220
-
221
221
  test('a bare-string `default` with no `itemSchema` is rejected with the list-prop requirement', () => {
222
222
  // The single most common first guess. The codec round-trips it and
223
223
  // `astro build` renders it, so the editor load path is the only guard.
@@ -279,6 +279,88 @@ describe('Schema Validation', () => {
279
279
  });
280
280
  });
281
281
 
282
+ describe('CMS field types are not prop types', () => {
283
+ // Component props (`PropTypeSchema`) and CMS fields (`CMSFieldTypeSchema`) are
284
+ // separate vocabularies that overlap on most names. Reaching for a CMS-only name
285
+ // in a `resolveProps(Astro, {…})` literal round-trips through the codec and builds
286
+ // fine under `astro build`, so this validator is the only place it surfaces — the
287
+ // message has to name the confusion, not just miss the enum.
288
+ test.each([
289
+ ['text', 'string'],
290
+ ['image', 'file'],
291
+ ['date', 'string'],
292
+ ])('`type: "%s"` is rejected with a pointer to "%s"', (cmsType, propType) => {
293
+ const result = PropDefinitionSchema.safeParse({ type: cmsType, default: '' });
294
+
295
+ expect(result.success).toBe(false);
296
+ if (!result.success) {
297
+ const messages = allMessages(result.error);
298
+ expect(messages.some((m) => m.includes(`"${cmsType}" is a CMS field type`))).toBe(true);
299
+ expect(messages.some((m) => m.includes(`use "${propType}"`))).toBe(true);
300
+ // Reported at the prop's own `type`, so the toast can name the prop.
301
+ expect(result.error.issues.some((i) => i.path.join('.') === 'type')).toBe(true);
302
+ }
303
+ });
304
+
305
+ test('a CMS-only type on a LIST ITEM field is blamed on that field, not on itemSchema/default', () => {
306
+ // Regression: the list branch used to collapse EVERY failure into "list prop
307
+ // requires `itemSchema` and an object-array `default`" — which is wrong and
308
+ // unactionable when itemSchema and default are both present and correct, and
309
+ // the real mistake is one word in one item field.
310
+ const propDef = {
311
+ type: 'list',
312
+ itemSchema: {
313
+ city: { type: 'string', default: 'City' },
314
+ address: { type: 'text', default: '' },
315
+ },
316
+ default: [{ city: 'City', address: '' }],
317
+ };
318
+
319
+ const result = PropDefinitionSchema.safeParse(propDef);
320
+
321
+ expect(result.success).toBe(false);
322
+ if (!result.success) {
323
+ const messages = allMessages(result.error);
324
+ expect(messages.some((m) => m.includes('"text" is a CMS field type'))).toBe(true);
325
+ expect(messages.some((m) => m.includes('list prop requires `itemSchema`'))).toBe(false);
326
+ expect(result.error.issues.some((i) => i.path.join('.') === 'itemSchema.address.type')).toBe(true);
327
+ }
328
+ });
329
+ });
330
+
331
+ describe('reference prop type', () => {
332
+ // `reference` is in the `PropType` union and the Studio props panel creates it,
333
+ // but the validator's enum used to omit it — so every component with a reference
334
+ // prop failed to open. Keep the two in sync.
335
+ test('a multi-select reference (ordered id array default) is valid', () => {
336
+ const propDef = { type: 'reference', collection: 'insights', multiple: true, default: [] };
337
+ expect(PropDefinitionSchema.safeParse(propDef).success).toBe(true);
338
+ });
339
+
340
+ test('a single reference (id string default) is valid', () => {
341
+ const propDef = { type: 'reference', collection: 'insights', default: 'my-post' };
342
+ expect(PropDefinitionSchema.safeParse(propDef).success).toBe(true);
343
+ });
344
+
345
+ test('a reference without a collection is rejected with the requirement', () => {
346
+ const result = PropDefinitionSchema.safeParse({ type: 'reference', default: '' });
347
+ expect(result.success).toBe(false);
348
+ if (!result.success) {
349
+ expect(allMessages(result.error).some((m) => m.includes('reference prop requires a `collection`'))).toBe(true);
350
+ }
351
+ });
352
+
353
+ test('a component whose prop is a reference opens (full ComponentDefinition path)', () => {
354
+ const componentFile = {
355
+ component: {
356
+ interface: { picks: { type: 'reference', collection: 'insights', multiple: true, default: [] } },
357
+ structure: { type: 'node', tag: 'div', children: [] },
358
+ },
359
+ };
360
+ expect(ComponentDefinitionSchema.safeParse(componentFile).success).toBe(true);
361
+ });
362
+ });
363
+
282
364
  describe('Missing required fields handled', () => {
283
365
  test('prop definition without default', () => {
284
366
  const propDef = {
@@ -337,6 +419,57 @@ describe('Schema Validation', () => {
337
419
  });
338
420
  });
339
421
 
422
+ describe('localized visibility (`if` as an _i18n value) validates', () => {
423
+ // A rejection here is silent data loss: the file wouldn't open in the editor and a
424
+ // save carrying per-locale visibility would be refused.
425
+ test('an _i18n object with boolean slots is accepted as an `if` condition', () => {
426
+ const node = {
427
+ type: 'node',
428
+ tag: 'div',
429
+ if: { _i18n: true, en: true, pl: false },
430
+ children: ['English only'],
431
+ };
432
+ expect(ComponentNodeSchema.safeParse(node).success).toBe(true);
433
+ });
434
+
435
+ test('it validates on every node type that carries an `if` (component instance, link)', () => {
436
+ expect(
437
+ ComponentNodeSchema.safeParse({
438
+ type: 'component',
439
+ component: 'Card',
440
+ if: { _i18n: true, en: false, pl: true },
441
+ props: { title: 'Hi' },
442
+ }).success,
443
+ ).toBe(true);
444
+ expect(
445
+ ComponentNodeSchema.safeParse({
446
+ type: 'link',
447
+ href: '/about',
448
+ if: { _i18n: true, en: true, pl: false },
449
+ children: ['About'],
450
+ }).success,
451
+ ).toBe(true);
452
+ });
453
+
454
+ test('a full component definition whose structure carries a localized `if` opens', () => {
455
+ const def = {
456
+ component: {
457
+ structure: {
458
+ type: 'node',
459
+ tag: 'section',
460
+ children: [{ type: 'node', tag: 'p', if: { _i18n: true, en: true, pl: false }, children: ['Localized'] }],
461
+ },
462
+ },
463
+ };
464
+ expect(ComponentDefinitionSchema.safeParse(def).success).toBe(true);
465
+ });
466
+
467
+ test('a bogus marker (_i18n not literally true) is rejected', () => {
468
+ const node = { type: 'node', tag: 'div', if: { _i18n: 'yes', en: true } };
469
+ expect(ComponentNodeSchema.safeParse(node).success).toBe(false);
470
+ });
471
+ });
472
+
340
473
  describe('meno-astro verbatim-code markers ({ _code, expr }) validate', () => {
341
474
  test('a _code child node is accepted (renders natively at build; nothing in preview)', () => {
342
475
  const node = {
@@ -13,7 +13,15 @@ import type { StyleObject, StyleMapping, LinkMapping, HtmlMapping } from '../typ
13
13
  import { NODE_TYPE } from '../constants';
14
14
 
15
15
  /**
16
- * Prop type schema - validates PropType values (excluding list for base definition)
16
+ * Prop type schema the base (scalar) PropType values. Mirrors
17
+ * `BasePropDefinition['type']` = `Exclude<PropType, 'list' | 'reference'>`: the two
18
+ * structured types are excluded because they carry their own required fields
19
+ * (`itemSchema` / `collection`) and get their own schema below.
20
+ *
21
+ * NOTE: there is deliberately **no `"text"`** here. `text` is a **CMS field** type
22
+ * (see `CMSFieldTypeSchema`), not a component prop type — the two vocabularies are
23
+ * separate and conflating them is the most common hand/AI-authoring mistake. Use
24
+ * `"string"` for a prop that holds plain text.
17
25
  */
18
26
  export const BasePropTypeSchema = z.enum([
19
27
  'string',
@@ -27,7 +35,8 @@ export const BasePropTypeSchema = z.enum([
27
35
  ]);
28
36
 
29
37
  /**
30
- * Full prop type schema including list
38
+ * Full prop type schema including the structured types (`list`, `reference`).
39
+ * Must stay in sync with the `PropType` union in `../types/components.ts`.
31
40
  */
32
41
  export const PropTypeSchema = z.enum([
33
42
  'string',
@@ -39,6 +48,7 @@ export const PropTypeSchema = z.enum([
39
48
  'rich-text',
40
49
  'embed',
41
50
  'list',
51
+ 'reference',
42
52
  ]);
43
53
 
44
54
  /**
@@ -125,6 +135,35 @@ export const ListPropDefinitionSchema = z
125
135
  })
126
136
  .passthrough();
127
137
 
138
+ /**
139
+ * CMS field types that are **not** component prop types, mapped to the prop type to
140
+ * use instead. Component props and CMS fields are two separate vocabularies
141
+ * (`PropTypeSchema` vs `CMSFieldTypeSchema`) that overlap on most names, so reaching
142
+ * for a CMS-only name in a `resolveProps(Astro, {…})` literal is the most common
143
+ * hand/AI-authoring mistake — and a silent one: the meno-astro codec round-trips it
144
+ * and `astro build` renders it, so it only ever surfaces as a failure to open the
145
+ * component in Studio. Naming the confusion turns that dead end into a one-line fix.
146
+ */
147
+ const CMS_ONLY_FIELD_TYPES: Record<string, string> = {
148
+ text: 'string',
149
+ image: 'file',
150
+ date: 'string',
151
+ };
152
+
153
+ /**
154
+ * Reference prop definition schema — a hand-picked selection of CMS items
155
+ * (`ReferencePropDefinition`). The instance value is an item id (single) or an
156
+ * ordered id array (`multiple: true`), so `default` accepts both shapes.
157
+ */
158
+ export const ReferencePropDefinitionSchema = z
159
+ .object({
160
+ type: z.literal('reference'),
161
+ collection: z.string(),
162
+ multiple: z.boolean().optional(),
163
+ default: z.union([z.string(), z.array(z.string())]).optional(),
164
+ })
165
+ .passthrough();
166
+
128
167
  /**
129
168
  * Prop definition schema — discriminates on `type` instead of a plain
130
169
  * `z.union([List, Base])`.
@@ -149,10 +188,51 @@ export const PropDefinitionSchema = z
149
188
  .object({ type: z.string() })
150
189
  .passthrough()
151
190
  .superRefine((val, ctx) => {
152
- const branch = val.type === 'list' ? ListPropDefinitionSchema : BasePropDefinitionSchema;
191
+ // A `type` that is not a PropType at all (typically a CMS field type such as
192
+ // `"text"` or `"image"`, whose vocabulary is separate — see CMS_ONLY_FIELD_TYPES)
193
+ // would otherwise be reported as a bare enum miss on `<prop>.type`, which never
194
+ // says WHY the guess was wrong. Name the confusion and the replacement instead.
195
+ const suggestion = CMS_ONLY_FIELD_TYPES[val.type as string];
196
+ if (suggestion) {
197
+ ctx.addIssue({
198
+ code: z.ZodIssueCode.custom,
199
+ path: ['type'],
200
+ message:
201
+ `"${val.type}" is a CMS field type, not a component prop type — ` +
202
+ `use "${suggestion}". Valid prop types: ${PropTypeSchema.options.join(', ')}`,
203
+ });
204
+ return;
205
+ }
206
+ const branch =
207
+ val.type === 'list'
208
+ ? ListPropDefinitionSchema
209
+ : val.type === 'reference'
210
+ ? ReferencePropDefinitionSchema
211
+ : BasePropDefinitionSchema;
153
212
  const result = branch.safeParse(val);
154
213
  if (result.success) return;
155
214
  if (val.type === 'list') {
215
+ // A CMS-only `type` on an ITEM field fails the list branch as a whole, so the
216
+ // collapse below would blame `itemSchema`/`default` for a one-word mistake in
217
+ // one field. Report the offending field instead, at its own path.
218
+ const itemSchema = (val as { itemSchema?: unknown }).itemSchema;
219
+ let named = false;
220
+ if (itemSchema && typeof itemSchema === 'object') {
221
+ for (const [field, def] of Object.entries(itemSchema as Record<string, unknown>)) {
222
+ const fieldType = (def as { type?: unknown } | null)?.type;
223
+ const fix = typeof fieldType === 'string' ? CMS_ONLY_FIELD_TYPES[fieldType] : undefined;
224
+ if (!fix) continue;
225
+ named = true;
226
+ ctx.addIssue({
227
+ code: z.ZodIssueCode.custom,
228
+ path: ['itemSchema', field, 'type'],
229
+ message:
230
+ `"${fieldType}" is a CMS field type, not a component prop type — ` +
231
+ `use "${fix}". Valid list-item field types: ${BasePropTypeSchema.options.join(', ')}`,
232
+ });
233
+ }
234
+ }
235
+ if (named) return;
156
236
  // Collapse the list-prop failure into one actionable message rather than
157
237
  // replaying zod's per-field noise. `itemSchema` is required and `default`
158
238
  // must be an array of OBJECTS (one `{ field: value }` record per item).
@@ -164,6 +244,15 @@ export const PropDefinitionSchema = z
164
244
  });
165
245
  return;
166
246
  }
247
+ if (val.type === 'reference') {
248
+ ctx.addIssue({
249
+ code: z.ZodIssueCode.custom,
250
+ message:
251
+ 'reference prop requires a `collection` name, and `default` must be an item ' +
252
+ 'id string (or an array of ids when `multiple` is true)',
253
+ });
254
+ return;
255
+ }
167
256
  // Non-list props keep their precise per-field errors.
168
257
  for (const issue of result.error.issues) ctx.addIssue(issue);
169
258
  });
@@ -202,9 +291,16 @@ export const BooleanMappingSchema = z
202
291
  export const VerbatimCodeSchema = z.object({ _code: z.literal(true), expr: z.string() }).passthrough();
203
292
 
204
293
  /**
205
- * If condition schema - can be boolean, string template, mapping, or a verbatim-code marker
294
+ * If condition schema - can be boolean, string template, mapping, an `_i18n` object
295
+ * (per-locale visibility: `{ _i18n: true, en: true, pl: false }`), or a verbatim-code marker
206
296
  */
207
- export const IfConditionSchema = z.union([z.boolean(), z.string(), BooleanMappingSchema, VerbatimCodeSchema]);
297
+ export const IfConditionSchema = z.union([
298
+ z.boolean(),
299
+ z.string(),
300
+ BooleanMappingSchema,
301
+ I18nValueObjectSchema,
302
+ VerbatimCodeSchema,
303
+ ]);
208
304
 
209
305
  /**
210
306
  * Link mapping schema - for link node href mappings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meno-core",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "meno": "./dist/bin/cli.js"
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../lib/shared/i18n.ts"],
4
- "sourcesContent": ["/**\n * Internationalization (i18n) utilities\n * Handles inline translation resolution for component props\n */\n\nimport type { I18nValue, I18nConfig, LocaleConfig } from './types/components';\n\n/**\n * Default i18n configuration\n */\nexport const DEFAULT_I18N_CONFIG: I18nConfig = {\n defaultLocale: 'en',\n locales: [{ code: 'en', name: 'English', nativeName: 'English', langTag: 'en-US' }],\n};\n\n// ============================================\n// Locale helper functions\n// ============================================\n\n/**\n * Get array of locale codes from config\n */\nexport function getLocaleCodes(config: I18nConfig): string[] {\n return config.locales.map((loc) => loc.code);\n}\n\n/**\n * Find a locale config by its code\n */\nexport function findLocaleByCode(config: I18nConfig, code: string): LocaleConfig | undefined {\n return config.locales.find((loc) => loc.code === code);\n}\n\n/**\n * Check if a locale code is valid/exists in config\n */\nexport function isValidLocaleCode(config: I18nConfig, code: string): boolean {\n return config.locales.some((loc) => loc.code === code);\n}\n\n// ============================================\n// Config Migration (old string[] -> new LocaleConfig[])\n// ============================================\n\n/**\n * Convert old locale format (string) to new format (LocaleConfig)\n */\nfunction migrateLocaleString(code: string): LocaleConfig {\n const upperCode = code.toUpperCase();\n return {\n code: code.toLowerCase(),\n name: upperCode,\n nativeName: upperCode,\n langTag: `${code.toLowerCase()}-${upperCode}`,\n };\n}\n\n/**\n * Check if locales array is in old string format\n */\nfunction isOldLocaleFormat(locales: unknown): locales is string[] {\n return Array.isArray(locales) && locales.length > 0 && typeof locales[0] === 'string';\n}\n\n/**\n * Migrate i18n config from old format to new format\n * Old: { defaultLocale: \"en\", locales: [\"en\", \"pl\"] }\n * New: { defaultLocale: \"en\", locales: [{ code: \"en\", name: \"EN\", ... }] }\n */\nexport function migrateI18nConfig(i18n: unknown): I18nConfig {\n if (!i18n || typeof i18n !== 'object') {\n return DEFAULT_I18N_CONFIG;\n }\n\n const config = i18n as Record<string, unknown>;\n const defaultLocale = (config.defaultLocale as string) || 'en';\n const locales = config.locales;\n\n if (!locales || !Array.isArray(locales)) {\n return DEFAULT_I18N_CONFIG;\n }\n\n // Migrate old string[] format\n if (isOldLocaleFormat(locales)) {\n return {\n defaultLocale,\n locales: locales.map(migrateLocaleString),\n };\n }\n\n // Already in new format\n return {\n defaultLocale,\n locales: locales as LocaleConfig[],\n };\n}\n\n/**\n * Type guard to check if a value is an I18nValue object\n */\nexport function isI18nValue(value: unknown): value is I18nValue {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n return '_i18n' in value && (value as Record<string, unknown>)._i18n === true;\n}\n\n/**\n * Resolve a single translation value for the given locale\n * Fallback order: exact locale -> default locale -> first available -> empty\n * Works with any value type (strings, arrays for list props, etc.)\n */\nexport function resolveTranslation(value: I18nValue, locale: string, config: I18nConfig): unknown {\n // Try exact locale match (any type - string, array, etc.)\n if (locale in value && value[locale] !== undefined && locale !== '_i18n') {\n return value[locale];\n }\n\n // Try default locale\n if (config.defaultLocale in value && value[config.defaultLocale] !== undefined) {\n return value[config.defaultLocale];\n }\n\n // Get first available value (skip _i18n marker)\n for (const key of Object.keys(value)) {\n if (key !== '_i18n' && value[key] !== undefined) {\n return value[key];\n }\n }\n\n // Return empty string for string i18n, empty array for list i18n\n // Check if any value is an array to determine the type\n const hasArrayValue = Object.entries(value).some(([k, v]) => k !== '_i18n' && Array.isArray(v));\n return hasArrayValue ? [] : '';\n}\n\n/**\n * Resolve a value that might be an I18nValue or a regular value\n * Returns the original value if not an I18nValue\n */\nexport function resolveI18nValue(value: unknown, locale: string, config: I18nConfig): unknown {\n if (isI18nValue(value)) {\n return resolveTranslation(value, locale, config);\n }\n return value;\n}\n\n/**\n * Recursively resolve all I18nValue objects in a props object\n */\nexport function resolveI18nInProps(\n props: Record<string, unknown>,\n locale: string,\n config: I18nConfig,\n): Record<string, unknown> {\n const resolved: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(props)) {\n if (isI18nValue(value)) {\n resolved[key] = resolveTranslation(value, locale, config);\n } else if (Array.isArray(value)) {\n resolved[key] = value.map((item) => (isI18nValue(item) ? resolveTranslation(item, locale, config) : item));\n } else if (typeof value === 'object' && value !== null) {\n // Recursively resolve nested objects (but not I18nValue which is already handled)\n resolved[key] = resolveI18nInProps(value as Record<string, unknown>, locale, config);\n } else {\n resolved[key] = value;\n }\n }\n\n return resolved;\n}\n\n/**\n * Extract locale from URL path prefix\n * e.g., '/en/about' -> 'en', '/pl/' -> 'pl', '/about' -> null\n */\nexport function extractLocaleFromPath(\n path: string,\n config: I18nConfig,\n): { locale: string | null; pathWithoutLocale: string } {\n // Remove leading slash and split\n const cleanPath = path.startsWith('/') ? path.slice(1) : path;\n const segments = cleanPath.split('/');\n\n const firstSegment = segments[0];\n if (firstSegment !== undefined && isValidLocaleCode(config, firstSegment)) {\n const locale = firstSegment;\n const pathWithoutLocale = `/${segments.slice(1).join('/')}`;\n return { locale, pathWithoutLocale: pathWithoutLocale || '/' };\n }\n\n return { locale: null, pathWithoutLocale: path };\n}\n\n/**\n * Build a localized path\n * e.g., ('/about', 'pl') -> '/pl/about'\n */\nexport function buildLocalizedPath(path: string, locale: string): string {\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return `/${locale}${cleanPath === '/' ? '' : cleanPath}`;\n}\n\n/**\n * Locale context containing resolved locale info\n */\nexport interface LocaleContext {\n /** The effective locale (never null) */\n locale: string;\n /** Path with locale prefix removed */\n pathWithoutLocale: string;\n /** Whether the current locale is the default */\n isDefaultLocale: boolean;\n}\n\n/**\n * Parse locale from path and return full context with effective locale\n * Combines extractLocaleFromPath + defaultLocale resolution in one call\n */\nexport function parseLocaleFromPath(path: string, config: I18nConfig): LocaleContext {\n const { locale, pathWithoutLocale } = extractLocaleFromPath(path, config);\n const effectiveLocale = locale || config.defaultLocale;\n\n return {\n locale: effectiveLocale,\n pathWithoutLocale,\n isDefaultLocale: effectiveLocale === config.defaultLocale,\n };\n}\n\n// ============================================\n// Client-side locale persistence utilities\n// ============================================\n\nconst LOCALE_STORAGE_KEY = 'meno_locale_preference';\nconst OLD_LOCALE_STORAGE_KEY = 'uplo_locale_preference';\n\n/**\n * Migrate locale storage key from old to new (one-time migration)\n */\nfunction migrateLocaleStorageKey(): void {\n if (typeof window === 'undefined') return;\n try {\n const oldValue = localStorage.getItem(OLD_LOCALE_STORAGE_KEY);\n const newValue = localStorage.getItem(LOCALE_STORAGE_KEY);\n if (oldValue && !newValue) {\n localStorage.setItem(LOCALE_STORAGE_KEY, oldValue);\n localStorage.removeItem(OLD_LOCALE_STORAGE_KEY);\n }\n } catch {\n // Storage not available\n }\n}\n\n/**\n * Get stored locale preference from localStorage (client-side only)\n */\nexport function getStoredLocale(): string | null {\n if (typeof window === 'undefined') return null;\n try {\n // Migrate from old key if needed\n migrateLocaleStorageKey();\n return localStorage.getItem(LOCALE_STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\n/**\n * Store locale preference to localStorage (client-side only)\n */\nexport function setStoredLocale(locale: string): void {\n if (typeof window === 'undefined') return;\n try {\n localStorage.setItem(LOCALE_STORAGE_KEY, locale);\n } catch {\n // Storage not available\n }\n}\n\n/**\n * Clear stored locale preference (client-side only)\n */\nexport function clearStoredLocale(): void {\n if (typeof window === 'undefined') return;\n try {\n localStorage.removeItem(LOCALE_STORAGE_KEY);\n } catch {\n // Storage not available\n }\n}\n"],
5
- "mappings": ";AAUO,IAAM,sBAAkC;AAAA,EAC7C,eAAe;AAAA,EACf,SAAS,CAAC,EAAE,MAAM,MAAM,MAAM,WAAW,YAAY,WAAW,SAAS,QAAQ,CAAC;AACpF;AASO,SAAS,eAAe,QAA8B;AAC3D,SAAO,OAAO,QAAQ,IAAI,CAAC,QAAQ,IAAI,IAAI;AAC7C;AAKO,SAAS,iBAAiB,QAAoB,MAAwC;AAC3F,SAAO,OAAO,QAAQ,KAAK,CAAC,QAAQ,IAAI,SAAS,IAAI;AACvD;AAKO,SAAS,kBAAkB,QAAoB,MAAuB;AAC3E,SAAO,OAAO,QAAQ,KAAK,CAAC,QAAQ,IAAI,SAAS,IAAI;AACvD;AASA,SAAS,oBAAoB,MAA4B;AACvD,QAAM,YAAY,KAAK,YAAY;AACnC,SAAO;AAAA,IACL,MAAM,KAAK,YAAY;AAAA,IACvB,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,SAAS,GAAG,KAAK,YAAY,CAAC,IAAI,SAAS;AAAA,EAC7C;AACF;AAKA,SAAS,kBAAkB,SAAuC;AAChE,SAAO,MAAM,QAAQ,OAAO,KAAK,QAAQ,SAAS,KAAK,OAAO,QAAQ,CAAC,MAAM;AAC/E;AAOO,SAAS,kBAAkB,MAA2B;AAC3D,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,WAAO;AAAA,EACT;AAEA,QAAM,SAAS;AACf,QAAM,gBAAiB,OAAO,iBAA4B;AAC1D,QAAM,UAAU,OAAO;AAEvB,MAAI,CAAC,WAAW,CAAC,MAAM,QAAQ,OAAO,GAAG;AACvC,WAAO;AAAA,EACT;AAGA,MAAI,kBAAkB,OAAO,GAAG;AAC9B,WAAO;AAAA,MACL;AAAA,MACA,SAAS,QAAQ,IAAI,mBAAmB;AAAA,IAC1C;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAKO,SAAS,YAAY,OAAoC;AAC9D,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,WAAO;AAAA,EACT;AACA,SAAO,WAAW,SAAU,MAAkC,UAAU;AAC1E;AAOO,SAAS,mBAAmB,OAAkB,QAAgB,QAA6B;AAEhG,MAAI,UAAU,SAAS,MAAM,MAAM,MAAM,UAAa,WAAW,SAAS;AACxE,WAAO,MAAM,MAAM;AAAA,EACrB;AAGA,MAAI,OAAO,iBAAiB,SAAS,MAAM,OAAO,aAAa,MAAM,QAAW;AAC9E,WAAO,MAAM,OAAO,aAAa;AAAA,EACnC;AAGA,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,QAAI,QAAQ,WAAW,MAAM,GAAG,MAAM,QAAW;AAC/C,aAAO,MAAM,GAAG;AAAA,IAClB;AAAA,EACF;AAIA,QAAM,gBAAgB,OAAO,QAAQ,KAAK,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,MAAM,WAAW,MAAM,QAAQ,CAAC,CAAC;AAC9F,SAAO,gBAAgB,CAAC,IAAI;AAC9B;AAMO,SAAS,iBAAiB,OAAgB,QAAgB,QAA6B;AAC5F,MAAI,YAAY,KAAK,GAAG;AACtB,WAAO,mBAAmB,OAAO,QAAQ,MAAM;AAAA,EACjD;AACA,SAAO;AACT;AAKO,SAAS,mBACd,OACA,QACA,QACyB;AACzB,QAAM,WAAoC,CAAC;AAE3C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,YAAY,KAAK,GAAG;AACtB,eAAS,GAAG,IAAI,mBAAmB,OAAO,QAAQ,MAAM;AAAA,IAC1D,WAAW,MAAM,QAAQ,KAAK,GAAG;AAC/B,eAAS,GAAG,IAAI,MAAM,IAAI,CAAC,SAAU,YAAY,IAAI,IAAI,mBAAmB,MAAM,QAAQ,MAAM,IAAI,IAAK;AAAA,IAC3G,WAAW,OAAO,UAAU,YAAY,UAAU,MAAM;AAEtD,eAAS,GAAG,IAAI,mBAAmB,OAAkC,QAAQ,MAAM;AAAA,IACrF,OAAO;AACL,eAAS,GAAG,IAAI;AAAA,IAClB;AAAA,EACF;AAEA,SAAO;AACT;AAMO,SAAS,sBACd,MACA,QACsD;AAEtD,QAAM,YAAY,KAAK,WAAW,GAAG,IAAI,KAAK,MAAM,CAAC,IAAI;AACzD,QAAM,WAAW,UAAU,MAAM,GAAG;AAEpC,QAAM,eAAe,SAAS,CAAC;AAC/B,MAAI,iBAAiB,UAAa,kBAAkB,QAAQ,YAAY,GAAG;AACzE,UAAM,SAAS;AACf,UAAM,oBAAoB,IAAI,SAAS,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC;AACzD,WAAO,EAAE,QAAQ,mBAAmB,qBAAqB,IAAI;AAAA,EAC/D;AAEA,SAAO,EAAE,QAAQ,MAAM,mBAAmB,KAAK;AACjD;AAMO,SAAS,mBAAmB,MAAc,QAAwB;AACvE,QAAM,YAAY,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI;AACxD,SAAO,IAAI,MAAM,GAAG,cAAc,MAAM,KAAK,SAAS;AACxD;AAkBO,SAAS,oBAAoB,MAAc,QAAmC;AACnF,QAAM,EAAE,QAAQ,kBAAkB,IAAI,sBAAsB,MAAM,MAAM;AACxE,QAAM,kBAAkB,UAAU,OAAO;AAEzC,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA,iBAAiB,oBAAoB,OAAO;AAAA,EAC9C;AACF;AAMA,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAK/B,SAAS,0BAAgC;AACvC,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,UAAM,WAAW,aAAa,QAAQ,sBAAsB;AAC5D,UAAM,WAAW,aAAa,QAAQ,kBAAkB;AACxD,QAAI,YAAY,CAAC,UAAU;AACzB,mBAAa,QAAQ,oBAAoB,QAAQ;AACjD,mBAAa,WAAW,sBAAsB;AAAA,IAChD;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAKO,SAAS,kBAAiC;AAC/C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AAEF,4BAAwB;AACxB,WAAO,aAAa,QAAQ,kBAAkB;AAAA,EAChD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,gBAAgB,QAAsB;AACpD,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,iBAAa,QAAQ,oBAAoB,MAAM;AAAA,EACjD,QAAQ;AAAA,EAER;AACF;AAKO,SAAS,oBAA0B;AACxC,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,iBAAa,WAAW,kBAAkB;AAAA,EAC5C,QAAQ;AAAA,EAER;AACF;",
6
- "names": []
7
- }