includio-cms 0.37.1 → 0.37.3

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 (75) hide show
  1. package/API.md +54 -4
  2. package/CHANGELOG.md +19 -0
  3. package/DOCS.md +1 -1
  4. package/ROADMAP.md +19 -0
  5. package/dist/admin/remote/booking.remote.js +1 -1
  6. package/dist/booking/payment-amount.d.ts +23 -0
  7. package/dist/booking/payment-amount.js +37 -0
  8. package/dist/booking/server/http/portal-handler.js +2 -1
  9. package/dist/booking/server/payments.d.ts +14 -4
  10. package/dist/booking/server/payments.js +11 -5
  11. package/dist/booking/server/portal.d.ts +5 -2
  12. package/dist/booking/server/portal.js +10 -4
  13. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  14. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  15. package/dist/core/cms.d.ts +4 -1
  16. package/dist/core/cms.js +9 -2
  17. package/dist/core/fields/jsonLd/builders.d.ts +74 -0
  18. package/dist/core/fields/jsonLd/builders.js +88 -0
  19. package/dist/core/fields/jsonLd/graph.d.ts +15 -0
  20. package/dist/core/fields/jsonLd/graph.js +43 -0
  21. package/dist/core/fields/jsonLd/index.d.ts +2 -0
  22. package/dist/core/fields/jsonLd/index.js +2 -0
  23. package/dist/core/fields/resolveSeo.d.ts +1 -1
  24. package/dist/core/fields/resolveSeo.js +1 -1
  25. package/dist/core/fields/slugPath.d.ts +1 -1
  26. package/dist/core/fields/slugPath.js +3 -2
  27. package/dist/core/fields/slugifyFilename.d.ts +7 -0
  28. package/dist/core/fields/slugifyFilename.js +14 -0
  29. package/dist/core/i18n/localeUrl.d.ts +20 -0
  30. package/dist/core/i18n/localeUrl.js +36 -0
  31. package/dist/core/index.d.ts +2 -0
  32. package/dist/core/index.js +3 -0
  33. package/dist/core/server/entries/operations/get.js +1 -1
  34. package/dist/core/server/entries/operations/resolveEntry.js +1 -1
  35. package/dist/core/server/fields/resolveRelationFields.js +1 -1
  36. package/dist/core/server/fields/resolveRichtextLinks.js +1 -1
  37. package/dist/core/server/fields/resolveUrlFields.js +1 -1
  38. package/dist/core/server/fields/slugResolver.d.ts +2 -2
  39. package/dist/core/server/fields/slugResolver.js +12 -4
  40. package/dist/core/server/fields/utils/fixOrphans.d.ts +15 -3
  41. package/dist/core/server/fields/utils/fixOrphans.js +30 -7
  42. package/dist/core/server/generator/generator.js +60 -1
  43. package/dist/files-local/index.js +5 -5
  44. package/dist/paraglide/messages/_index.d.ts +3 -36
  45. package/dist/paraglide/messages/_index.js +3 -71
  46. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  47. package/dist/paraglide/messages/hello_world.js +33 -0
  48. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  49. package/dist/paraglide/messages/login_hello.js +34 -0
  50. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  51. package/dist/paraglide/messages/login_please_login.js +34 -0
  52. package/dist/sveltekit/components/json-ld.svelte +12 -0
  53. package/dist/sveltekit/components/json-ld.svelte.d.ts +6 -0
  54. package/dist/sveltekit/components/seo.svelte +62 -11
  55. package/dist/sveltekit/components/seo.svelte.d.ts +10 -0
  56. package/dist/sveltekit/index.d.ts +2 -0
  57. package/dist/sveltekit/index.js +2 -0
  58. package/dist/sveltekit/server/handle.js +5 -5
  59. package/dist/sveltekit/server/index.d.ts +1 -0
  60. package/dist/sveltekit/server/index.js +2 -0
  61. package/dist/sveltekit/server/sitemap.d.ts +19 -0
  62. package/dist/sveltekit/server/sitemap.js +43 -0
  63. package/dist/types/cms.d.ts +3 -2
  64. package/dist/types/cms.schema.d.ts +10 -0
  65. package/dist/types/cms.schema.js +7 -1
  66. package/dist/types/languages.d.ts +13 -0
  67. package/dist/types/languages.js +2 -1
  68. package/dist/updates/0.37.2/index.d.ts +2 -0
  69. package/dist/updates/0.37.2/index.js +20 -0
  70. package/dist/updates/index.js +3 -1
  71. package/package.json +1 -1
  72. package/dist/paraglide/messages/en.d.ts +0 -5
  73. package/dist/paraglide/messages/en.js +0 -14
  74. package/dist/paraglide/messages/pl.d.ts +0 -5
  75. package/dist/paraglide/messages/pl.js +0 -14
@@ -0,0 +1,88 @@
1
+ import { thing, imageNode } from './graph.js';
2
+ export function organization(input) {
3
+ return thing('Organization', {
4
+ name: input.name,
5
+ url: input.url,
6
+ logo: input.logo ? imageNode(input.logo) : undefined,
7
+ sameAs: input.sameAs,
8
+ description: input.description
9
+ });
10
+ }
11
+ export function localBusiness(input) {
12
+ return thing('LocalBusiness', {
13
+ name: input.name,
14
+ url: input.url,
15
+ logo: input.logo ? imageNode(input.logo) : undefined,
16
+ image: input.image ? imageNode(input.image) : undefined,
17
+ description: input.description,
18
+ sameAs: input.sameAs,
19
+ telephone: input.telephone,
20
+ email: input.email,
21
+ address: input.address ? thing('PostalAddress', { ...input.address }) : undefined,
22
+ geo: input.geo ? thing('GeoCoordinates', { ...input.geo }) : undefined,
23
+ openingHours: input.openingHours,
24
+ priceRange: input.priceRange
25
+ });
26
+ }
27
+ export function website(input) {
28
+ return thing('WebSite', {
29
+ name: input.name,
30
+ url: input.url,
31
+ description: input.description,
32
+ inLanguage: input.inLanguage,
33
+ potentialAction: input.searchUrlTemplate
34
+ ? thing('SearchAction', {
35
+ target: input.searchUrlTemplate,
36
+ 'query-input': 'required name=search_term_string'
37
+ })
38
+ : undefined
39
+ });
40
+ }
41
+ export function webPage(input) {
42
+ return thing('WebPage', {
43
+ name: input.name,
44
+ url: input.url,
45
+ description: input.description,
46
+ inLanguage: input.inLanguage,
47
+ isPartOf: input.isPartOf ? { '@id': input.isPartOf } : undefined
48
+ });
49
+ }
50
+ export function breadcrumbList(items) {
51
+ return thing('BreadcrumbList', {
52
+ itemListElement: items.map((item, i) => thing('ListItem', { position: i + 1, name: item.name, item: item.url }))
53
+ });
54
+ }
55
+ function authorNode(author) {
56
+ return typeof author === 'string'
57
+ ? thing('Person', { name: author })
58
+ : thing('Person', { name: author.name, url: author.url });
59
+ }
60
+ function buildArticle(type, input) {
61
+ return thing(type, {
62
+ headline: input.headline,
63
+ url: input.url,
64
+ image: input.image ? imageNode(input.image) : undefined,
65
+ description: input.description,
66
+ datePublished: input.datePublished,
67
+ dateModified: input.dateModified,
68
+ author: input.author ? authorNode(input.author) : undefined,
69
+ publisher: input.publisher ? organization(input.publisher) : undefined,
70
+ inLanguage: input.inLanguage,
71
+ articleSection: input.section,
72
+ keywords: input.keywords
73
+ });
74
+ }
75
+ export function article(input) {
76
+ return buildArticle('Article', input);
77
+ }
78
+ export function blogPosting(input) {
79
+ return buildArticle('BlogPosting', input);
80
+ }
81
+ export function faqPage(items) {
82
+ return thing('FAQPage', {
83
+ mainEntity: items.map((qa) => thing('Question', {
84
+ name: qa.question,
85
+ acceptedAnswer: thing('Answer', { text: qa.answer })
86
+ }))
87
+ });
88
+ }
@@ -0,0 +1,15 @@
1
+ import type { MediaFile } from '../../../types/media.js';
2
+ export type JsonLdNode = Record<string, unknown> & {
3
+ '@type': string | string[];
4
+ };
5
+ /** Recursively drop undefined/null/'' and empty arrays/objects. */
6
+ export declare function pruneEmpty<T>(value: T): T;
7
+ /** Build a schema.org node of `type` from `props`, pruning empty values. */
8
+ export declare function thing(type: string | string[], props?: Record<string, unknown>): JsonLdNode;
9
+ /** Wrap node(s) into a schema.org @graph document. */
10
+ export declare function jsonLdGraph(nodes: JsonLdNode | JsonLdNode[]): {
11
+ '@context': 'https://schema.org';
12
+ '@graph': JsonLdNode[];
13
+ };
14
+ /** Normalize an image to a URL string or an ImageObject with dimensions. */
15
+ export declare function imageNode(img: string | MediaFile): string | JsonLdNode;
@@ -0,0 +1,43 @@
1
+ function isEmpty(v) {
2
+ if (v === undefined || v === null || v === '')
3
+ return true;
4
+ if (Array.isArray(v))
5
+ return v.length === 0;
6
+ if (typeof v === 'object')
7
+ return Object.keys(v).length === 0;
8
+ return false;
9
+ }
10
+ /** Recursively drop undefined/null/'' and empty arrays/objects. */
11
+ export function pruneEmpty(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.map((v) => pruneEmpty(v)).filter((v) => !isEmpty(v));
14
+ }
15
+ if (value && typeof value === 'object') {
16
+ const out = {};
17
+ for (const [k, v] of Object.entries(value)) {
18
+ const pv = pruneEmpty(v);
19
+ if (!isEmpty(pv))
20
+ out[k] = pv;
21
+ }
22
+ return out;
23
+ }
24
+ return value;
25
+ }
26
+ /** Build a schema.org node of `type` from `props`, pruning empty values. */
27
+ export function thing(type, props = {}) {
28
+ return pruneEmpty({ '@type': type, ...props });
29
+ }
30
+ /** Wrap node(s) into a schema.org @graph document. */
31
+ export function jsonLdGraph(nodes) {
32
+ return { '@context': 'https://schema.org', '@graph': Array.isArray(nodes) ? nodes : [nodes] };
33
+ }
34
+ /** Normalize an image to a URL string or an ImageObject with dimensions. */
35
+ export function imageNode(img) {
36
+ if (typeof img === 'string')
37
+ return img;
38
+ return thing('ImageObject', {
39
+ url: img.url,
40
+ width: img.width ?? undefined,
41
+ height: img.height ?? undefined
42
+ });
43
+ }
@@ -0,0 +1,2 @@
1
+ export * from './graph.js';
2
+ export * from './builders.js';
@@ -0,0 +1,2 @@
1
+ export * from './graph.js';
2
+ export * from './builders.js';
@@ -46,7 +46,7 @@ export type ResolvedSeo = {
46
46
  * ```svelte
47
47
  * <script lang="ts">
48
48
  * import { resolveSeo } from 'includio-cms/core';
49
- * import { Seo } from 'includio-cms';
49
+ * import { Seo } from 'includio-cms/sveltekit';
50
50
  * let { entry, language } = $props();
51
51
  * const seo = $derived(resolveSeo(entry, language));
52
52
  * </script>
@@ -45,7 +45,7 @@ void _SEO_KEYS_EXHAUSTIVE;
45
45
  * ```svelte
46
46
  * <script lang="ts">
47
47
  * import { resolveSeo } from 'includio-cms/core';
48
- * import { Seo } from 'includio-cms';
48
+ * import { Seo } from 'includio-cms/sveltekit';
49
49
  * let { entry, language } = $props();
50
50
  * const seo = $derived(resolveSeo(entry, language));
51
51
  * </script>
@@ -11,7 +11,7 @@ export declare function getSlugFromEntryData(data: Record<string, unknown>, slug
11
11
  * without doubling: neither the admin list (which used to print `//blog/x`) nor
12
12
  * a consumer `href` (which used to get a *relative* `blog/x`) has to patch it.
13
13
  */
14
- export declare function applyPathTemplate(pathTemplate: string | undefined, slug: string): string;
14
+ export declare function applyPathTemplate(pathTemplate: string | undefined, slug: string, prefix?: string): string;
15
15
  /** @internal Compose full entry URL; undefined when no resolvable slug. */
16
16
  export declare function resolveEntryUrl(opts: {
17
17
  slugField?: string;
@@ -30,12 +30,13 @@ const ABSOLUTE_URL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
30
30
  * without doubling: neither the admin list (which used to print `//blog/x`) nor
31
31
  * a consumer `href` (which used to get a *relative* `blog/x`) has to patch it.
32
32
  */
33
- export function applyPathTemplate(pathTemplate, slug) {
33
+ export function applyPathTemplate(pathTemplate, slug, prefix) {
34
34
  const raw = pathTemplate ? pathTemplate.replace('{slug}', slug) : slug;
35
35
  if (ABSOLUTE_URL_RE.test(raw))
36
36
  return raw;
37
37
  const collapsed = raw.replace(/\/{2,}/g, '/');
38
- return collapsed.startsWith('/') ? collapsed : `/${collapsed}`;
38
+ const path = collapsed.startsWith('/') ? collapsed : `/${collapsed}`;
39
+ return prefix ? `${prefix}${path}` : path;
39
40
  }
40
41
  /** @internal Compose full entry URL; undefined when no resolvable slug. */
41
42
  export function resolveEntryUrl(opts) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Slugify a media file base name (extension handled by the caller).
3
+ * Empty result falls back to `'file'`. `strict:true` removes dots/slashes,
4
+ * so no path-traversal survives. Capped at 200 chars (trailing `-` trimmed)
5
+ * to stay within filesystem name limits.
6
+ */
7
+ export declare function slugifyFilename(base: string): string;
@@ -0,0 +1,14 @@
1
+ import slugify from './slugify.js';
2
+ const MAX_FILENAME_LENGTH = 200;
3
+ /**
4
+ * Slugify a media file base name (extension handled by the caller).
5
+ * Empty result falls back to `'file'`. `strict:true` removes dots/slashes,
6
+ * so no path-traversal survives. Capped at 200 chars (trailing `-` trimmed)
7
+ * to stay within filesystem name limits.
8
+ */
9
+ export function slugifyFilename(base) {
10
+ const slug = slugify(base, { lower: true, strict: true, trim: true })
11
+ .slice(0, MAX_FILENAME_LENGTH)
12
+ .replace(/-+$/, '');
13
+ return slug || 'file';
14
+ }
@@ -0,0 +1,20 @@
1
+ import type { LanguageEntry } from '../../types/languages.js';
2
+ export type LocaleUrlEntry = {
3
+ strategy: 'pathPrefix';
4
+ pathPrefix: string;
5
+ } | {
6
+ strategy: 'host';
7
+ host: string;
8
+ };
9
+ export interface LocaleUrl {
10
+ defaultLocale: string;
11
+ byCode: Record<string, LocaleUrlEntry>;
12
+ /** [prefix, code] posortowane malejąco po długości prefiksu — do dopasowania z URL. */
13
+ byPrefix: Array<[string, string]>;
14
+ }
15
+ /** Buduje znormalizowaną mapę locale→URL z `cms.config.languages`. Jeden punkt prawdy. */
16
+ export declare function buildLocaleUrl(languages: LanguageEntry[]): LocaleUrl;
17
+ /** Locale storefrontu z pathname: najdłuższy pasujący `pathPrefix`, inaczej default. */
18
+ export declare function localeFromPath(localeUrl: LocaleUrl, pathname: string): string;
19
+ /** Prefiks ścieżki dla locale (pusty dla default i strategii host). */
20
+ export declare function pathPrefixFor(localeUrl: LocaleUrl, locale: string | undefined): string;
@@ -0,0 +1,36 @@
1
+ /** Buduje znormalizowaną mapę locale→URL z `cms.config.languages`. Jeden punkt prawdy. */
2
+ export function buildLocaleUrl(languages) {
3
+ const objs = languages.map((l) => typeof l === 'string' ? { code: l } : l);
4
+ const defaultLocale = (objs.find((o) => o.default) ?? objs[0]).code;
5
+ const byCode = {};
6
+ const byPrefix = [];
7
+ for (const o of objs) {
8
+ if (o.code === defaultLocale)
9
+ continue; // domyślny — bez wyróżnika URL
10
+ if (o.url?.host) {
11
+ byCode[o.code] = { strategy: 'host', host: o.url.host };
12
+ }
13
+ else {
14
+ const pathPrefix = o.url?.pathPrefix ?? `/${o.code}`;
15
+ byCode[o.code] = { strategy: 'pathPrefix', pathPrefix };
16
+ byPrefix.push([pathPrefix, o.code]);
17
+ }
18
+ }
19
+ byPrefix.sort((a, b) => b[0].length - a[0].length);
20
+ return { defaultLocale, byCode, byPrefix };
21
+ }
22
+ /** Locale storefrontu z pathname: najdłuższy pasujący `pathPrefix`, inaczej default. */
23
+ export function localeFromPath(localeUrl, pathname) {
24
+ for (const [prefix, code] of localeUrl.byPrefix) {
25
+ if (pathname === prefix || pathname.startsWith(prefix + '/'))
26
+ return code;
27
+ }
28
+ return localeUrl.defaultLocale;
29
+ }
30
+ /** Prefiks ścieżki dla locale (pusty dla default i strategii host). */
31
+ export function pathPrefixFor(localeUrl, locale) {
32
+ if (!locale || locale === localeUrl.defaultLocale)
33
+ return '';
34
+ const e = localeUrl.byCode[locale];
35
+ return e && e.strategy === 'pathPrefix' ? e.pathPrefix : '';
36
+ }
@@ -1,2 +1,4 @@
1
1
  export { resolveSeo, type ResolvedSeo } from './fields/resolveSeo.js';
2
2
  export type { EmailAdapter, SendMailOptions } from '../types/adapters/email.js';
3
+ export * from './fields/jsonLd/index.js';
4
+ export { slugifyFilename } from './fields/slugifyFilename.js';
@@ -3,3 +3,6 @@
3
3
  // browser/component bundle no longer pulls in `cms.js` and its server graph
4
4
  // ($env/dynamic/private).
5
5
  export { resolveSeo } from './fields/resolveSeo.js';
6
+ // JSON-LD (schema.org) builders + <JsonLd> primitives — client-safe, pure.
7
+ export * from './fields/jsonLd/index.js';
8
+ export { slugifyFilename } from './fields/slugifyFilename.js';
@@ -212,7 +212,7 @@ export const getEntryVersion = async (options) => {
212
212
  });
213
213
  const slugPath = getEntrySlugPath(dbEntry.slug);
214
214
  const entrySlug = getSlugFromEntryData(dbEntryVersion.data, slugPath, language);
215
- const _url = entrySlug ? getEntryPath(dbEntry.slug, entrySlug) : undefined;
215
+ const _url = entrySlug ? getEntryPath(dbEntry.slug, entrySlug, language) : undefined;
216
216
  const result = {
217
217
  _id: dbEntry.id,
218
218
  _slug: dbEntry.slug,
@@ -30,7 +30,7 @@ async function buildEntry(dbEntry, version, ctx) {
30
30
  const populated = await _populate(version.data, fields, ctx);
31
31
  const slugPath = getEntrySlugPath(dbEntry.slug);
32
32
  const slugValue = getSlugFromEntryData(version.data, slugPath, ctx.locale);
33
- const _url = slugValue ? getEntryPath(dbEntry.slug, slugValue) : undefined;
33
+ const _url = slugValue ? getEntryPath(dbEntry.slug, slugValue, ctx.locale) : undefined;
34
34
  const result = {
35
35
  _id: dbEntry.id,
36
36
  _slug: dbEntry.slug,
@@ -148,7 +148,7 @@ export async function resolveRelationFields(data, fields, ctx) {
148
148
  const populated = await _populate(picked.data, nestedFields, nestedCtx);
149
149
  const slugPath = getEntrySlugPath(dbEntry.slug);
150
150
  const slugValue = getSlugFromEntryData(picked.data, slugPath, ctx.locale);
151
- const _url = slugValue ? getEntryPath(dbEntry.slug, slugValue) : undefined;
151
+ const _url = slugValue ? getEntryPath(dbEntry.slug, slugValue, ctx.locale) : undefined;
152
152
  entriesMap[dbEntry.id] = {
153
153
  _id: dbEntry.id,
154
154
  _slug: dbEntry.slug,
@@ -109,7 +109,7 @@ export async function resolveRichtextLinks(data, fields, ctx) {
109
109
  const slugPath = configSlug ? getEntrySlugPath(configSlug) : 'seo.slug';
110
110
  const slug = getSlugFromEntryData(rawData, slugPath, language);
111
111
  if (slug) {
112
- slugMap[entryId] = configSlug ? getEntryPath(configSlug, slug) : slug;
112
+ slugMap[entryId] = configSlug ? getEntryPath(configSlug, slug, language) : slug;
113
113
  }
114
114
  }
115
115
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -130,7 +130,7 @@ export async function resolveUrlFields(data, fields, ctx) {
130
130
  const slugPath = getEntrySlugPath(entry.slug);
131
131
  const slug = getSlugFromEntryData(version.data, slugPath, language);
132
132
  if (slug)
133
- slugMap[entry.id] = getEntryPath(entry.slug, slug);
133
+ slugMap[entry.id] = getEntryPath(entry.slug, slug, language);
134
134
  }
135
135
  }
136
136
  }
@@ -1,5 +1,5 @@
1
1
  export { getSlugFromEntryData } from '../../fields/slugPath.js';
2
2
  /** Get dot-path to slug field for a collection. Falls back to 'seo.slug'. */
3
3
  export declare function getEntrySlugPath(configSlug: string): string;
4
- /** Build full entry path using collection's pathTemplate. */
5
- export declare function getEntryPath(configSlug: string, slug: string): string;
4
+ /** Build full entry path using collection's pathTemplate, prefixed per locale. */
5
+ export declare function getEntryPath(configSlug: string, slug: string, locale?: string): string;
@@ -1,5 +1,6 @@
1
1
  import { getCMS } from '../../cms.js';
2
2
  import { getSlugFromEntryData, resolveSlugPath, applyPathTemplate } from '../../fields/slugPath.js';
3
+ import { pathPrefixFor } from '../../i18n/localeUrl.js';
3
4
  export { getSlugFromEntryData } from '../../fields/slugPath.js';
4
5
  /** Get dot-path to slug field for a collection. Falls back to 'seo.slug'. */
5
6
  export function getEntrySlugPath(configSlug) {
@@ -10,12 +11,19 @@ export function getEntrySlugPath(configSlug) {
10
11
  return resolveSlugPath(undefined);
11
12
  }
12
13
  }
13
- /** Build full entry path using collection's pathTemplate. */
14
- export function getEntryPath(configSlug, slug) {
14
+ /** Build full entry path using collection's pathTemplate, prefixed per locale. */
15
+ export function getEntryPath(configSlug, slug, locale) {
16
+ let prefix = '';
15
17
  try {
16
- return applyPathTemplate(getCMS().getBySlug(configSlug).pathTemplate, slug);
18
+ prefix = pathPrefixFor(getCMS().localeUrl, locale);
17
19
  }
18
20
  catch {
19
- return applyPathTemplate(undefined, slug);
21
+ prefix = '';
22
+ }
23
+ try {
24
+ return applyPathTemplate(getCMS().getBySlug(configSlug).pathTemplate, slug, prefix);
25
+ }
26
+ catch {
27
+ return applyPathTemplate(undefined, slug, prefix);
20
28
  }
21
29
  }
@@ -1,5 +1,17 @@
1
1
  /**
2
- * Replace regular space after single-letter Polish conjunctions with non-breaking space.
3
- * Idempotent already-fixed text (with \u00A0) is not modified.
2
+ * Krótkie polskie wyrazy, które nie powinny zostawać na końcu wiersza.
3
+ * 1-literowe + wybrane przyimki 2–3-literowe. Konserwatywnie: bez spójników/partykuł
4
+ * (`nie oraz lub że bo to co`) — te można dołożyć przez parametr `words`.
4
5
  */
5
- export declare function fixOrphans(text: string): string;
6
+ export declare const DEFAULT_ORPHAN_WORDS: readonly string[];
7
+ /**
8
+ * Zamienia zwykłą spację po krótkim wyrazie na niełamliwą (NBSP, U+00A0).
9
+ * Idempotentna — tekst z NBSP nie jest ponownie modyfikowany (regex wymaga
10
+ * zwykłej spacji). Flaga `i` obsługuje oba przypadki wielkości; `$1` zachowuje
11
+ * oryginalną wielkość dopasowanego wyrazu.
12
+ *
13
+ * @param text - tekst do przetworzenia
14
+ * @param words - opcjonalna lista wyrazów-sierot (domyślnie {@link DEFAULT_ORPHAN_WORDS});
15
+ * rozszerzaj przez `[...DEFAULT_ORPHAN_WORDS, 'oraz']`
16
+ */
17
+ export declare function fixOrphans(text: string, words?: readonly string[]): string;
@@ -1,12 +1,35 @@
1
1
  /**
2
- * Polish orphan conjunctions that should not appear at line end.
3
- * Lowercase + uppercase variants of single-letter words.
2
+ * Krótkie polskie wyrazy, które nie powinny zostawać na końcu wiersza.
3
+ * 1-literowe + wybrane przyimki 2–3-literowe. Konserwatywnie: bez spójników/partykuł
4
+ * (`nie oraz lub że bo to co`) — te można dołożyć przez parametr `words`.
4
5
  */
5
- const ORPHAN_REGEX = /(?<=\s|^)([iIwWzZoOuUaAeE]) (?=\S)/g;
6
+ export const DEFAULT_ORPHAN_WORDS = [
7
+ 'a', 'e', 'i', 'o', 'u', 'w', 'z',
8
+ 'do', 'na', 'od', 'po', 'we', 'za', 'ze', 'ku',
9
+ 'nad', 'pod', 'bez', 'dla', 'przy'
10
+ ];
11
+ // Non-breaking space (U+00A0), built from char code to keep the source ASCII-only.
12
+ const NBSP = String.fromCharCode(0xa0);
13
+ function escapeRegex(s) {
14
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
+ }
16
+ function buildOrphanRegex(words) {
17
+ const alt = [...words]
18
+ .sort((a, b) => b.length - a.length)
19
+ .map(escapeRegex)
20
+ .join('|');
21
+ return new RegExp(`(?<=\\s|^)(${alt}) (?=\\S)`, 'gi');
22
+ }
6
23
  /**
7
- * Replace regular space after single-letter Polish conjunctions with non-breaking space.
8
- * Idempotentalready-fixed text (with \u00A0) is not modified.
24
+ * Zamienia zwykłą spację po krótkim wyrazie na niełamliwą (NBSP, U+00A0).
25
+ * Idempotentnatekst z NBSP nie jest ponownie modyfikowany (regex wymaga
26
+ * zwykłej spacji). Flaga `i` obsługuje oba przypadki wielkości; `$1` zachowuje
27
+ * oryginalną wielkość dopasowanego wyrazu.
28
+ *
29
+ * @param text - tekst do przetworzenia
30
+ * @param words - opcjonalna lista wyrazów-sierot (domyślnie {@link DEFAULT_ORPHAN_WORDS});
31
+ * rozszerzaj przez `[...DEFAULT_ORPHAN_WORDS, 'oraz']`
9
32
  */
10
- export function fixOrphans(text) {
11
- return text.replace(ORPHAN_REGEX, '$1\u00A0');
33
+ export function fixOrphans(text, words = DEFAULT_ORPHAN_WORDS) {
34
+ return text.replace(buildOrphanRegex(words), `$1${NBSP}`);
12
35
  }
@@ -5,6 +5,8 @@ import { generateTsTypeFromFormFields } from './formFields.js';
5
5
  import { generateZodSchemaStringFromFormFieldsAsString } from './formFieldSchemaToString.js';
6
6
  import { toPascalCase, quoteKey } from './utils.js';
7
7
  import { getFieldsFromConfig } from '../../fields/layoutUtils.js';
8
+ import { buildLocaleUrl } from '../../i18n/localeUrl.js';
9
+ import { languageCode } from '../../../types/languages.js';
8
10
  function createCmsRuntimeDir() {
9
11
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
10
12
  mkdirSync(cmsDir, { recursive: true });
@@ -171,7 +173,7 @@ function generateTypes(config) {
171
173
  }
172
174
  }
173
175
  }
174
- code += `\n export type SiteLanguage = ${config.languages.map((lang) => JSON.stringify(lang)).join(' | ')};\n\n`;
176
+ code += `\n export type SiteLanguage = ${config.languages.map((lang) => JSON.stringify(languageCode(lang))).join(' | ')};\n\n`;
175
177
  writeIfChanged(filePath, code);
176
178
  }
177
179
  function generateAPI(config) {
@@ -307,6 +309,62 @@ function generateRemote(config) {
307
309
  });
308
310
  writeIfChanged(filePath, code);
309
311
  }
312
+ /**
313
+ * Emituje konfigurację app-owego Paraglide z `cms.config.languages` — jedno źródło prawdy i18n.
314
+ * `project.inlang/settings.json` (baseLocale=default, locales) + `src/lib/cms/runtime/i18n.ts`
315
+ * (`paraglideUrlPatterns` do vite.config + `reroute` do hooks). Konsument importuje te artefakty
316
+ * zamiast utrzymywać osobny, ręcznie synchronizowany config.
317
+ */
318
+ function generateI18n(config) {
319
+ const lu = buildLocaleUrl(config.languages);
320
+ const codes = config.languages.map((l) => typeof l === 'string' ? l : l.code);
321
+ // 1) project.inlang/settings.json
322
+ const settings = {
323
+ $schema: 'https://inlang.com/schema/project-settings',
324
+ modules: [
325
+ 'https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js',
326
+ 'https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js'
327
+ ],
328
+ 'plugin.inlang.messageFormat': { pathPattern: './messages/{locale}.json' },
329
+ baseLocale: lu.defaultLocale,
330
+ locales: codes
331
+ };
332
+ const inlangDir = join(process.cwd(), 'project.inlang');
333
+ mkdirSync(inlangDir, { recursive: true });
334
+ writeIfChanged(join(inlangDir, 'settings.json'), JSON.stringify(settings, null, '\t') + '\n');
335
+ createCmsRuntimeDir();
336
+ // 2) src/lib/cms/runtime/i18n.ts — CZYSTE dane urlPatterns (bez importów paraglide),
337
+ // żeby dało się importować w vite.config.ts (paraglide runtime jeszcze nie istnieje przy config-load).
338
+ // KOLEJNOŚĆ: prefiksowane locale NAJPIERW, domyślny (catch-all `/:path`) OSTATNI — inaczej
339
+ // catch-all złapałby `/en/...` przed wzorcem `/en`.
340
+ const ordered = [...codes].sort((a, b) => {
341
+ const aDef = a === lu.defaultLocale ? 1 : 0;
342
+ const bDef = b === lu.defaultLocale ? 1 : 0;
343
+ return aDef - bDef;
344
+ });
345
+ const localized = ordered
346
+ .map((c) => {
347
+ const e = lu.byCode[c];
348
+ const p = e && e.strategy === 'pathPrefix' ? e.pathPrefix : '';
349
+ return `\t\t\t['${c}', '${p}/:path(.*)?']`;
350
+ })
351
+ .join(',\n');
352
+ // Buduj specyfikator `$lib/...` przez konkatenację — inaczej svelte-package (build rdzenia)
353
+ // potraktuje literał jak import w tym pliku i przepisze go na relatywną ścieżkę RDZENIA.
354
+ const paraglideRuntime = '$' + 'lib/paraglide/runtime';
355
+ const i18n = `// AUTOGENEROWANE przez includio-cms (generateRuntime). Nie edytuj ręcznie.\n` +
356
+ `// \`import type\` jest wymazywany przy buildzie → można importować w vite.config.ts bez cyklu runtime.\n` +
357
+ `import type { Locale } from '${paraglideRuntime}';\n\n` +
358
+ `export const paraglideUrlPatterns: Array<{ pattern: string; localized: Array<[Locale, string]> }> = [\n` +
359
+ `\t{\n\t\tpattern: '/:path(.*)?',\n\t\tlocalized: [\n${localized}\n\t\t]\n\t}\n];\n`;
360
+ writeIfChanged(join(process.cwd(), 'src/lib/cms/runtime/i18n.ts'), i18n);
361
+ // 3) src/lib/cms/runtime/reroute.ts — reroute (importuje paraglide runtime), do wpięcia w hooks.ts
362
+ const reroute = `// AUTOGENEROWANE przez includio-cms (generateRuntime). Nie edytuj ręcznie.\n` +
363
+ `import { deLocalizeUrl } from '${paraglideRuntime}';\n` +
364
+ `import type { Reroute } from '@sveltejs/kit';\n\n` +
365
+ `export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname;\n`;
366
+ writeIfChanged(join(process.cwd(), 'src/lib/cms/runtime/reroute.ts'), reroute);
367
+ }
310
368
  export function generateRuntime(config) {
311
369
  // Build custom fields map from plugins for type generation
312
370
  const customFields = new Map();
@@ -325,4 +383,5 @@ export function generateRuntime(config) {
325
383
  generateSchemas(config);
326
384
  generateDrizzleSchema(config);
327
385
  generateRemote(config);
386
+ generateI18n(config);
328
387
  }
@@ -4,7 +4,7 @@ import { readFile, writeFile, rename, unlink, mkdir, readdir } from 'node:fs/pro
4
4
  import sharp from 'sharp';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { dirname } from 'path';
7
- import { sanitizeFilename } from './sanitizeFilename.js';
7
+ import { slugifyFilename } from '../core/fields/slugifyFilename.js';
8
8
  import { processVideo, setFfmpegPaths } from './video.js';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
@@ -60,7 +60,7 @@ export function local(config) {
60
60
  const id = randomUUID();
61
61
  const ext = extname(file.name); // File extension
62
62
  const rawName = basename(file.name, ext); // File name without extension
63
- let filename = sanitizeFilename(rawName);
63
+ let filename = slugifyFilename(rawName);
64
64
  let filepath = ''; // Full file path
65
65
  const MAX_COLLISION_ATTEMPTS = 100;
66
66
  let attempt = 0;
@@ -77,7 +77,7 @@ export function local(config) {
77
77
  try {
78
78
  await readFile(resolvedFilePath);
79
79
  // File exists, append counter
80
- filename = `${sanitizeFilename(rawName)}-${++attempt}`;
80
+ filename = `${slugifyFilename(rawName)}-${++attempt}`;
81
81
  }
82
82
  catch {
83
83
  // File doesn't exist, safe to use
@@ -147,7 +147,7 @@ export function local(config) {
147
147
  name: oldName
148
148
  };
149
149
  }
150
- const sanitizedNewName = sanitizeFilename(newName);
150
+ const sanitizedNewName = slugifyFilename(newName);
151
151
  const ext = extname(url);
152
152
  // Derive old filename from URL to avoid double extension
153
153
  const oldFilename = url.split('/').pop() || `${oldName}${ext}`;
@@ -199,7 +199,7 @@ export function local(config) {
199
199
  await ensureDir(privateDir);
200
200
  const ext = extname(file.name);
201
201
  const rawName = basename(file.name, ext);
202
- const safeName = sanitizeFilename(rawName);
202
+ const safeName = slugifyFilename(rawName);
203
203
  const filename = `${randomUUID().slice(0, 8)}-${safeName}${ext}`;
204
204
  const filepath = resolve(privateDir, filename);
205
205
  if (!filepath.startsWith(resolve(privateDir))) {
@@ -1,36 +1,3 @@
1
- export function hello_world(inputs: {
2
- name: NonNullable<unknown>;
3
- }, options?: {
4
- locale?: "en" | "pl";
5
- }): string;
6
- /**
7
- * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
8
- *
9
- * - Changing this function will be over-written by the next build.
10
- *
11
- * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
12
- * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
13
- *
14
- * @param {{}} inputs
15
- * @param {{ locale?: "en" | "pl" }} options
16
- * @returns {string}
17
- */
18
- declare function login_hello(inputs?: {}, options?: {
19
- locale?: "en" | "pl";
20
- }): string;
21
- /**
22
- * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
23
- *
24
- * - Changing this function will be over-written by the next build.
25
- *
26
- * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
27
- * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
28
- *
29
- * @param {{}} inputs
30
- * @param {{ locale?: "en" | "pl" }} options
31
- * @returns {string}
32
- */
33
- declare function login_please_login(inputs?: {}, options?: {
34
- locale?: "en" | "pl";
35
- }): string;
36
- export { login_hello as login.hello, login_please_login as login.please_login };
1
+ export * from "./hello_world.js";
2
+ export * from "./login_hello.js";
3
+ export * from "./login_please_login.js";